| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | |
| 6 | "reasonix/internal/control" |
| 7 | ) |
| 8 | |
| 9 | // contextRead binds every field in an overview response to one runtime. Reads |
| 10 | // must never re-key the tab's telemetry: an old read can finish after navigation. |
| 11 | type contextRead struct { |
| 12 | tabID string |
| 13 | tab *WorkspaceTab |
| 14 | ctrl control.SessionAPI |
| 15 | generation uint64 |
| 16 | sessionID string |
| 17 | storedPath string |
| 18 | controllerPath string |
| 19 | workspaceRoot string |
| 20 | telemetry tabTelemetrySnapshot |
| 21 | runtime sessionRuntime |
| 22 | } |
| 23 | |
| 24 | // sessionRuntime splits runtime into finished turns and the running turn's |
| 25 | // start, so a reader can keep counting between snapshots. |
| 26 | type sessionRuntime struct { |
| 27 | completedMs int64 |
| 28 | turnStartedAt int64 |
| 29 | } |
| 30 | |
| 31 | func (a *App) captureContextRead(tabID string) contextRead { |
| 32 | a.mu.RLock() |
| 33 | if strings.TrimSpace(tabID) == "" { |
| 34 | tabID = a.activeTabID |
| 35 | } |
| 36 | tab := a.tabByIDLocked(tabID) |
| 37 | if tab == nil { |
| 38 | a.mu.RUnlock() |
| 39 | return contextRead{} |
| 40 | } |
| 41 | r := contextRead{tabID: tabID, tab: tab, ctrl: tab.Ctrl, generation: tab.SessionGeneration, |
| 42 | sessionID: tab.SessionID, storedPath: tab.SessionPath, workspaceRoot: tab.WorkspaceRoot} |
| 43 | tab.telemMu.Lock() |
| 44 | key := tab.telemetrySessionKey |
| 45 | r.telemetry = tab.displayTelemetrySnapshotLocked() |
| 46 | r.runtime = sessionRuntime{completedMs: tab.usageTelemetry.ElapsedMs, turnStartedAt: tab.usageTelemetry.activeTurnStartedAt} |
| 47 | tab.telemMu.Unlock() |
| 48 | a.mu.RUnlock() |
| 49 | if r.ctrl != nil { |
| 50 | r.controllerPath = r.ctrl.SessionPath() |
| 51 | if r.controllerPath != "" && sessionRuntimeKey(r.controllerPath) != key { |
| 52 | // A legacy /new can rotate before the next event re-keys telemetry. |
| 53 | // Read its sidecar without mutating the live tab or wallet overlay. |
| 54 | r.telemetry = loadTelemetry(r.controllerPath + ".telemetry.json") |
| 55 | r.runtime = sessionRuntime{completedMs: r.telemetry.Usage.ElapsedMs} |
| 56 | } |
| 57 | } |
| 58 | return r |
| 59 | } |
| 60 | |
| 61 | func (r contextRead) current(a *App) bool { |
| 62 | if r.tab == nil { |
| 63 | return false |
| 64 | } |
| 65 | // Controller methods stay outside App.mu; some can re-enter the host. |
| 66 | if r.ctrl != nil && r.ctrl.SessionPath() != r.controllerPath { |
| 67 | return false |
| 68 | } |
| 69 | a.mu.RLock() |
| 70 | defer a.mu.RUnlock() |
| 71 | return a.tabs[r.tabID] == r.tab && !r.tab.removed && r.tab.Ctrl == r.ctrl && |
| 72 | r.tab.SessionGeneration == r.generation && r.tab.SessionID == r.sessionID && |
| 73 | r.tab.SessionPath == r.storedPath |
| 74 | } |
| 75 |