pi-cockpit 0.6.0 → 0.8.0
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 +22 -1
- package/package.json +75 -71
- package/src/agents-store.ts +155 -69
- package/src/ambient.ts +73 -16
- package/src/bash-bg-overlay.ts +30 -7
- package/src/bash-bg-widget.ts +54 -50
- package/src/config.ts +61 -1
- package/src/editor-bottom.ts +128 -0
- package/src/footer.ts +26 -3
- package/src/icons.ts +5 -1
- package/src/index.ts +504 -35
- package/src/layout.ts +181 -129
- package/src/quiet-tools.ts +367 -310
- package/src/render.ts +123 -25
- package/src/settings/cockpit-provider.ts +682 -0
- package/src/settings/coordinator.ts +323 -0
- package/src/settings/i18n.ts +150 -0
- package/src/settings/locale-state.ts +138 -0
- package/src/settings/registry.ts +160 -0
- package/src/settings/settings-shell.ts +1147 -0
- package/src/settings-view.ts +98 -0
- package/src/sidebar-controller.ts +319 -266
- package/src/sidebar-render.ts +413 -312
- package/src/split-pane.ts +382 -377
- package/src/stack-widget.ts +16 -12
- package/src/thinking-timer.ts +249 -224
- package/src/tick-policy.ts +54 -0
- package/src/title-gen.ts +60 -0
- package/src/title-llm.ts +115 -0
- package/src/types.ts +98 -5
- package/src/viewport-stability.ts +101 -0
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.8.0**.
|
|
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
|
|
|
@@ -73,6 +73,16 @@ Cockpit has no package dependency on `pi-maestro-flow`. It observes optional pro
|
|
|
73
73
|
"width": 40,
|
|
74
74
|
"density": "comfortable"
|
|
75
75
|
},
|
|
76
|
+
"title": {
|
|
77
|
+
"enabled": true,
|
|
78
|
+
"showSession": true,
|
|
79
|
+
"showCwd": false,
|
|
80
|
+
"showModel": false,
|
|
81
|
+
"showThinking": false,
|
|
82
|
+
"showGit": false,
|
|
83
|
+
"showMaestro": false,
|
|
84
|
+
"maxLength": 80
|
|
85
|
+
},
|
|
76
86
|
"theme": ""
|
|
77
87
|
}
|
|
78
88
|
```
|
|
@@ -86,6 +96,17 @@ Cockpit has no package dependency on `pi-maestro-flow`. It observes optional pro
|
|
|
86
96
|
- `sidebar.width`: persisted dock width, rounded and clamped to `32..56`; default `40`.
|
|
87
97
|
- `sidebar.density`: `"comfortable"` or `"compact"`.
|
|
88
98
|
- `icons.mode`: `"auto"` (detect Nerd Font), `"nerd"`, or `"ascii"`.
|
|
99
|
+
- `title.enabled`: master switch for the terminal tab title (session summary + working state + opt-in tags). On by default.
|
|
100
|
+
- `title.showSession`: include the session summary (the `session_info` name, else a short session id) right after `pi`. On by default.
|
|
101
|
+
- `title.showCwd`: include the working directory after the session. Off by default — the title stays short, since the tab strip has no room for a wall of tags.
|
|
102
|
+
- `title.showModel`: include the active model tag (`m:gpt-5.6-sol`). Off by default.
|
|
103
|
+
- `title.showThinking`: include the thinking level tag (`t:high`); skipped while off. Off by default.
|
|
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
|
+
- `title.showMaestro`: include the Maestro workflow status tag (`wf:running`, `wf:done`). Off by default.
|
|
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.
|
|
108
|
+
|
|
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`).
|
|
89
110
|
- `theme`: named theme override; empty string follows the Pi session theme.
|
|
90
111
|
|
|
91
112
|
## Commands
|
package/package.json
CHANGED
|
@@ -1,71 +1,75 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-cockpit",
|
|
3
|
-
"version": "0.
|
|
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/sidebar-render.test.ts tests/sidebar-controller.test.ts tests/quiet-tools.test.ts tests/agents-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/footer.test.ts tests/extension-status.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/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts",
|
|
22
|
-
"typecheck": "tsc --noEmit"
|
|
23
|
-
},
|
|
24
|
-
"pi": {
|
|
25
|
-
"extensions": [
|
|
26
|
-
"./src/extension/index.ts"
|
|
27
|
-
],
|
|
28
|
-
"themes": [
|
|
29
|
-
"./themes"
|
|
30
|
-
]
|
|
31
|
-
},
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"pi-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"@earendil-works/pi-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"@
|
|
62
|
-
"pi-
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-cockpit",
|
|
3
|
+
"version": "0.8.0",
|
|
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/agents-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/footer.test.ts tests/extension-status.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/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",
|
|
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
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@earendil-works/pi-agent-core": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"@earendil-works/pi-ai": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@earendil-works/pi-coding-agent": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@earendil-works/pi-tui": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"pi-maestro-teammate": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@earendil-works/pi-agent-core": "0.83.0",
|
|
62
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
63
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
64
|
+
"@earendil-works/pi-tui": "0.83.0",
|
|
65
|
+
"@types/node": "^22.0.0",
|
|
66
|
+
"pi-maestro-teammate": "1.6.0",
|
|
67
|
+
"typescript": "^5.5.0"
|
|
68
|
+
},
|
|
69
|
+
"main": "./src/index.ts",
|
|
70
|
+
"exports": {
|
|
71
|
+
".": "./src/index.ts",
|
|
72
|
+
"./v1/events": "./src/public/v1/events.ts",
|
|
73
|
+
"./src/*": "./src/*"
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/agents-store.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
TeammateCompleteEvent,
|
|
3
|
+
TeammateProgressMessageEvent,
|
|
4
|
+
TeammateStartedEvent,
|
|
5
|
+
} from "pi-maestro-teammate/v1/events";
|
|
6
|
+
import type { AgentProgressSnapshot } from "pi-maestro-teammate/v1/types";
|
|
2
7
|
import { sanitizeExtensionStatusText } from "./extension-status.ts";
|
|
3
8
|
import type { AgentRow, AgentStatus } from "./types.ts";
|
|
4
9
|
|
|
@@ -19,56 +24,28 @@ function truncateTail(raw: string): string {
|
|
|
19
24
|
return flat.length > TAIL_MAX ? flat.slice(0, TAIL_MAX - 1) + "…" : flat;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
taskIndex: number;
|
|
37
|
-
dependencies?: number[];
|
|
38
|
-
status?: string;
|
|
39
|
-
startedAt?: number | string;
|
|
40
|
-
completedAt?: number | string;
|
|
41
|
-
durationMs?: number;
|
|
42
|
-
lastActivityAt?: number | string;
|
|
27
|
+
// Store inputs are compatibility-relaxed projections of the versioned public
|
|
28
|
+
// contract. The event boundary validates discriminators and identifiers; the
|
|
29
|
+
// optional fields let older teammate versions continue to feed the same store.
|
|
30
|
+
export type StartedPayload = Pick<TeammateStartedEvent, "correlationId" | "agent">
|
|
31
|
+
& Partial<Omit<TeammateStartedEvent, "correlationId" | "agent" | "startedAt">>
|
|
32
|
+
& { startedAt?: number | string };
|
|
33
|
+
export type ProgressPayload = Pick<AgentProgressSnapshot, "correlationId" | "agent" | "taskIndex">
|
|
34
|
+
& Partial<Omit<AgentProgressSnapshot, "correlationId" | "agent" | "taskIndex" | "startedAt" | "completedAt">>
|
|
35
|
+
& { startedAt?: number | string; completedAt?: number | string };
|
|
36
|
+
export type MessagePayload = Partial<Omit<
|
|
37
|
+
TeammateProgressMessageEvent,
|
|
38
|
+
"progress" | "recentTools" | "isSend" | "isInteraction"
|
|
39
|
+
>> & {
|
|
40
|
+
progress?: ProgressPayload[];
|
|
43
41
|
recentTools?: Array<string | { name?: string; status?: string }>;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
outputTokens?: number;
|
|
48
|
-
lastMessage?: string;
|
|
49
|
-
}
|
|
50
|
-
export interface MessagePayload {
|
|
51
|
-
correlationId: string;
|
|
52
|
-
taskCorrelationId?: string;
|
|
53
|
-
// Present on progress/interaction deltas (teammate/.../index.ts publishProgress);
|
|
54
|
-
// absent on send deltas. Used to self-heal a row when `started` was missed.
|
|
55
|
-
agent?: string;
|
|
56
|
-
name?: string;
|
|
57
|
-
taskIndex?: number;
|
|
58
|
-
dependencies?: number[];
|
|
42
|
+
isSend?: boolean;
|
|
43
|
+
isInteraction?: boolean;
|
|
44
|
+
/** Compatibility with pre-v1 progress deltas; discriminated send events are ignored. */
|
|
59
45
|
message?: string;
|
|
60
|
-
|
|
61
|
-
recentTools?: Array<string | { name?: string; status?: string }>;
|
|
62
|
-
toolCount?: number;
|
|
63
|
-
tokens?: number;
|
|
64
|
-
inputTokens?: number;
|
|
65
|
-
outputTokens?: number;
|
|
66
|
-
status?: string;
|
|
67
|
-
lastActivityAt?: number | string;
|
|
68
|
-
progress?: ProgressPayload[];
|
|
69
|
-
}
|
|
46
|
+
};
|
|
70
47
|
export type CompletePayload = Pick<TeammateCompleteEvent, "correlationId" | "exitCode">
|
|
71
|
-
& Partial<Pick<TeammateCompleteEvent, "durationMs" | "wakeable">>;
|
|
48
|
+
& Partial<Pick<TeammateCompleteEvent, "durationMs" | "wakeable" | "cancelled">>;
|
|
72
49
|
|
|
73
50
|
/**
|
|
74
51
|
* How long a failed agent stays on screen after it completes.
|
|
@@ -86,6 +63,14 @@ export const FAILED_LINGER_MS = 30_000;
|
|
|
86
63
|
*/
|
|
87
64
|
export const SLEEPING_LINGER_MS = 60_000;
|
|
88
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;
|
|
73
|
+
|
|
89
74
|
/**
|
|
90
75
|
* How long a completed agent's tombstone suppresses self-healing.
|
|
91
76
|
*
|
|
@@ -121,12 +106,30 @@ export function mapAgentStatus(status: unknown): AgentStatus {
|
|
|
121
106
|
return "done";
|
|
122
107
|
case "failed":
|
|
123
108
|
return "failed";
|
|
109
|
+
case "terminated":
|
|
110
|
+
return "terminated";
|
|
124
111
|
case "running":
|
|
125
112
|
default:
|
|
126
113
|
return "running";
|
|
127
114
|
}
|
|
128
115
|
}
|
|
129
116
|
|
|
117
|
+
function activityFromProgressStatus(status: unknown): "running" | "sleeping" | undefined {
|
|
118
|
+
if (typeof status !== "string") return undefined;
|
|
119
|
+
return status === "sleeping" || status === "completed" || status === "failed" || status === "terminated"
|
|
120
|
+
? "sleeping"
|
|
121
|
+
: "running";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const AGENT_STALL_TIMEOUT_MS = 30_000;
|
|
125
|
+
export type AgentDisplayStatus = AgentStatus | "result-ready" | "stalled";
|
|
126
|
+
|
|
127
|
+
export function effectiveAgentStatus(row: AgentRow, now: number = Date.now()): AgentDisplayStatus {
|
|
128
|
+
if (row.status !== "running") return row.status;
|
|
129
|
+
if (row.resultReadyAt !== undefined) return "result-ready";
|
|
130
|
+
return now - row.lastActivityAt >= AGENT_STALL_TIMEOUT_MS ? "stalled" : "running";
|
|
131
|
+
}
|
|
132
|
+
|
|
130
133
|
function normalizeStartedAt(value: number | string | undefined, fallback: number): number {
|
|
131
134
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
132
135
|
if (typeof value === "string") {
|
|
@@ -142,14 +145,15 @@ function terminalTime(
|
|
|
142
145
|
now: number,
|
|
143
146
|
): number {
|
|
144
147
|
const completedAt = normalizeStartedAt(payload.completedAt, Number.NaN);
|
|
145
|
-
|
|
148
|
+
const turnStartedAt = row.turnStartedAt ?? row.startedAt;
|
|
149
|
+
if (Number.isFinite(completedAt)) return Math.max(turnStartedAt, completedAt);
|
|
146
150
|
if (typeof payload.durationMs === "number" && Number.isFinite(payload.durationMs)) {
|
|
147
|
-
return
|
|
151
|
+
return turnStartedAt + Math.max(0, payload.durationMs);
|
|
148
152
|
}
|
|
149
153
|
return row.finishedAt ?? now;
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
function latestTool(tools:
|
|
156
|
+
function latestTool(tools: Array<string | { name?: string; status?: string }> | undefined): string | undefined {
|
|
153
157
|
if (!tools?.length) return undefined;
|
|
154
158
|
const tool = tools.find((candidate) => typeof candidate === "object" && candidate?.status === "running")
|
|
155
159
|
?? tools.at(-1);
|
|
@@ -187,6 +191,7 @@ export class AgentsStore {
|
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
applyStarted(p: StartedPayload, now: number = Date.now()): void {
|
|
194
|
+
if (typeof p.correlationId !== "string" || p.correlationId.length === 0) return;
|
|
190
195
|
const id = p.correlationId;
|
|
191
196
|
// An explicit start is authoritative: a woken or re-dispatched agent
|
|
192
197
|
// reuses its correlationId and must reappear even if an earlier run was
|
|
@@ -201,6 +206,14 @@ export class AgentsStore {
|
|
|
201
206
|
role: deriveRole(p.agent, p.name),
|
|
202
207
|
task: prev?.task ?? clean(p.name),
|
|
203
208
|
status: p.status === undefined ? prev?.status ?? "running" : mapAgentStatus(p.status),
|
|
209
|
+
phase: typeof p.phase === "string" ? clean(p.phase) : prev?.phase,
|
|
210
|
+
lastOutcome: p.lastOutcome
|
|
211
|
+
? {
|
|
212
|
+
status: p.lastOutcome.status,
|
|
213
|
+
...(p.lastOutcome.message ? { message: truncateTail(p.lastOutcome.message) } : {}),
|
|
214
|
+
settledAt: p.lastOutcome.settledAt,
|
|
215
|
+
}
|
|
216
|
+
: prev?.lastOutcome,
|
|
204
217
|
tail: prev?.tail ?? "",
|
|
205
218
|
startedAt: prev?.startedAt ?? normalizeStartedAt(p.startedAt, now),
|
|
206
219
|
lastActivityAt: normalizeStartedAt(p.lastActivityAt, now),
|
|
@@ -210,15 +223,30 @@ export class AgentsStore {
|
|
|
210
223
|
? { parentCorrelationId: prev.parentCorrelationId }
|
|
211
224
|
: {}),
|
|
212
225
|
};
|
|
213
|
-
if (row.status
|
|
226
|
+
if (row.status === "running") {
|
|
227
|
+
row.turnStartedAt = now;
|
|
228
|
+
delete row.finishedAt;
|
|
229
|
+
delete row.failedAt;
|
|
230
|
+
} else if (row.status === "sleeping") {
|
|
231
|
+
row.finishedAt = row.lastOutcome?.settledAt ?? prev?.finishedAt ?? now;
|
|
232
|
+
} else if (row.status !== "done" && row.status !== "failed" && row.status !== "terminated") {
|
|
233
|
+
delete row.finishedAt;
|
|
234
|
+
}
|
|
214
235
|
this.roster.set(id, row);
|
|
215
236
|
}
|
|
216
237
|
|
|
217
238
|
applyMessage(p: MessagePayload, now = Date.now()): void {
|
|
239
|
+
if (p.isSend === true || p.isInteraction === true) return;
|
|
240
|
+
if (typeof p.correlationId !== "string" || p.correlationId.length === 0) return;
|
|
218
241
|
if (p.progress) {
|
|
219
|
-
for (const progress of p.progress)
|
|
242
|
+
for (const progress of p.progress) {
|
|
243
|
+
if (typeof progress.correlationId !== "string" || progress.correlationId.length === 0) continue;
|
|
244
|
+
this.applyProgress(p.correlationId, progress, now);
|
|
245
|
+
}
|
|
220
246
|
}
|
|
221
|
-
const targetId = p.taskCorrelationId
|
|
247
|
+
const targetId = typeof p.taskCorrelationId === "string" && p.taskCorrelationId.length > 0
|
|
248
|
+
? p.taskCorrelationId
|
|
249
|
+
: p.correlationId;
|
|
222
250
|
const parentId = p.taskCorrelationId && p.taskCorrelationId !== p.correlationId
|
|
223
251
|
? p.correlationId
|
|
224
252
|
: undefined;
|
|
@@ -235,7 +263,7 @@ export class AgentsStore {
|
|
|
235
263
|
correlationId: targetId,
|
|
236
264
|
agent: p.agent ?? "",
|
|
237
265
|
name: p.name,
|
|
238
|
-
status: p.status,
|
|
266
|
+
status: activityFromProgressStatus(p.status),
|
|
239
267
|
lastActivityAt: p.lastActivityAt,
|
|
240
268
|
...(p.taskCorrelationId && p.taskCorrelationId !== p.correlationId
|
|
241
269
|
? { spawnedBy: p.correlationId }
|
|
@@ -246,7 +274,7 @@ export class AgentsStore {
|
|
|
246
274
|
if (!row) return;
|
|
247
275
|
const progressActivity = p.progress?.find((progress) => progress.correlationId === targetId)?.lastActivityAt;
|
|
248
276
|
row.lastActivityAt = normalizeStartedAt(p.lastActivityAt ?? progressActivity, now);
|
|
249
|
-
const tail = p.
|
|
277
|
+
const tail = p.lastMessage ?? p.message;
|
|
250
278
|
if (typeof tail === "string" && tail.length > 0) row.tail = truncateTail(tail);
|
|
251
279
|
if (p.recentTools) {
|
|
252
280
|
const tool = latestTool(p.recentTools);
|
|
@@ -257,17 +285,31 @@ export class AgentsStore {
|
|
|
257
285
|
if (typeof p.tokens === "number") row.tokens = p.tokens;
|
|
258
286
|
if (typeof p.inputTokens === "number") row.inputTokens = p.inputTokens;
|
|
259
287
|
if (typeof p.outputTokens === "number") row.outputTokens = p.outputTokens;
|
|
288
|
+
if (typeof p.cacheReadTokens === "number") row.cacheReadTokens = p.cacheReadTokens;
|
|
289
|
+
if (typeof p.cacheWriteTokens === "number") row.cacheWriteTokens = p.cacheWriteTokens;
|
|
290
|
+
if (typeof p.error === "string") row.error = truncateTail(p.error);
|
|
291
|
+
if (typeof p.requestedModel === "string") row.requestedModel = clean(p.requestedModel);
|
|
292
|
+
if (typeof p.resolvedModel === "string") row.resolvedModel = clean(p.resolvedModel);
|
|
293
|
+
if (Array.isArray(p.attemptedModels)) {
|
|
294
|
+
row.attemptedModels = p.attemptedModels.map((model) => clean(model)).filter(Boolean);
|
|
295
|
+
}
|
|
260
296
|
if (typeof p.status === "string") {
|
|
261
297
|
row.taskStatus = p.status;
|
|
262
298
|
row.status = mapAgentStatus(p.status);
|
|
263
|
-
if (row.status === "done" || row.status === "failed"
|
|
264
|
-
|
|
299
|
+
if (row.status === "done" || row.status === "failed" || row.status === "terminated") {
|
|
300
|
+
row.finishedAt ??= now;
|
|
301
|
+
} else {
|
|
302
|
+
delete row.finishedAt;
|
|
303
|
+
}
|
|
265
304
|
}
|
|
305
|
+
if (typeof p.phase === "string") row.phase = clean(p.phase);
|
|
306
|
+
else if (row.status === "sleeping") delete row.phase;
|
|
266
307
|
if (typeof p.taskIndex === "number") row.taskIndex = p.taskIndex;
|
|
267
308
|
if (Array.isArray(p.dependencies)) row.dependencies = [...p.dependencies];
|
|
268
309
|
}
|
|
269
310
|
|
|
270
311
|
applyComplete(p: CompletePayload, now = Date.now()): void {
|
|
312
|
+
if (typeof p.correlationId !== "string" || p.correlationId.length === 0) return;
|
|
271
313
|
const pending = [p.correlationId];
|
|
272
314
|
const visited = new Set<string>();
|
|
273
315
|
while (pending.length > 0) {
|
|
@@ -281,21 +323,46 @@ export class AgentsStore {
|
|
|
281
323
|
// only evidence of the failure disappeared in the same frame it appeared.
|
|
282
324
|
// Successes still vanish immediately — the work has simply moved on.
|
|
283
325
|
const row = this.roster.get(id);
|
|
326
|
+
const cancelledById = id === p.correlationId && p.cancelled === true;
|
|
284
327
|
const failedByExitCode = id === p.correlationId
|
|
285
328
|
&& Number.isFinite(p.exitCode)
|
|
286
329
|
&& p.exitCode !== 0;
|
|
287
|
-
if (row && (row.status === "
|
|
288
|
-
|
|
289
|
-
|
|
330
|
+
if (row && (cancelledById || row.status === "terminated")) {
|
|
331
|
+
// Cancelled/terminated runs carry exitCode 1 but are not failures:
|
|
332
|
+
// a user abort or a result-ready reclaim must not light the red ✗.
|
|
333
|
+
row.status = "terminated";
|
|
334
|
+
row.taskStatus = "terminated";
|
|
290
335
|
row.finishedAt = terminalTime(row, id === p.correlationId ? p : {}, now);
|
|
291
|
-
row.
|
|
336
|
+
row.lastOutcome = { status: "terminated", settledAt: row.finishedAt };
|
|
292
337
|
row.lastActivityAt = now;
|
|
293
338
|
delete row.activeTool;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
//
|
|
339
|
+
delete row.failedAt;
|
|
340
|
+
} else if (row && id === p.correlationId && p.wakeable) {
|
|
341
|
+
// A recoverable logical agent is sleeping regardless of the previous
|
|
342
|
+
// run outcome. Failure remains visible as orthogonal outcome metadata.
|
|
297
343
|
row.status = "sleeping";
|
|
344
|
+
row.taskStatus = "sleeping";
|
|
345
|
+
row.phase = undefined;
|
|
346
|
+
row.finishedAt = terminalTime(row, p, now);
|
|
347
|
+
row.lastActivityAt = now;
|
|
348
|
+
row.lastOutcome = {
|
|
349
|
+
status: failedByExitCode ? "failed" : "completed",
|
|
350
|
+
...(failedByExitCode && row.error ? { message: row.error } : {}),
|
|
351
|
+
settledAt: row.finishedAt,
|
|
352
|
+
};
|
|
353
|
+
if (failedByExitCode) row.failedAt = now;
|
|
354
|
+
else delete row.failedAt;
|
|
355
|
+
delete row.activeTool;
|
|
356
|
+
} else if (row && (row.status === "failed" || failedByExitCode)) {
|
|
357
|
+
row.status = "failed";
|
|
358
|
+
row.taskStatus = "failed";
|
|
298
359
|
row.finishedAt = terminalTime(row, id === p.correlationId ? p : {}, now);
|
|
360
|
+
row.lastOutcome = {
|
|
361
|
+
status: "failed",
|
|
362
|
+
...(row.error ? { message: row.error } : {}),
|
|
363
|
+
settledAt: row.finishedAt,
|
|
364
|
+
};
|
|
365
|
+
row.failedAt = now;
|
|
299
366
|
row.lastActivityAt = now;
|
|
300
367
|
delete row.activeTool;
|
|
301
368
|
} else {
|
|
@@ -307,13 +374,16 @@ export class AgentsStore {
|
|
|
307
374
|
}
|
|
308
375
|
}
|
|
309
376
|
|
|
310
|
-
/** Drop failed or expired sleeping rows that have had their time on screen. Returns true if any went. */
|
|
377
|
+
/** Drop failed, terminated or expired sleeping rows that have had their time on screen. Returns true if any went. */
|
|
311
378
|
prune(now = Date.now()): boolean {
|
|
312
379
|
let changed = false;
|
|
313
380
|
for (const [id, row] of this.roster) {
|
|
314
381
|
if (row.failedAt !== undefined && now - row.failedAt >= FAILED_LINGER_MS) {
|
|
315
382
|
this.roster.delete(id);
|
|
316
383
|
changed = true;
|
|
384
|
+
} else if (row.status === "terminated" && row.finishedAt !== undefined && now - row.finishedAt >= TERMINATED_LINGER_MS) {
|
|
385
|
+
this.roster.delete(id);
|
|
386
|
+
changed = true;
|
|
317
387
|
} else if (row.status === "sleeping" && row.finishedAt !== undefined && now - row.finishedAt >= SLEEPING_LINGER_MS) {
|
|
318
388
|
this.roster.delete(id);
|
|
319
389
|
changed = true;
|
|
@@ -325,10 +395,11 @@ export class AgentsStore {
|
|
|
325
395
|
return changed;
|
|
326
396
|
}
|
|
327
397
|
|
|
328
|
-
/** True while a failed or sleeping row is still counting down, so the redraw loop must run. */
|
|
398
|
+
/** True while a failed, terminated or sleeping row is still counting down, so the redraw loop must run. */
|
|
329
399
|
hasLingering(): boolean {
|
|
330
400
|
for (const row of this.roster.values()) {
|
|
331
401
|
if (row.failedAt !== undefined) return true;
|
|
402
|
+
if (row.status === "terminated") return true;
|
|
332
403
|
if (row.status === "sleeping") return true;
|
|
333
404
|
}
|
|
334
405
|
return false;
|
|
@@ -345,7 +416,7 @@ export class AgentsStore {
|
|
|
345
416
|
correlationId: p.correlationId,
|
|
346
417
|
agent: p.agent,
|
|
347
418
|
name: p.name,
|
|
348
|
-
status: p.status,
|
|
419
|
+
status: activityFromProgressStatus(p.status),
|
|
349
420
|
startedAt: p.startedAt,
|
|
350
421
|
lastActivityAt: p.lastActivityAt,
|
|
351
422
|
...(parentCorrelationId !== p.correlationId
|
|
@@ -367,12 +438,17 @@ export class AgentsStore {
|
|
|
367
438
|
if (typeof p.status === "string") {
|
|
368
439
|
row.taskStatus = p.status;
|
|
369
440
|
row.status = mapAgentStatus(p.status);
|
|
370
|
-
if (row.status === "done" || row.status === "failed") {
|
|
441
|
+
if (row.status === "done" || row.status === "failed" || row.status === "terminated") {
|
|
371
442
|
row.finishedAt = terminalTime(row, p, now);
|
|
372
443
|
} else {
|
|
373
444
|
delete row.finishedAt;
|
|
374
445
|
}
|
|
375
446
|
}
|
|
447
|
+
if (typeof p.phase === "string") row.phase = clean(p.phase);
|
|
448
|
+
else if (row.status === "sleeping") delete row.phase;
|
|
449
|
+
row.resultReadyAt = p.resultReadyAt === undefined
|
|
450
|
+
? undefined
|
|
451
|
+
: normalizeStartedAt(p.resultReadyAt, now);
|
|
376
452
|
row.taskIndex = p.taskIndex;
|
|
377
453
|
row.dependencies = Array.isArray(p.dependencies) ? [...p.dependencies] : [];
|
|
378
454
|
row.lastActivityAt = normalizeStartedAt(p.lastActivityAt, now);
|
|
@@ -385,6 +461,16 @@ export class AgentsStore {
|
|
|
385
461
|
if (typeof p.tokens === "number") row.tokens = p.tokens;
|
|
386
462
|
if (typeof p.inputTokens === "number") row.inputTokens = p.inputTokens;
|
|
387
463
|
if (typeof p.outputTokens === "number") row.outputTokens = p.outputTokens;
|
|
464
|
+
row.cacheReadTokens = p.cacheReadTokens;
|
|
465
|
+
row.cacheWriteTokens = p.cacheWriteTokens;
|
|
466
|
+
if (p.error === undefined) delete row.error;
|
|
467
|
+
else row.error = truncateTail(p.error);
|
|
468
|
+
if (p.requestedModel === undefined) delete row.requestedModel;
|
|
469
|
+
else row.requestedModel = clean(p.requestedModel);
|
|
470
|
+
if (p.resolvedModel === undefined) delete row.resolvedModel;
|
|
471
|
+
else row.resolvedModel = clean(p.resolvedModel);
|
|
472
|
+
if (p.attemptedModels === undefined) delete row.attemptedModels;
|
|
473
|
+
else row.attemptedModels = p.attemptedModels.map((model) => clean(model)).filter(Boolean);
|
|
388
474
|
if (typeof p.lastMessage === "string" && p.lastMessage.length > 0) {
|
|
389
475
|
row.tail = truncateTail(p.lastMessage);
|
|
390
476
|
}
|