osdy-pi 1.11.7 → 1.11.9
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
CHANGED
|
@@ -492,12 +492,14 @@ While work is active, Osdy Pi centers an activity label and compact animated gly
|
|
|
492
492
|
|
|
493
493
|
| Source | Example | Meaning |
|
|
494
494
|
| --- | --- | --- |
|
|
495
|
-
| Validated public `agent` argument | `
|
|
496
|
-
| Public foreground progress from Joker (`pi-subagents-j0k3r`) | `
|
|
495
|
+
| Validated public `agent` argument | `Working...` | Generic fallback, not measured progress or a claimed editing operation. This provider-neutral fallback works with Gentle Agents, which currently emits no live child progress. |
|
|
496
|
+
| Public foreground progress from Joker (`pi-subagents-j0k3r`) | `Exploring...` | Named `read` activity selects both the Exploring label and its existing glyph frames, overriding fallback. |
|
|
497
497
|
|
|
498
|
-
|
|
498
|
+
Live child activity uses the same six states and corresponding animated glyphs listed above, without agent names: `Thinking...`, `Exploring...`, `Verifying...`, `Working...`, `Delegating...`, or `Executing...`. Both public thinking and response streaming select Thinking; model labels, reasoning and response text are never read. Named reads/searches/direct documentation tools select Exploring, `lens_diagnostics` selects Verifying, edits/writes select Working, and `subagent_run` selects Delegating. Only named checks qualify: public tool metadata lacks arguments, so `bash`, wrappers needing selectors, and unknown tool names select Executing. Tool completion/failure events classify the reported operation while the child is running; they do not claim whole-task completion.
|
|
499
499
|
|
|
500
|
-
|
|
500
|
+
The fallback is `Working...` **with the Working glyph**, regardless of optional `label` metadata. Missing or empty tool names supply no usable operation; named unknown tools instead select Executing. A continuation with an absent agent validates the generic `Subagent` identity without displaying it, never inferring an identity; missing/invalid run identity or explicitly invalid continuation identity fails closed to ordinary parent activity unless a live snapshot supplies its own validated public agent. Full tasks, prompts, context, content, trails and transcripts are never read or displayed.
|
|
501
|
+
|
|
502
|
+
For multiple running children, the indicator uses the first child in snapshot order with a compact additional-child count (`(+N)`). Missing, malformed, terminal or no-running-child progress restores both the generic `Working...` fallback text and Working glyph, or ordinary parent activity when validated agent identity is unavailable. Labels are bounded, control-safe and clipped to terminal width. Matching tool end, agent/session end and reload cleanup discard per-call state.
|
|
501
503
|
|
|
502
504
|
**Background limitation:** agent fallbacks appear only while their parent call is active. There is no persistent background live feed, polling or task cache.
|
|
503
505
|
|
|
@@ -1,68 +1,105 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { registerHooks } from "node:module";
|
|
3
3
|
import test from "node:test";
|
|
4
|
+
import type { WorkingActivity } from "./types.js";
|
|
4
5
|
|
|
5
6
|
registerHooks({ resolve(specifier, context, nextResolve) {
|
|
6
7
|
if (specifier.startsWith(".") && specifier.endsWith(".js") && context.parentURL?.endsWith(".ts"))
|
|
7
8
|
return nextResolve(`${specifier.slice(0, -3)}.ts`, context);
|
|
8
9
|
return nextResolve(specifier, context);
|
|
9
10
|
} });
|
|
10
|
-
const {
|
|
11
|
+
const { childActivityState: activity, childAssignmentState: assignment } = await import("./working-child-activity.js");
|
|
12
|
+
const working = () => ({ activity: "working", label: "Working..." });
|
|
11
13
|
|
|
12
14
|
void test("fallback uses only validated public agent identity regardless of optional label", () => {
|
|
13
|
-
assert.
|
|
15
|
+
assert.deepEqual(assignment("subagent_run", { agent: "explore", label: "map footer data" }), working());
|
|
14
16
|
for (const args of [undefined, null, [], "scout", {}, { agent: 1 }, { agent: "\u0000\n" }, { agent: "a".repeat(4097) }, { label: "secret" }])
|
|
15
17
|
assert.equal(assignment("subagent_run", args), undefined);
|
|
16
18
|
for (const label of [undefined, null, 1, [], "", "\u0000", "x".repeat(4097)])
|
|
17
|
-
assert.
|
|
18
|
-
const metadata = { agent: "scout", get
|
|
19
|
-
get
|
|
20
|
-
|
|
21
|
-
assert.
|
|
22
|
-
assert.equal(assignment("read", { agent: "scout"
|
|
23
|
-
assert.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
assert.
|
|
19
|
+
assert.deepEqual(assignment("subagent_run", { agent: "scout", label, task: "SECRET", prompt: "SECRET", context: "SECRET" }), working());
|
|
20
|
+
const metadata = { agent: "scout", get label() { throw new Error("label must not be read"); },
|
|
21
|
+
get task() { throw new Error("task must not be read"); }, get prompt() { throw new Error("prompt must not be read"); },
|
|
22
|
+
get context() { throw new Error("context must not be read"); } };
|
|
23
|
+
assert.deepEqual(assignment("subagent_run", metadata), working());
|
|
24
|
+
assert.equal(assignment("read", { agent: "scout" }), undefined);
|
|
25
|
+
assert.deepEqual(assignment("subagent_continue", { id: "scout" }), working());
|
|
26
|
+
for (const agent of [42, null, "", "\u0000"])
|
|
27
|
+
assert.equal(assignment("subagent_continue", { agent }), undefined);
|
|
28
|
+
assert.deepEqual(assignment("subagent_run", { agent: "\u001b[31m探索\u001b[0m\n\u202eagent" }), working());
|
|
29
|
+
assert.deepEqual(assignment("subagent_run", { agent: "🙂".repeat(1000) }), working());
|
|
28
30
|
});
|
|
29
31
|
|
|
30
|
-
function task(agent = "scout", kind = "tool_running",
|
|
31
|
-
return { agent, status: "running", live_activity: { current: { kind,
|
|
32
|
+
function task(agent = "scout", kind = "tool_running", tool_names: unknown = ["read"]) {
|
|
33
|
+
return { agent, status: "running", live_activity: { current: { kind, tool_names,
|
|
34
|
+
get label() { throw new Error("model label must not be read"); },
|
|
35
|
+
get thinking() { throw new Error("thinking must not be read"); },
|
|
36
|
+
get content() { throw new Error("content must not be read"); },
|
|
37
|
+
get response() { throw new Error("response must not be read"); } },
|
|
38
|
+
get trail() { throw new Error("trail must not be read"); } },
|
|
39
|
+
get task() { throw new Error("task must not be read"); },
|
|
40
|
+
get prompt() { throw new Error("prompt must not be read"); },
|
|
41
|
+
get context() { throw new Error("context must not be read"); },
|
|
42
|
+
get transcript() { throw new Error("transcript must not be read"); } };
|
|
32
43
|
}
|
|
33
44
|
const progress = (...tasks: unknown[]) => ({ details: { tasks } });
|
|
34
45
|
|
|
35
|
-
void test("
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
void test("public operations return structured six-state activity and label", () => {
|
|
47
|
+
const cases: Array<[string, WorkingActivity, string]> = [
|
|
48
|
+
["read", "exploring", "Exploring..."], ["grep", "exploring", "Exploring..."],
|
|
49
|
+
["query-docs", "exploring", "Exploring..."], ["lens_diagnostics", "verifying", "Verifying..."],
|
|
50
|
+
["edit", "working", "Working..."], ["write", "working", "Working..."],
|
|
51
|
+
["subagent_run", "delegating", "Delegating..."], ["unknown", "executing", "Executing..."],
|
|
52
|
+
["bash", "executing", "Executing..."], ["mcp", "executing", "Executing..."],
|
|
53
|
+
["mcp__context7", "executing", "Executing..."], ["intercom", "executing", "Executing..."],
|
|
54
|
+
];
|
|
55
|
+
for (const kind of ["tool_running", "tool_completed", "tool_failed"])
|
|
56
|
+
for (const [tool, state, label] of cases)
|
|
57
|
+
assert.deepEqual(activity(progress(task("scout", kind, [tool]))), { activity: state, label });
|
|
58
|
+
for (const kind of ["thinking", "streaming_response"])
|
|
59
|
+
assert.deepEqual(activity(progress(task("scout", kind))), { activity: "thinking", label: "Thinking..." });
|
|
41
60
|
});
|
|
42
61
|
|
|
43
|
-
void test("multiple children select
|
|
44
|
-
assert.
|
|
45
|
-
|
|
62
|
+
void test("multiple children select first running snapshot child and validate all children", () => {
|
|
63
|
+
assert.deepEqual(activity(progress({ agent: "old", status: "completed" }, task("first"), task("second"), task("third"))),
|
|
64
|
+
{ activity: "exploring", label: "Exploring... (+2)" });
|
|
65
|
+
assert.deepEqual(activity(progress(task("first", "tool_running", ["read", "write"]))), { activity: "exploring", label: "Exploring..." });
|
|
66
|
+
assert.equal(activity(progress(task(), task("bad", "tool_running", ["read", 1]))), undefined);
|
|
67
|
+
assert.equal(activity(progress(...Array.from({ length: 65 }, () => task()))), undefined);
|
|
68
|
+
assert.equal(activity(progress(...Array.from({ length: 64 }, () => task())))?.label, "Exploring... (+63)");
|
|
46
69
|
});
|
|
47
70
|
|
|
48
|
-
void test("terminal, missing, malformed and unsupported progress
|
|
49
|
-
for (const status of ["completed", "failed", "cancelled", "aborted", "pending", "interrupted", "stopping", "queued"]) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
assert.equal(childActivityLabel(value), undefined, JSON.stringify(value));
|
|
71
|
+
void test("terminal, missing, malformed and unsupported progress has no usable activity", () => {
|
|
72
|
+
for (const status of ["completed", "failed", "cancelled", "aborted", "pending", "interrupted", "stopping", "queued", "unknown"]) {
|
|
73
|
+
const child = task();
|
|
74
|
+
child.status = status;
|
|
75
|
+
assert.equal(activity(progress(child)), undefined);
|
|
54
76
|
}
|
|
77
|
+
for (const value of [undefined, null, [], {}, { details: null }, { details: { tasks: {} } }, progress(), progress(null),
|
|
78
|
+
progress({ agent: 1, status: "running" }), progress({ agent: "scout", status: "running" }), progress(task("scout", "unknown"))])
|
|
79
|
+
assert.equal(activity(value), undefined);
|
|
80
|
+
for (const tool_names of [undefined, [], [""], ["\u0000"], [1], "read", ["read", null], Array(17).fill("read"), ["x".repeat(4097)]])
|
|
81
|
+
assert.equal(activity(progress({ agent: "scout", status: "running", live_activity: { current: { kind: "tool_running", tool_names } } })), undefined);
|
|
82
|
+
assert.equal(activity(progress(task("scout", "tool_running", Array(16).fill("read"))))?.activity, "exploring");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
void test("non-string snapshot agent is rejected despite otherwise valid live activity", () => {
|
|
86
|
+
assert.equal(activity(progress(Object.assign(task(), { agent: 1 }))), undefined);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
void test("oversized snapshot agent is rejected despite otherwise valid live activity", () => {
|
|
90
|
+
assert.equal(activity(progress(task("a".repeat(4097)))), undefined);
|
|
55
91
|
});
|
|
56
92
|
|
|
57
|
-
void test("
|
|
58
|
-
assert.
|
|
59
|
-
|
|
93
|
+
void test("snapshot agent at the input bound remains valid without appearing in the label", () => {
|
|
94
|
+
assert.deepEqual(activity(progress(task("a".repeat(4096)))),
|
|
95
|
+
{ activity: "exploring", label: "Exploring..." });
|
|
60
96
|
});
|
|
61
97
|
|
|
62
|
-
void test("external
|
|
98
|
+
void test("external names are single-line, control-free and bounded before classification", () => {
|
|
63
99
|
const dirty = "\u001b[31mAgent\u001b[0m\n\t\u202eName\u0007";
|
|
64
|
-
assert.
|
|
65
|
-
|
|
66
|
-
assert.
|
|
67
|
-
|
|
100
|
+
assert.deepEqual(activity(progress(task(dirty, "tool_running", ["\u001b]0;title\u0007read\nfile"]))),
|
|
101
|
+
{ activity: "executing", label: "Executing..." });
|
|
102
|
+
assert.deepEqual(activity(progress(task("a".repeat(1000), "tool_running", ["b".repeat(1000)]))),
|
|
103
|
+
{ activity: "executing", label: "Executing..." });
|
|
104
|
+
assert.equal(activity(progress(task("\u0000\n"))), undefined);
|
|
68
105
|
});
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import type { WorkingActivity } from "./types.js";
|
|
3
|
+
import { classifyWorkingActivity, WORKING_ACTIVITY_LABELS } from "./working-activity.js";
|
|
4
|
+
|
|
5
|
+
export type ChildWorkingState = { activity: WorkingActivity; label: string };
|
|
6
|
+
|
|
7
|
+
function childState(activity: WorkingActivity): ChildWorkingState {
|
|
8
|
+
return { activity, label: WORKING_ACTIVITY_LABELS[activity] };
|
|
9
|
+
}
|
|
2
10
|
|
|
3
11
|
function record(value: unknown): value is Record<string, unknown> {
|
|
4
12
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -14,21 +22,20 @@ function safeLabel(value: unknown, limit: number): string | undefined {
|
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
/** Assignment only: never infer activity or identity from task, prompt or context. */
|
|
17
|
-
export function
|
|
25
|
+
export function childAssignmentState(toolName: string, args: unknown): ChildWorkingState | undefined {
|
|
18
26
|
if (toolName !== "subagent_run" && toolName !== "subagent_continue") return undefined;
|
|
19
27
|
if (!record(args)) return undefined;
|
|
20
28
|
const agent = args.agent === undefined && toolName === "subagent_continue" ? "Subagent" : safeLabel(args.agent, 32);
|
|
21
29
|
if (!agent) return undefined;
|
|
22
|
-
return
|
|
30
|
+
return childState("working");
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
function observableActivity(value: unknown):
|
|
26
|
-
if (!record(value)
|
|
27
|
-
//
|
|
28
|
-
if (value.kind === "thinking"
|
|
29
|
-
if (value.kind === "streaming_response") return "Responding";
|
|
33
|
+
function observableActivity(value: unknown): WorkingActivity | undefined {
|
|
34
|
+
if (!record(value)) return undefined;
|
|
35
|
+
// Do not even access model labels: they may expose private response or reasoning text.
|
|
36
|
+
if (value.kind === "thinking" || value.kind === "streaming_response") return "thinking";
|
|
30
37
|
if (value.kind !== "tool_running" && value.kind !== "tool_completed" && value.kind !== "tool_failed") return undefined;
|
|
31
|
-
let tool
|
|
38
|
+
let tool: string | undefined;
|
|
32
39
|
if (value.tool_names !== undefined) {
|
|
33
40
|
if (!Array.isArray(value.tool_names) || value.tool_names.length > 16) return undefined;
|
|
34
41
|
const names: string[] = [];
|
|
@@ -37,19 +44,18 @@ function observableActivity(value: unknown): string | undefined {
|
|
|
37
44
|
if (!safe) return undefined;
|
|
38
45
|
names.push(safe);
|
|
39
46
|
}
|
|
40
|
-
tool = names[0]
|
|
47
|
+
tool = names[0];
|
|
41
48
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return tool === "Tool" ? "Tool running" : tool;
|
|
49
|
+
// Public snapshots provide names only; never invent command or wrapper selectors.
|
|
50
|
+
return tool === undefined ? undefined : classifyWorkingActivity(tool, undefined);
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
/** Consume only public foreground metadata, not content, trails or transcripts. */
|
|
48
|
-
export function
|
|
54
|
+
export function childActivityState(partialResult: unknown): ChildWorkingState | undefined {
|
|
49
55
|
if (!record(partialResult) || !record(partialResult.details)) return undefined;
|
|
50
56
|
const tasks: unknown = partialResult.details.tasks;
|
|
51
57
|
if (!Array.isArray(tasks) || tasks.length > 64) return undefined;
|
|
52
|
-
let first:
|
|
58
|
+
let first: ChildWorkingState | undefined;
|
|
53
59
|
let running = 0;
|
|
54
60
|
for (const task of tasks) {
|
|
55
61
|
if (!record(task) || typeof task.status !== "string") return undefined;
|
|
@@ -57,10 +63,10 @@ export function childActivityLabel(partialResult: unknown): string | undefined {
|
|
|
57
63
|
const agent = safeLabel(task.agent, 32);
|
|
58
64
|
const activity = record(task.live_activity) ? observableActivity(task.live_activity.current) : undefined;
|
|
59
65
|
if (!agent || !activity) return undefined;
|
|
60
|
-
first ??=
|
|
66
|
+
first ??= childState(activity);
|
|
61
67
|
running++;
|
|
62
68
|
}
|
|
63
69
|
if (!first) return undefined;
|
|
64
|
-
if (running > 1) return `${first} (+${running - 1})
|
|
70
|
+
if (running > 1) return { ...first, label: `${first.label} (+${running - 1})` };
|
|
65
71
|
return first;
|
|
66
72
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { registerHooks } from "node:module";
|
|
3
3
|
import test from "node:test";
|
|
4
|
-
import type { WorkingWidgetState } from "./types.js";
|
|
4
|
+
import type { WorkingActivity, WorkingWidgetState } from "./types.js";
|
|
5
5
|
|
|
6
6
|
registerHooks({ resolve(specifier, context, nextResolve) {
|
|
7
7
|
if (specifier.startsWith(".") && specifier.endsWith(".js") && context.parentURL?.endsWith(".ts"))
|
|
@@ -9,6 +9,17 @@ registerHooks({ resolve(specifier, context, nextResolve) {
|
|
|
9
9
|
return nextResolve(specifier, context);
|
|
10
10
|
} });
|
|
11
11
|
const { createWorkingController } = await import("./working-controller.js");
|
|
12
|
+
const { renderWorkingWidget } = await import("./working-animation.js");
|
|
13
|
+
const { WORKING_ACTIVITY_FRAMES } = await import("./constants.js");
|
|
14
|
+
|
|
15
|
+
function assertDisplay(working: WorkingWidgetState, activity: WorkingActivity, label: string): void {
|
|
16
|
+
assert.equal(working.activity, activity);
|
|
17
|
+
assert.equal(working.label, label);
|
|
18
|
+
for (let frame = 0; frame < WORKING_ACTIVITY_FRAMES[activity].length; frame++) {
|
|
19
|
+
const line = renderWorkingWidget({ ...working, frame }, { fg: (_color, text) => text }, 100)[0];
|
|
20
|
+
assert.equal(line?.trim(), `${WORKING_ACTIVITY_FRAMES[activity][frame]} ${label}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
12
23
|
|
|
13
24
|
function fixture() {
|
|
14
25
|
const state = { enabled: true };
|
|
@@ -107,26 +118,48 @@ const childProgress = (agent: string, tool = "read", status = "running") => ({ d
|
|
|
107
118
|
{ agent, status, live_activity: { current: { kind: "tool_running", label: "Reading", tool_names: [tool] } } },
|
|
108
119
|
] } });
|
|
109
120
|
|
|
121
|
+
void test("child states select matching text and every existing glyph frame", (t) => {
|
|
122
|
+
t.mock.timers.enable({ apis: ["setInterval"] });
|
|
123
|
+
const { working, controller: c } = fixture();
|
|
124
|
+
c.onToolStart("subagent_run", "child", { agent: "scout" });
|
|
125
|
+
assertDisplay(working, "working", "Working...");
|
|
126
|
+
const cases: Array<[string, string | undefined, WorkingActivity, string]> = [
|
|
127
|
+
["thinking", undefined, "thinking", "Thinking..."],
|
|
128
|
+
["streaming_response", undefined, "thinking", "Thinking..."],
|
|
129
|
+
["tool_running", "read", "exploring", "Exploring..."],
|
|
130
|
+
["tool_completed", "lens_diagnostics", "verifying", "Verifying..."],
|
|
131
|
+
["tool_failed", "write", "working", "Working..."],
|
|
132
|
+
["tool_running", "subagent_run", "delegating", "Delegating..."],
|
|
133
|
+
["tool_running", "unknown", "executing", "Executing..."],
|
|
134
|
+
];
|
|
135
|
+
for (const [kind, tool, activity, label] of cases) {
|
|
136
|
+
c.onToolUpdate("subagent_run", "child", { details: { tasks: [{ agent: "scout", status: "running",
|
|
137
|
+
live_activity: { current: { kind, label: "IGNORED", tool_names: tool ? [tool] : undefined } } }] } });
|
|
138
|
+
assertDisplay(working, activity, label);
|
|
139
|
+
}
|
|
140
|
+
c.onShutdown();
|
|
141
|
+
});
|
|
142
|
+
|
|
110
143
|
void test("child updates respect tool identity and latest remaining tool ownership", (t) => {
|
|
111
144
|
t.mock.timers.enable({ apis: ["setInterval"] });
|
|
112
145
|
const { working, controller: c } = fixture();
|
|
113
146
|
c.onToolStart("subagent_run", "a");
|
|
114
147
|
c.onToolUpdate("subagent_run", "a", childProgress("scout"));
|
|
115
|
-
|
|
148
|
+
assertDisplay(working, "exploring", "Exploring...");
|
|
116
149
|
c.onToolStart("subagent_run", "b");
|
|
117
150
|
c.onToolUpdate("subagent_run", "b", childProgress("writer", "edit"));
|
|
118
151
|
c.onToolUpdate("subagent_run", "a", childProgress("scout", "grep"));
|
|
119
152
|
c.onToolUpdate("read", "b", childProgress("wrong"));
|
|
120
153
|
c.onToolUpdate("subagent_run", "missing", childProgress("late"));
|
|
121
154
|
c.onToolEnd("missing");
|
|
122
|
-
|
|
155
|
+
assertDisplay(working, "working", "Working...");
|
|
123
156
|
c.onToolStart("read", "ordinary");
|
|
124
157
|
c.onToolUpdate("subagent_run", "ordinary", childProgress("wrong"));
|
|
125
158
|
assert.equal(working.label, "Exploring...");
|
|
126
|
-
c.onToolEnd("ordinary");
|
|
127
|
-
c.onToolEnd("b");
|
|
159
|
+
c.onToolEnd("ordinary"); assertDisplay(working, "working", "Working...");
|
|
160
|
+
c.onToolEnd("b"); assertDisplay(working, "exploring", "Exploring...");
|
|
128
161
|
c.onToolUpdate("subagent_run", "b", childProgress("late"));
|
|
129
|
-
|
|
162
|
+
assertDisplay(working, "exploring", "Exploring...");
|
|
130
163
|
c.onToolUpdate("subagent_run", "a", childProgress("scout", "read", "completed"));
|
|
131
164
|
assert.equal(working.label, "Delegating...");
|
|
132
165
|
c.onToolUpdate("subagent_run", "a", childProgress("scout"));
|
|
@@ -154,14 +187,16 @@ void test("agent fallback appears at start and resets after unavailable, malform
|
|
|
154
187
|
t.mock.timers.enable({ apis: ["setInterval"] });
|
|
155
188
|
const { working, controller: c } = fixture();
|
|
156
189
|
c.onToolStart("subagent_run", "a", { agent: "explore", label: "map footer data", task: "SECRET TASK", prompt: "SECRET PROMPT" });
|
|
157
|
-
|
|
190
|
+
assertDisplay(working, "working", "Working...");
|
|
158
191
|
c.onToolUpdate("subagent_run", "a", childProgress("explore"));
|
|
159
|
-
|
|
160
|
-
for (const update of [undefined, {}, { details: { tasks: null } }, { details: { tasks: [{ agent: "explore", status: "running" }] } }, childProgress("explore", "read", "completed")
|
|
192
|
+
assertDisplay(working, "exploring", "Exploring...");
|
|
193
|
+
for (const update of [undefined, {}, { details: { tasks: null } }, { details: { tasks: [{ agent: "explore", status: "running" }] } }, childProgress("explore", "read", "completed"), childProgress("explore", ""),
|
|
194
|
+
{ details: { tasks: [{ agent: "explore", status: "running", live_activity: { current: { kind: "tool_running", tool_names: [] } } }] } },
|
|
195
|
+
{ details: { tasks: [{ agent: "explore", status: "running", live_activity: { current: { kind: "tool_running" } } }] } }]) {
|
|
161
196
|
c.onToolUpdate("subagent_run", "a", childProgress("explore"));
|
|
162
|
-
|
|
197
|
+
assertDisplay(working, "exploring", "Exploring...");
|
|
163
198
|
c.onToolUpdate("subagent_run", "a", update);
|
|
164
|
-
|
|
199
|
+
assertDisplay(working, "working", "Working...");
|
|
165
200
|
}
|
|
166
201
|
c.onToolEnd("a"); assert.equal(working.label, "Thinking...");
|
|
167
202
|
});
|
|
@@ -170,37 +205,67 @@ void test("continuations use generic identity without guessing and preserve call
|
|
|
170
205
|
t.mock.timers.enable({ apis: ["setInterval"] });
|
|
171
206
|
const { working, controller: c } = fixture();
|
|
172
207
|
c.onToolStart("subagent_run", "a", { agent: "scout" });
|
|
173
|
-
assert.equal(working.label, "
|
|
208
|
+
assert.equal(working.label, "Working...");
|
|
174
209
|
c.onToolStart("subagent_continue", "b", { id: "a", label: "check footer" });
|
|
175
|
-
assert.equal(working.label, "
|
|
210
|
+
assert.equal(working.label, "Working...");
|
|
176
211
|
c.onToolStart("subagent_continue", "a", { agent: "wrong", label: "wrong" });
|
|
177
212
|
c.onToolUpdate("subagent_run", "b", childProgress("wrong"));
|
|
178
213
|
c.onToolUpdate("subagent_continue", "missing", childProgress("wrong"));
|
|
179
|
-
assert.equal(working.label, "
|
|
214
|
+
assert.equal(working.label, "Working...");
|
|
180
215
|
c.onToolUpdate("subagent_continue", "b", childProgress("writer", "edit"));
|
|
181
|
-
|
|
216
|
+
assertDisplay(working, "working", "Working...");
|
|
182
217
|
c.onToolUpdate("subagent_run", "a", childProgress("scout"));
|
|
183
|
-
|
|
218
|
+
assertDisplay(working, "working", "Working...");
|
|
184
219
|
c.onToolUpdate("subagent_continue", "b", {});
|
|
185
|
-
assert.equal(working.label, "
|
|
186
|
-
c.onToolEnd("b");
|
|
220
|
+
assert.equal(working.label, "Working...");
|
|
221
|
+
c.onToolEnd("b"); assertDisplay(working, "exploring", "Exploring...");
|
|
187
222
|
c.onToolUpdate("subagent_continue", "b", childProgress("late"));
|
|
188
|
-
c.onToolUpdate("subagent_run", "a", {}); assert.equal(working.label, "
|
|
223
|
+
c.onToolUpdate("subagent_run", "a", {}); assert.equal(working.label, "Working...");
|
|
189
224
|
c.onToolEnd("a");
|
|
190
225
|
c.onToolStart("subagent_continue", "c", { agent: "writer", label: "finish" });
|
|
191
|
-
assert.equal(working.label, "
|
|
226
|
+
assert.equal(working.label, "Working...");
|
|
192
227
|
c.onToolEnd("c");
|
|
193
228
|
c.onToolStart("subagent_continue", "d", { id: "c", task: "SECRET" });
|
|
194
|
-
assert.equal(working.label, "
|
|
229
|
+
assert.equal(working.label, "Working...");
|
|
195
230
|
c.onToolEnd("d");
|
|
196
231
|
});
|
|
197
232
|
|
|
233
|
+
void test("identity failures stay parent-owned and live snapshots retain validated public identity", (t) => {
|
|
234
|
+
t.mock.timers.enable({ apis: ["setInterval"] });
|
|
235
|
+
const { working, controller: c } = fixture();
|
|
236
|
+
for (const args of [undefined, {}, { agent: null }, { agent: "" }]) {
|
|
237
|
+
c.onToolStart("subagent_run", "run", args);
|
|
238
|
+
assertDisplay(working, "delegating", "Delegating...");
|
|
239
|
+
c.onToolUpdate("subagent_run", "run", childProgress("public"));
|
|
240
|
+
assertDisplay(working, "exploring", "Exploring...");
|
|
241
|
+
c.onToolUpdate("subagent_run", "run", {});
|
|
242
|
+
assertDisplay(working, "delegating", "Delegating...");
|
|
243
|
+
c.onToolEnd("run");
|
|
244
|
+
}
|
|
245
|
+
c.onToolStart("subagent_continue", "continue", { agent: null });
|
|
246
|
+
assertDisplay(working, "executing", "Executing...");
|
|
247
|
+
c.onToolEnd("continue");
|
|
248
|
+
c.onToolStart("subagent_continue", "continue", {});
|
|
249
|
+
assertDisplay(working, "working", "Working...");
|
|
250
|
+
c.onToolUpdate("subagent_continue", "continue", { details: { tasks: [
|
|
251
|
+
{ agent: "first", status: "running", live_activity: { current: { kind: "tool_running", tool_names: ["read"] } } },
|
|
252
|
+
{ agent: "second", status: "running", live_activity: { current: { kind: "thinking" } } },
|
|
253
|
+
] } });
|
|
254
|
+
assertDisplay(working, "exploring", "Exploring... (+1)");
|
|
255
|
+
c.onToolStart("subagent_run", "continue", { agent: "wrong" });
|
|
256
|
+
c.onToolUpdate("subagent_run", "continue", childProgress("wrong"));
|
|
257
|
+
assertDisplay(working, "exploring", "Exploring... (+1)");
|
|
258
|
+
c.onToolUpdate("subagent_continue", "continue", {});
|
|
259
|
+
assertDisplay(working, "working", "Working...");
|
|
260
|
+
c.onShutdown();
|
|
261
|
+
});
|
|
262
|
+
|
|
198
263
|
void test("assignment metadata is cleared on agent and session cleanup", (t) => {
|
|
199
264
|
t.mock.timers.enable({ apis: ["setInterval"] });
|
|
200
265
|
const { working, controller: c } = fixture();
|
|
201
266
|
for (const cleanup of [() => c.onAgentEnd(), () => c.onShutdown()]) {
|
|
202
267
|
c.onToolStart("subagent_run", "a", { agent: "scout", label: "map" });
|
|
203
|
-
assert.equal(working.label, "
|
|
268
|
+
assert.equal(working.label, "Working...");
|
|
204
269
|
cleanup(); c.onToolUpdate("subagent_run", "a", childProgress("late"));
|
|
205
270
|
c.refreshWorking(); assert.equal(working.label, "Thinking...");
|
|
206
271
|
assert.equal(working.active, false); assert.equal(working.timer, undefined);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OsdyState, WorkingActivity, WorkingWidgetState } from "./types.js";
|
|
2
2
|
import { classifyWorkingActivity, WORKING_ACTIVITY_LABELS } from "./working-activity.js";
|
|
3
|
-
import {
|
|
3
|
+
import { childActivityState, childAssignmentState, type ChildWorkingState } from "./working-child-activity.js";
|
|
4
4
|
|
|
5
5
|
export type WorkingController = {
|
|
6
6
|
onAgentStart(): void;
|
|
@@ -19,7 +19,12 @@ export function createWorkingController(
|
|
|
19
19
|
): WorkingController {
|
|
20
20
|
let activeAgent = false;
|
|
21
21
|
// Map insertion order is start order; duplicate starts must not reorder it.
|
|
22
|
-
type ActiveTool = {
|
|
22
|
+
type ActiveTool = {
|
|
23
|
+
activity: WorkingActivity;
|
|
24
|
+
toolName: string;
|
|
25
|
+
childState: ChildWorkingState | undefined;
|
|
26
|
+
assignmentState: ChildWorkingState | undefined;
|
|
27
|
+
};
|
|
23
28
|
const activeTools = new Map<string, ActiveTool>();
|
|
24
29
|
|
|
25
30
|
const requestWorkingRender = () => workingState.tui?.requestRender();
|
|
@@ -47,9 +52,9 @@ export function createWorkingController(
|
|
|
47
52
|
const refreshWorking = () => {
|
|
48
53
|
let current: ActiveTool | undefined;
|
|
49
54
|
for (const remaining of activeTools.values()) current = remaining;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
55
|
+
const child = current?.childState ?? current?.assignmentState;
|
|
56
|
+
setActivity(child?.activity ?? current?.activity ?? "thinking");
|
|
57
|
+
if (child) workingState.label = child.label;
|
|
53
58
|
if (!state.enabled || (!activeAgent && activeTools.size === 0)) {
|
|
54
59
|
stopWorking();
|
|
55
60
|
return;
|
|
@@ -76,13 +81,13 @@ export function createWorkingController(
|
|
|
76
81
|
onToolStart(toolName: string, toolCallId: string, args?: unknown): void {
|
|
77
82
|
if (activeTools.has(toolCallId)) return;
|
|
78
83
|
activeTools.set(toolCallId, { activity: classifyWorkingActivity(toolName, args), toolName,
|
|
79
|
-
|
|
84
|
+
childState: undefined, assignmentState: childAssignmentState(toolName, args) });
|
|
80
85
|
refreshWorking();
|
|
81
86
|
},
|
|
82
87
|
onToolUpdate(toolName: string, toolCallId: string, partialResult: unknown): void {
|
|
83
88
|
const tool = activeTools.get(toolCallId);
|
|
84
89
|
if ((toolName !== "subagent_run" && toolName !== "subagent_continue") || tool?.toolName !== toolName) return;
|
|
85
|
-
tool.
|
|
90
|
+
tool.childState = childActivityState(partialResult);
|
|
86
91
|
refreshWorking();
|
|
87
92
|
},
|
|
88
93
|
onToolEnd(toolCallId: string): void {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "osdy-pi",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.9",
|
|
4
4
|
"description": "Osdy Pi package: themes, header, and custom editor for Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"typescript": "^5.9.3",
|
|
84
84
|
"typescript-eslint": "^8.46.3"
|
|
85
85
|
},
|
|
86
|
-
"releaseSource": "
|
|
86
|
+
"releaseSource": "5e7c5215302d92dc19cf7aedea5d8b767cebd76a",
|
|
87
87
|
"repository": {
|
|
88
88
|
"type": "git",
|
|
89
89
|
"url": "git+https://github.com/OsdyOrtiz/Osdy-Pi.git"
|