pi-cockpit 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/package.json +78 -73
- package/src/agent-scroll.ts +64 -0
- package/src/agents-store.ts +115 -56
- package/src/bash-bg-overlay.ts +387 -385
- package/src/bash-bg-store.ts +90 -90
- package/src/edit-guard.ts +402 -0
- package/src/extension-status.ts +25 -25
- package/src/footer.ts +9 -0
- package/src/index.ts +689 -140
- package/src/input-routing.ts +63 -0
- package/src/maestro-store.ts +273 -273
- package/src/model-picker.ts +149 -0
- package/src/mouse-reporting.ts +65 -0
- package/src/public/v1/events.ts +174 -136
- package/src/quiet-tools.ts +23 -75
- package/src/session-bar.ts +135 -0
- package/src/session-detail.ts +168 -0
- package/src/settings/cockpit-provider.ts +0 -15
- package/src/settings/i18n.ts +34 -2
- package/src/settings/settings-shell.ts +558 -66
- package/src/settings/ui-primitives.ts +59 -0
- package/src/settings-view.ts +22 -16
- package/src/sidebar-controller.ts +185 -3
- package/src/sidebar-render.ts +119 -26
- package/src/split-pane.ts +55 -12
- package/src/stack-widget.ts +28 -3
- package/src/supervision-store.ts +109 -0
- package/src/theme-picker.ts +255 -255
- package/src/thinking-fold.ts +86 -86
- package/src/tick-policy.ts +56 -54
- package/src/title-gen.ts +60 -60
- package/src/title-llm.ts +115 -115
- package/src/types.ts +9 -10
- package/src/viewport-stability.ts +101 -101
- package/src/viewport.ts +62 -62
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A responsive **agent cockpit** for [Pi](https://pi.dev): wide terminals get a docked Maestro operations sidebar, narrow terminals automatically fall back to the existing Todo and Agent widgets, and every layout keeps the Starship-style footer.
|
|
4
4
|
|
|
5
|
-
It is the third plugin of the `pi-maestro-flow` project (alongside `pi-maestro-flow` and `pi-maestro-teammate`). It is installed and registered with `pi-maestro-flow`, but it also runs standalone. Current version: **0.
|
|
5
|
+
It is the third plugin of the `pi-maestro-flow` project (alongside `pi-maestro-flow` and `pi-maestro-teammate`). It is installed and registered with `pi-maestro-flow`, but it also runs standalone. Current version: **0.9.1**.
|
|
6
6
|
|
|
7
7
|
The dock is a non-capturing top-right overlay. Cockpit reserves its columns by wrapping the active TUI renderer at runtime, so the Pi workspace reflows instead of rendering underneath it. No Pi source files are modified.
|
|
8
8
|
|
|
@@ -25,7 +25,7 @@ The dock is a non-capturing top-right overlay. Cockpit reserves its columns by w
|
|
|
25
25
|
`pi-cockpit` comes automatically with the orchestration layer — installing `pi-maestro-flow` pulls `pi-cockpit` and registers it into `settings.packages` on postinstall, no manual setup required. To use it on its own:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
pi install npm:pi-cockpit # standalone from npm
|
|
28
|
+
pi install npm:pi-cockpit@0.9.1 # standalone from npm
|
|
29
29
|
# or, for one run:
|
|
30
30
|
pi -e ./packages/pi-cockpit
|
|
31
31
|
```
|
|
@@ -104,7 +104,7 @@ Cockpit has no package dependency on `pi-maestro-flow`. It observes optional pro
|
|
|
104
104
|
- `title.showGit`: include the git branch tag (`git:main`, `git:detached`); read synchronously from `.git/HEAD`, so it costs no process spawn. Off by default.
|
|
105
105
|
- `title.showMaestro`: include the Maestro workflow status tag (`wf:running`, `wf:done`). Off by default.
|
|
106
106
|
- `title.maxLength`: hard cap on the composed title; the middle is ellided to keep the head and the working-state tail. Clamped to `20..200`; default `80`.
|
|
107
|
-
- `title.generationModel`: `"provider/model"` (e.g. `"maestro-qwen/qwen3.8-max-preview"`) of a model registered with the `/api-manager` command (or any other provider pi resolves). When set, the session title is generated by that model over the first completed turn (user prompt + assistant reply) through its OpenAI-compatible endpoint, with a 10s timeout. Empty (default) uses the offline rule-based extractor instead. Generation is best-effort — on any failure the title silently falls back to the rule-based one.
|
|
107
|
+
- `title.generationModel`: `"provider/model"` (e.g. `"maestro-qwen/qwen3.8-max-preview"`) of a model registered with the `/api-manager` command (or any other provider pi resolves). When set, the session title is generated by that model over the first completed turn (user prompt + assistant reply) through its OpenAI-compatible endpoint, with a 10s timeout. Empty (default) uses the offline rule-based extractor instead. Generation is best-effort — on any failure the title silently falls back to the rule-based one. Editable from the `/cockpit` settings overlay (`z` or cursor to the `title gen model` row, Enter, type, Enter to save; empty clears back to rule-based).
|
|
108
108
|
|
|
109
109
|
The tab title is `frame + pi - <session> - <working state>`. The session part follows Claude Code's chain (`sessionTitle ?? agentTitle ?? haikuTitle ?? default`): the `/session name` title wins, otherwise the generated title, otherwise a short session id. The frame is Claude Code's title chrome: `⠂`/`⠐` braille spinner while a turn runs, static `✳` when idle; failure replaces it with `✗`. On quit, Cockpit clears the title so no stale tab label lingers (Claude Code's `CLEAR_TERMINAL_TITLE`).
|
|
110
110
|
- `theme`: named theme override; empty string follows the Pi session theme.
|
|
@@ -112,6 +112,8 @@ The tab title is `frame + pi - <session> - <working state>`. The session part fo
|
|
|
112
112
|
## Commands
|
|
113
113
|
|
|
114
114
|
- `/cockpit` — opens the settings overlay.
|
|
115
|
+
- `/maestro-settings` — opens the unified settings shell (Cockpit, Flow, Teammate and integrations).
|
|
116
|
+
- `Alt+J` — opens the background Bash jobs overlay (live status, command, cwd, duration, output tail).
|
|
115
117
|
- `/cockpit sidebar` — reports the current sidebar mode, width, and density.
|
|
116
118
|
- `/cockpit sidebar auto|on|off` — selects dock behavior.
|
|
117
119
|
- `/cockpit sidebar resize` or `Ctrl+Shift+R` — enters temporary Resize mode. Left/Right adjusts one column, Shift+Left/Shift+Right adjusts four, Enter accepts, and Escape rolls back. Mouse reporting is active only during Resize mode.
|
|
@@ -120,7 +122,7 @@ The tab title is `frame + pi - <session> - <working state>`. The session part fo
|
|
|
120
122
|
|
|
121
123
|
## Sidebar compatibility
|
|
122
124
|
|
|
123
|
-
The split-pane wrapper depends on Pi's current TUI renderer shape and is verified against Pi `0.
|
|
125
|
+
The split-pane wrapper depends on Pi's current TUI renderer shape and is verified against Pi `0.83.0`. A render integration failure disables the split and retries the original renderer at full width.
|
|
124
126
|
|
|
125
127
|
Do not enable `pi-cockpit`'s dock and `pi-atelier@0.7.0`'s sidebar together. Both reserve columns by wrapping the same renderer, and `pi-atelier@0.7.0` does not participate in Cockpit's split-owner marker protocol. Use `"sidebar": { "mode": "off" }` when running Atelier.
|
|
126
128
|
|
package/package.json
CHANGED
|
@@ -1,75 +1,80 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
2
|
+
"name": "pi-cockpit",
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"tui",
|
|
11
|
+
"teammate",
|
|
12
|
+
"todo"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"files": [
|
|
16
|
+
"src/",
|
|
17
|
+
"themes/",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test --experimental-transform-types tests/config.test.ts tests/public-events.test.ts tests/maestro-store.test.ts tests/split-pane.test.ts tests/editor-bottom.test.ts tests/sidebar-render.test.ts tests/sidebar-controller.test.ts tests/viewport-stability.test.ts tests/quiet-tools.test.ts tests/edit-guard.test.ts tests/agents-store.test.ts tests/supervision-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/agent-scroll.test.ts tests/session-bar.test.ts tests/session-detail.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/input-routing.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/settings-view.test.ts tests/thinking-fold.test.ts tests/thinking-timer.test.ts tests/tick-policy.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts tests/model-picker.test.ts tests/title-gen.test.ts tests/title-llm.test.ts tests/settings-registry.test.ts tests/settings-coordinator.test.ts tests/settings-locale.test.ts tests/settings-shell.test.ts tests/settings-cockpit-provider.test.ts tests/settings-cross-package.test.ts tests/ui-primitives.test.ts tests/settings-shell-secret.test.ts tests/settings-shell-listcrud.test.ts",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"pi": {
|
|
25
|
+
"extensions": [
|
|
26
|
+
"./src/extension/index.ts"
|
|
27
|
+
],
|
|
28
|
+
"themes": [
|
|
29
|
+
"./themes"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"pi-maestro-settings-core": "0.1.0",
|
|
34
|
+
"proper-lockfile": "4.1.2"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-agent-core": "*",
|
|
38
|
+
"@earendil-works/pi-ai": "*",
|
|
39
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
40
|
+
"@earendil-works/pi-tui": "*",
|
|
41
|
+
"pi-maestro-teammate": "^1.6.0",
|
|
42
|
+
"typebox": "*"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"@earendil-works/pi-agent-core": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"@earendil-works/pi-ai": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"@earendil-works/pi-coding-agent": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"@earendil-works/pi-tui": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"pi-maestro-teammate": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"typebox": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@earendil-works/pi-agent-core": "0.83.0",
|
|
66
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
67
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
68
|
+
"@earendil-works/pi-tui": "0.83.0",
|
|
69
|
+
"@types/node": "^22.0.0",
|
|
70
|
+
"pi-maestro-teammate": "1.7.1",
|
|
71
|
+
"typebox": "1.3.7",
|
|
72
|
+
"typescript": "^5.5.0"
|
|
73
|
+
},
|
|
74
|
+
"main": "./src/index.ts",
|
|
75
|
+
"exports": {
|
|
76
|
+
".": "./src/index.ts",
|
|
77
|
+
"./v1/events": "./src/public/v1/events.ts",
|
|
78
|
+
"./src/*": "./src/*"
|
|
79
|
+
}
|
|
75
80
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-list scroll window.
|
|
3
|
+
*
|
|
4
|
+
* The below-input agent list is bounded by the terminal height. It follows the
|
|
5
|
+
* newest activity by default (auto-scroll as agents stream); Shift+↑/Shift+↓
|
|
6
|
+
* (with an empty composer, via a cockpit terminal-input hook) scroll up/down
|
|
7
|
+
* through the whole roster. Scrolling up pauses follow; reaching the bottom
|
|
8
|
+
* resumes it. Pure state math, no UI dependencies.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { panelRows } from "./viewport.ts";
|
|
12
|
+
|
|
13
|
+
export interface AgentScrollState {
|
|
14
|
+
/** Window start over the activity-ordered roster. */
|
|
15
|
+
offset: number;
|
|
16
|
+
/** When true the window tracks the newest rows (auto-scroll). */
|
|
17
|
+
following: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Window start for a state; follow pins the window to the newest rows. */
|
|
21
|
+
export function scrollWindowStart(total: number, budget: number, state: AgentScrollState): number {
|
|
22
|
+
if (total <= budget) return 0;
|
|
23
|
+
if (state.following) return total - budget;
|
|
24
|
+
return Math.max(0, Math.min(state.offset, total - budget));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Move the window by `delta` rows; reaching the bottom resumes follow. */
|
|
28
|
+
export function scrollBy(
|
|
29
|
+
state: AgentScrollState,
|
|
30
|
+
delta: number,
|
|
31
|
+
total: number,
|
|
32
|
+
budget: number,
|
|
33
|
+
): AgentScrollState {
|
|
34
|
+
if (total <= budget) return { offset: 0, following: true };
|
|
35
|
+
const start = scrollWindowStart(total, budget, state);
|
|
36
|
+
const next = Math.max(0, Math.min(start + delta, total - budget));
|
|
37
|
+
return { offset: next, following: next >= total - budget };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Tail-following window: newest rows first. */
|
|
41
|
+
export function followWindow(total: number, budget: number): AgentScrollState {
|
|
42
|
+
return { offset: Math.max(0, total - budget), following: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* How many agent rows the panel may render. Mirrors the roster widget's own
|
|
47
|
+
* budget: a width cap (6 wide / 3 narrow) plus one marker row, bounded by the
|
|
48
|
+
* terminal-height panel share.
|
|
49
|
+
*/
|
|
50
|
+
export function agentListWindowRows(
|
|
51
|
+
terminalColumns: number | undefined,
|
|
52
|
+
terminalRows: number | undefined,
|
|
53
|
+
total?: number,
|
|
54
|
+
): number {
|
|
55
|
+
const widthCap = (terminalColumns ?? 80) < 40 ? 3 : 6;
|
|
56
|
+
const panel = panelRows(terminalRows);
|
|
57
|
+
const panelBudget = Math.min(widthCap + 1, panel ?? widthCap + 1);
|
|
58
|
+
const bodyBudget = Math.max(1, panelBudget - 1); // roster header owns one row
|
|
59
|
+
// One combined overflow marker is paid from the body budget, preserving at
|
|
60
|
+
// least one actual agent row even in the minimum three-row panel.
|
|
61
|
+
return total !== undefined && total > bodyBudget
|
|
62
|
+
? Math.max(1, bodyBudget - 1)
|
|
63
|
+
: bodyBudget;
|
|
64
|
+
}
|
package/src/agents-store.ts
CHANGED
|
@@ -7,7 +7,9 @@ import type { AgentProgressSnapshot } from "pi-maestro-teammate/v1/types";
|
|
|
7
7
|
import { sanitizeExtensionStatusText } from "./extension-status.ts";
|
|
8
8
|
import type { AgentRow, AgentStatus } from "./types.ts";
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const STATUS_TEXT_MAX = 48;
|
|
11
|
+
/** Selected-session output kept for the expandable fixed detail region. */
|
|
12
|
+
export const SESSION_CONTENT_MAX = 2_048;
|
|
11
13
|
|
|
12
14
|
// Every string here originates in an LLM-authored teammate event. A raw newline
|
|
13
15
|
// would split one widget row into several physical terminal rows and a raw escape
|
|
@@ -18,10 +20,18 @@ function clean(raw: string | undefined): string {
|
|
|
18
20
|
return raw === undefined ? "" : sanitizeExtensionStatusText(raw);
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
function
|
|
23
|
+
function truncateText(raw: string, max: number): string {
|
|
22
24
|
const flat = clean(raw);
|
|
23
25
|
if (flat.length === 0) return "";
|
|
24
|
-
return flat.length >
|
|
26
|
+
return flat.length > max ? flat.slice(0, max - 1) + "…" : flat;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function truncateStatusText(raw: string): string {
|
|
30
|
+
return truncateText(raw, STATUS_TEXT_MAX);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function truncateSessionContent(raw: string): string {
|
|
34
|
+
return truncateText(raw, SESSION_CONTENT_MAX);
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
// Store inputs are compatibility-relaxed projections of the versioned public
|
|
@@ -47,29 +57,11 @@ export type MessagePayload = Partial<Omit<
|
|
|
47
57
|
export type CompletePayload = Pick<TeammateCompleteEvent, "correlationId" | "exitCode">
|
|
48
58
|
& Partial<Pick<TeammateCompleteEvent, "durationMs" | "wakeable" | "cancelled">>;
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*/
|
|
56
|
-
export const FAILED_LINGER_MS = 30_000;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* How long a sleeping (wakeable) row stays visible after completion.
|
|
60
|
-
*
|
|
61
|
-
* Matches the native widget's idle-hide window so both surfaces agree on
|
|
62
|
-
* when an idle agent stops occupying screen space.
|
|
63
|
-
*/
|
|
64
|
-
export const SLEEPING_LINGER_MS = 60_000;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* How long a terminated (cancelled/aborted) row stays on screen after it ends.
|
|
68
|
-
*
|
|
69
|
-
* Shorter than a failure: cancellation is expected, but the user should still
|
|
70
|
-
* see the × long enough to read which agent was taken down.
|
|
71
|
-
*/
|
|
72
|
-
export const TERMINATED_LINGER_MS = 15_000;
|
|
60
|
+
export const AGENT_LINGER_MS = 60_000;
|
|
61
|
+
/** Compatibility exports: every terminal state now uses one visible window. */
|
|
62
|
+
export const FAILED_LINGER_MS = AGENT_LINGER_MS;
|
|
63
|
+
export const SLEEPING_LINGER_MS = AGENT_LINGER_MS;
|
|
64
|
+
export const TERMINATED_LINGER_MS = AGENT_LINGER_MS;
|
|
73
65
|
|
|
74
66
|
/**
|
|
75
67
|
* How long a completed agent's tombstone suppresses self-healing.
|
|
@@ -158,11 +150,11 @@ function latestTool(tools: Array<string | { name?: string; status?: string }> |
|
|
|
158
150
|
const tool = tools.find((candidate) => typeof candidate === "object" && candidate?.status === "running")
|
|
159
151
|
?? tools.at(-1);
|
|
160
152
|
if (!tool) return undefined;
|
|
161
|
-
if (typeof tool === "string") return
|
|
153
|
+
if (typeof tool === "string") return truncateStatusText(tool);
|
|
162
154
|
const name = tool?.name?.trim();
|
|
163
155
|
if (!name) return undefined;
|
|
164
156
|
const status = tool.status?.trim();
|
|
165
|
-
return
|
|
157
|
+
return truncateStatusText(status && status !== "completed" ? `${name} (${status})` : name);
|
|
166
158
|
}
|
|
167
159
|
|
|
168
160
|
// Self-accumulating roster. The teammate extension only broadcasts deltas
|
|
@@ -175,6 +167,8 @@ export class AgentsStore {
|
|
|
175
167
|
private readonly roster = new Map<string, AgentRow>();
|
|
176
168
|
/** correlationId -> completion time; suppresses post-complete self-healing. */
|
|
177
169
|
private readonly completedAt = new Map<string, number>();
|
|
170
|
+
/** correlationId of the agent currently shown in teammate's viewing view. */
|
|
171
|
+
private viewingId: string | undefined;
|
|
178
172
|
|
|
179
173
|
private isTombstoned(id: string | undefined, now: number): boolean {
|
|
180
174
|
if (id === undefined) return false;
|
|
@@ -198,6 +192,29 @@ export class AgentsStore {
|
|
|
198
192
|
// tombstoned.
|
|
199
193
|
this.completedAt.delete(id);
|
|
200
194
|
const prev = this.roster.get(id);
|
|
195
|
+
const prevIsTerminal = prev?.status === "done"
|
|
196
|
+
|| prev?.status === "failed"
|
|
197
|
+
|| prev?.status === "terminated"
|
|
198
|
+
|| prev?.status === "sleeping";
|
|
199
|
+
const nextStatus = p.status === undefined
|
|
200
|
+
? prevIsTerminal ? "running" : prev?.status ?? "running"
|
|
201
|
+
: mapAgentStatus(p.status);
|
|
202
|
+
if ((clean(p.agent) || prev?.agent || "").startsWith("graph(") && nextStatus === "running") {
|
|
203
|
+
// Restarting a graph is authoritative for its retained descendants too;
|
|
204
|
+
// their next progress snapshot may reuse the same correlation ids.
|
|
205
|
+
const pending = [id];
|
|
206
|
+
const visited = new Set<string>();
|
|
207
|
+
while (pending.length > 0) {
|
|
208
|
+
const parent = pending.pop()!;
|
|
209
|
+
if (visited.has(parent)) continue;
|
|
210
|
+
visited.add(parent);
|
|
211
|
+
for (const candidate of this.roster.values()) {
|
|
212
|
+
if (candidate.parentCorrelationId !== parent) continue;
|
|
213
|
+
this.completedAt.delete(candidate.correlationId);
|
|
214
|
+
pending.push(candidate.correlationId);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
201
218
|
const row: AgentRow = {
|
|
202
219
|
...prev,
|
|
203
220
|
correlationId: id,
|
|
@@ -205,12 +222,12 @@ export class AgentsStore {
|
|
|
205
222
|
name: p.name === undefined ? prev?.name : clean(p.name),
|
|
206
223
|
role: deriveRole(p.agent, p.name),
|
|
207
224
|
task: prev?.task ?? clean(p.name),
|
|
208
|
-
status:
|
|
225
|
+
status: nextStatus,
|
|
209
226
|
phase: typeof p.phase === "string" ? clean(p.phase) : prev?.phase,
|
|
210
227
|
lastOutcome: p.lastOutcome
|
|
211
228
|
? {
|
|
212
229
|
status: p.lastOutcome.status,
|
|
213
|
-
...(p.lastOutcome.message ? { message:
|
|
230
|
+
...(p.lastOutcome.message ? { message: truncateStatusText(p.lastOutcome.message) } : {}),
|
|
214
231
|
settledAt: p.lastOutcome.settledAt,
|
|
215
232
|
}
|
|
216
233
|
: prev?.lastOutcome,
|
|
@@ -227,12 +244,16 @@ export class AgentsStore {
|
|
|
227
244
|
row.turnStartedAt = now;
|
|
228
245
|
delete row.finishedAt;
|
|
229
246
|
delete row.failedAt;
|
|
247
|
+
delete row.resultReadyAt;
|
|
230
248
|
} else if (row.status === "sleeping") {
|
|
231
249
|
row.finishedAt = row.lastOutcome?.settledAt ?? prev?.finishedAt ?? now;
|
|
232
250
|
} else if (row.status !== "done" && row.status !== "failed" && row.status !== "terminated") {
|
|
233
251
|
delete row.finishedAt;
|
|
234
252
|
}
|
|
235
253
|
this.roster.set(id, row);
|
|
254
|
+
if (row.status === "done" || row.status === "failed" || row.status === "terminated" || row.status === "sleeping") {
|
|
255
|
+
this.completedAt.set(id, row.finishedAt ?? now);
|
|
256
|
+
}
|
|
236
257
|
}
|
|
237
258
|
|
|
238
259
|
applyMessage(p: MessagePayload, now = Date.now()): void {
|
|
@@ -269,13 +290,16 @@ export class AgentsStore {
|
|
|
269
290
|
? { spawnedBy: p.correlationId }
|
|
270
291
|
: {}),
|
|
271
292
|
}, now);
|
|
293
|
+
// This same delta is the authoritative content for the self-healed row;
|
|
294
|
+
// a provisional sleeping seed must not tombstone itself before ingestion.
|
|
295
|
+
this.completedAt.delete(targetId);
|
|
272
296
|
}
|
|
273
297
|
const row = this.roster.get(targetId);
|
|
274
|
-
if (!row) return;
|
|
298
|
+
if (!row || this.isTombstoned(targetId, now)) return;
|
|
275
299
|
const progressActivity = p.progress?.find((progress) => progress.correlationId === targetId)?.lastActivityAt;
|
|
276
300
|
row.lastActivityAt = normalizeStartedAt(p.lastActivityAt ?? progressActivity, now);
|
|
277
301
|
const tail = p.lastMessage ?? p.message;
|
|
278
|
-
if (typeof tail === "string" && tail.length > 0) row.tail =
|
|
302
|
+
if (typeof tail === "string" && tail.length > 0) row.tail = truncateSessionContent(tail);
|
|
279
303
|
if (p.recentTools) {
|
|
280
304
|
const tool = latestTool(p.recentTools);
|
|
281
305
|
if (tool) row.activeTool = tool;
|
|
@@ -287,7 +311,7 @@ export class AgentsStore {
|
|
|
287
311
|
if (typeof p.outputTokens === "number") row.outputTokens = p.outputTokens;
|
|
288
312
|
if (typeof p.cacheReadTokens === "number") row.cacheReadTokens = p.cacheReadTokens;
|
|
289
313
|
if (typeof p.cacheWriteTokens === "number") row.cacheWriteTokens = p.cacheWriteTokens;
|
|
290
|
-
if (typeof p.error === "string") row.error =
|
|
314
|
+
if (typeof p.error === "string") row.error = truncateStatusText(p.error);
|
|
291
315
|
if (typeof p.requestedModel === "string") row.requestedModel = clean(p.requestedModel);
|
|
292
316
|
if (typeof p.resolvedModel === "string") row.resolvedModel = clean(p.resolvedModel);
|
|
293
317
|
if (Array.isArray(p.attemptedModels)) {
|
|
@@ -298,6 +322,10 @@ export class AgentsStore {
|
|
|
298
322
|
row.status = mapAgentStatus(p.status);
|
|
299
323
|
if (row.status === "done" || row.status === "failed" || row.status === "terminated") {
|
|
300
324
|
row.finishedAt ??= now;
|
|
325
|
+
this.completedAt.set(targetId, now);
|
|
326
|
+
} else if (row.status === "sleeping") {
|
|
327
|
+
row.finishedAt ??= now;
|
|
328
|
+
this.completedAt.set(targetId, now);
|
|
301
329
|
} else {
|
|
302
330
|
delete row.finishedAt;
|
|
303
331
|
}
|
|
@@ -319,10 +347,11 @@ export class AgentsStore {
|
|
|
319
347
|
for (const row of this.roster.values()) {
|
|
320
348
|
if (row.parentCorrelationId === id) pending.push(row.correlationId);
|
|
321
349
|
}
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
350
|
+
// Every terminal row remains visible for one minute. Tombstone it now so
|
|
351
|
+
// late progress cannot regress the retained row back to running; an
|
|
352
|
+
// explicit started event clears the tombstone for a genuine new turn.
|
|
325
353
|
const row = this.roster.get(id);
|
|
354
|
+
this.completedAt.set(id, now);
|
|
326
355
|
const cancelledById = id === p.correlationId && p.cancelled === true;
|
|
327
356
|
const failedByExitCode = id === p.correlationId
|
|
328
357
|
&& Number.isFinite(p.exitCode)
|
|
@@ -365,27 +394,30 @@ export class AgentsStore {
|
|
|
365
394
|
row.failedAt = now;
|
|
366
395
|
row.lastActivityAt = now;
|
|
367
396
|
delete row.activeTool;
|
|
368
|
-
} else {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
397
|
+
} else if (row) {
|
|
398
|
+
row.status = "done";
|
|
399
|
+
row.taskStatus = "completed";
|
|
400
|
+
row.finishedAt = terminalTime(row, id === p.correlationId ? p : {}, now);
|
|
401
|
+
row.lastOutcome = { status: "completed", settledAt: row.finishedAt };
|
|
402
|
+
row.lastActivityAt = now;
|
|
403
|
+
delete row.activeTool;
|
|
404
|
+
delete row.failedAt;
|
|
373
405
|
}
|
|
374
406
|
}
|
|
375
407
|
}
|
|
376
408
|
|
|
377
|
-
/** Drop
|
|
409
|
+
/** Drop terminal rows after their shared one-minute visible window. */
|
|
378
410
|
prune(now = Date.now()): boolean {
|
|
379
411
|
let changed = false;
|
|
380
412
|
for (const [id, row] of this.roster) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
} else if (row.status === "sleeping" && row.finishedAt !== undefined && now - row.finishedAt >= SLEEPING_LINGER_MS) {
|
|
413
|
+
const terminal = row.status === "done"
|
|
414
|
+
|| row.status === "failed"
|
|
415
|
+
|| row.status === "terminated"
|
|
416
|
+
|| row.status === "sleeping";
|
|
417
|
+
const settledAt = this.completedAt.get(id);
|
|
418
|
+
if (terminal && settledAt !== undefined && now - settledAt >= AGENT_LINGER_MS) {
|
|
388
419
|
this.roster.delete(id);
|
|
420
|
+
if (this.viewingId === id) this.viewingId = undefined;
|
|
389
421
|
changed = true;
|
|
390
422
|
}
|
|
391
423
|
}
|
|
@@ -395,17 +427,27 @@ export class AgentsStore {
|
|
|
395
427
|
return changed;
|
|
396
428
|
}
|
|
397
429
|
|
|
398
|
-
/** True while a
|
|
430
|
+
/** True while a terminal row is counting down, so the redraw loop can expire it. */
|
|
399
431
|
hasLingering(): boolean {
|
|
400
432
|
for (const row of this.roster.values()) {
|
|
401
|
-
if (row.
|
|
402
|
-
|
|
403
|
-
|
|
433
|
+
if (row.status === "done"
|
|
434
|
+
|| row.status === "failed"
|
|
435
|
+
|| row.status === "terminated"
|
|
436
|
+
|| row.status === "sleeping") return true;
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** True while any agent (foreground or background) is still running, so elapsed/stall repaints keep the loop alive. */
|
|
442
|
+
hasActive(): boolean {
|
|
443
|
+
for (const row of this.roster.values()) {
|
|
444
|
+
if (row.status === "running") return true;
|
|
404
445
|
}
|
|
405
446
|
return false;
|
|
406
447
|
}
|
|
407
448
|
|
|
408
449
|
private applyProgress(parentCorrelationId: string, p: ProgressPayload, now: number): void {
|
|
450
|
+
if (this.isTombstoned(p.correlationId, now)) return;
|
|
409
451
|
if (!this.roster.has(p.correlationId)
|
|
410
452
|
&& this.canMaterialize(p.correlationId, parentCorrelationId, now)) {
|
|
411
453
|
// Self-heal: see applyMessage. A graph child whose `started` delta was
|
|
@@ -440,6 +482,10 @@ export class AgentsStore {
|
|
|
440
482
|
row.status = mapAgentStatus(p.status);
|
|
441
483
|
if (row.status === "done" || row.status === "failed" || row.status === "terminated") {
|
|
442
484
|
row.finishedAt = terminalTime(row, p, now);
|
|
485
|
+
this.completedAt.set(p.correlationId, now);
|
|
486
|
+
} else if (row.status === "sleeping") {
|
|
487
|
+
row.finishedAt = terminalTime(row, p, now);
|
|
488
|
+
this.completedAt.set(p.correlationId, now);
|
|
443
489
|
} else {
|
|
444
490
|
delete row.finishedAt;
|
|
445
491
|
}
|
|
@@ -464,7 +510,7 @@ export class AgentsStore {
|
|
|
464
510
|
row.cacheReadTokens = p.cacheReadTokens;
|
|
465
511
|
row.cacheWriteTokens = p.cacheWriteTokens;
|
|
466
512
|
if (p.error === undefined) delete row.error;
|
|
467
|
-
else row.error =
|
|
513
|
+
else row.error = truncateStatusText(p.error);
|
|
468
514
|
if (p.requestedModel === undefined) delete row.requestedModel;
|
|
469
515
|
else row.requestedModel = clean(p.requestedModel);
|
|
470
516
|
if (p.resolvedModel === undefined) delete row.resolvedModel;
|
|
@@ -472,7 +518,7 @@ export class AgentsStore {
|
|
|
472
518
|
if (p.attemptedModels === undefined) delete row.attemptedModels;
|
|
473
519
|
else row.attemptedModels = p.attemptedModels.map((model) => clean(model)).filter(Boolean);
|
|
474
520
|
if (typeof p.lastMessage === "string" && p.lastMessage.length > 0) {
|
|
475
|
-
row.tail =
|
|
521
|
+
row.tail = truncateSessionContent(p.lastMessage);
|
|
476
522
|
}
|
|
477
523
|
}
|
|
478
524
|
|
|
@@ -483,7 +529,19 @@ export class AgentsStore {
|
|
|
483
529
|
return [...this.roster.values()].sort((a, b) => {
|
|
484
530
|
const activity = b.lastActivityAt - a.lastActivityAt;
|
|
485
531
|
return activity || a.correlationId.localeCompare(b.correlationId);
|
|
486
|
-
})
|
|
532
|
+
}).map((row) =>
|
|
533
|
+
row.correlationId === this.viewingId ? { ...row, viewing: true } : row,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Mark which agent the teammate viewing view currently shows. */
|
|
538
|
+
setViewingAgent(correlationId: string | undefined): void {
|
|
539
|
+
this.viewingId = correlationId;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Correlation id of the agent currently shown in the viewing view. */
|
|
543
|
+
getViewingAgent(): string | undefined {
|
|
544
|
+
return this.viewingId;
|
|
487
545
|
}
|
|
488
546
|
|
|
489
547
|
get size(): number {
|
|
@@ -497,5 +555,6 @@ export class AgentsStore {
|
|
|
497
555
|
clear(): void {
|
|
498
556
|
this.roster.clear();
|
|
499
557
|
this.completedAt.clear();
|
|
558
|
+
this.viewingId = undefined;
|
|
500
559
|
}
|
|
501
560
|
}
|