osdy-pi 1.11.6 → 1.11.8

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
@@ -488,18 +488,20 @@ While work is active, Osdy Pi centers an activity label and compact animated gly
488
488
  | `Delegating...` | ✧ ✦ ✧ | Subagent handoffs. |
489
489
  | `Executing...` | ◴ ◷ ◶ ◵ | Unknown or ambiguous tools. |
490
490
 
491
- **Subagent assignment is not live activity.** While a `subagent_run` or `subagent_continue` parent call is active, the indicator distinguishes the two:
491
+ **Subagent fallback is not live activity.** While a `subagent_run` or `subagent_continue` parent call is active, the indicator distinguishes the two:
492
492
 
493
493
  | Source | Example | Meaning |
494
494
  | --- | --- | --- |
495
- | Validated public `agent` and optional `label` arguments | `explore · Task: map footer data` | Submitted assignment, not a claimed tool 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`) | `scout · read` | Observable child activity; takes precedence over assignment copy. |
495
+ | Validated public `agent` argument | `explore · 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`) | `scout · Exploring...` | Named `read` activity selects both the Exploring label and its existing glyph frames, overriding fallback. |
497
497
 
498
- Without a usable label, assignment copy is `Task assigned`; continuations without agent identity use `Subagent`, never an inferred identity. Full tasks, prompts and context are never read or displayed. Model activity uses fixed `Waiting on model` / `Responding` labels, never reasoning or response text.
498
+ Live child activity uses the same six states and corresponding animated glyphs listed above, retaining the agent prefix: `scout · 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
- For multiple running children, the indicator uses the first child in snapshot order with a compact additional-child count. Missing, malformed or no-running-child progress restores the assignment, or ordinary parent activity when assignment metadata 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.
500
+ The fallback is `<agent> · 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 uses `Subagent`, never an inferred 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
501
 
502
- **Background limitation:** assignments appear only while their parent call is active. There is no persistent background live feed, polling or task cache.
502
+ For multiple running children, the indicator uses the first child in snapshot order with a compact additional-child count. Missing, malformed, terminal or no-running-child progress restores both the agent's generic 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.
503
+
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
 
504
506
  When tools overlap, the latest remaining tool owns the display. All glyphs share a brightness pulse through the active theme's `mdQuoteBorder`, `thinkingHigh`, `accent`, and `borderAccent` colors; the animation signals activity, not measured percentage progress. Glyphs and labels use a single 80 ms timer, with no new controls or settings.
505
507
 
@@ -1,67 +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 { childActivityLabel, childAssignmentLabel: assignment } = await import("./working-child-activity.js");
11
+ const { childActivityState: activity, childAssignmentState: assignment } = await import("./working-child-activity.js");
12
+ const working = (agent: string) => ({ activity: "working", label: `${agent} · Working...` });
11
13
 
12
- void test("assignment uses only validated public agent and label metadata", () => {
13
- assert.equal(assignment("subagent_run", { agent: "explore", label: "map footer data" }), "explore · Task: map footer data");
14
+ void test("fallback uses only validated public agent identity regardless of optional label", () => {
15
+ assert.deepEqual(assignment("subagent_run", { agent: "explore", label: "map footer data" }), working("explore"));
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.equal(assignment("subagent_run", { agent: "scout", label, task: "SECRET", prompt: "SECRET", context: "SECRET" }), "scout · Task assigned");
18
- const metadata = { agent: "scout", get task() { throw new Error("task must not be read"); },
19
- get prompt() { throw new Error("prompt must not be read"); }, get context() { throw new Error("context must not be read"); } };
20
- assert.equal(assignment("subagent_run", metadata), "scout · Task assigned");
21
- assert.equal(assignment("read", { agent: "scout", label: "map" }), undefined);
22
- assert.equal(assignment("subagent_continue", { id: "scout", label: "resume" }), "Subagent · Task: resume");
23
- assert.equal(assignment("subagent_continue", { agent: 42, label: "resume" }), undefined);
24
- assert.equal(assignment("subagent_run", { agent: "\u001b[31m探索\u001b[0m\n\u202eagent", label: "\u001b]0;title\u0007map\t👩‍💻 footer\u0000" }), "探索 agent · Task: map 👩‍💻 footer");
25
- const bounded = assignment("subagent_run", { agent: "🙂".repeat(1000), label: "界".repeat(1000) });
26
- assert.equal(Array.from(bounded ?? "").length, 32 + " · Task: ".length + 80);
19
+ assert.deepEqual(assignment("subagent_run", { agent: "scout", label, task: "SECRET", prompt: "SECRET", context: "SECRET" }), working("scout"));
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("scout"));
24
+ assert.equal(assignment("read", { agent: "scout" }), undefined);
25
+ assert.deepEqual(assignment("subagent_continue", { id: "scout" }), working("Subagent"));
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("探索 agent"));
29
+ assert.deepEqual(assignment("subagent_run", { agent: "🙂".repeat(1000) }), working("🙂".repeat(32)));
27
30
  });
28
31
 
29
- function task(agent = "scout", kind = "tool_running", label = "Reading files", tool_names: unknown = ["read"]) {
30
- return { agent, status: "running", live_activity: { current: { kind, label, tool_names } } };
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"); } };
31
43
  }
32
44
  const progress = (...tasks: unknown[]) => ({ details: { tasks } });
33
45
 
34
- void test("running child shows name and current tool; successive snapshots replace activity", () => {
35
- assert.equal(childActivityLabel(progress(task())), "scout · read");
36
- assert.equal(childActivityLabel(progress(task("scout", "tool_running", "Checking", ["bash"]))), "scout · bash");
37
- assert.equal(childActivityLabel(progress(task("scout", "tool_completed", "Read completed", undefined))), "scout · read (done)");
38
- assert.equal(childActivityLabel(progress(task("scout", "tool_failed", "Failed", ["bash"]))), "scout · bash (failed)");
39
- assert.equal(childActivityLabel(progress({ agent: "scout", status: "running", live_activity: { current: { kind: "tool_running", label: "UNTRUSTED TEXT" } } })), "scout · Tool running");
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: `scout · ${label}` });
58
+ for (const kind of ["thinking", "streaming_response"])
59
+ assert.deepEqual(activity(progress(task("scout", kind))), { activity: "thinking", label: "scout · Thinking..." });
40
60
  });
41
61
 
42
- void test("multiple children select the first running child in snapshot order with bounded count", () => {
43
- assert.equal(childActivityLabel(progress({ agent: "old", status: "completed" }, task("first"), task("second"), task("third"))), "first · read (+2)");
44
- assert.equal(childActivityLabel(progress(...Array.from({ length: 65 }, () => task()))), undefined);
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: "first · Exploring... (+2)" });
65
+ assert.deepEqual(activity(progress(task("first", "tool_running", ["read", "write"]))), { activity: "exploring", label: "first · 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, "scout · Exploring... (+63)");
45
69
  });
46
70
 
47
- void test("terminal, missing, malformed and unsupported progress falls back", () => {
48
- for (const status of ["completed", "failed", "cancelled", "aborted", "pending", "interrupted", "stopping", "queued"]) {
49
- assert.equal(childActivityLabel(progress({ ...task(), status })), undefined);
50
- }
51
- for (const value of [undefined, null, [], {}, { details: null }, { details: { tasks: {} } }, progress(), progress(null), progress({ ...task(), agent: 1 }), progress({ ...task(), status: "unknown" }), progress({ agent: "scout", status: "running" }), progress(task("scout", "unknown")), progress(task("scout", "tool_running", "Read", [1]))]) {
52
- 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);
53
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);
54
91
  });
55
92
 
56
- void test("model activity never exposes thinking labels or response text", () => {
57
- assert.equal(childActivityLabel(progress(task("scout", "thinking", "PRIVATE REASONING"))), "scout · Waiting on model");
58
- assert.equal(childActivityLabel(progress(task("scout", "streaming_response", "PRIVATE RESPONSE"))), "scout · Responding");
93
+ void test("snapshot agent at the input bound remains valid and is truncated for display", () => {
94
+ assert.deepEqual(activity(progress(task("a".repeat(4096)))),
95
+ { activity: "exploring", label: `${"a".repeat(32)} · Exploring...` });
59
96
  });
60
97
 
61
- void test("external labels are single-line, control-free and bounded", () => {
98
+ void test("external names are single-line, control-free and bounded before classification", () => {
62
99
  const dirty = "\u001b[31mAgent\u001b[0m\n\t\u202eName\u0007";
63
- assert.equal(childActivityLabel(progress(task(dirty, "tool_running", "unused", ["\u001b]0;title\u0007read\nfile"]))), "Agent Name · read file");
64
- const label = childActivityLabel(progress(task("a".repeat(1000), "tool_running", "unused", ["b".repeat(1000)])));
65
- assert.ok(label && label.length <= 100);
66
- assert.equal(childActivityLabel(progress(task("\u0000\n"))), undefined);
100
+ assert.deepEqual(activity(progress(task(dirty, "tool_running", ["\u001b]0;title\u0007read\nfile"]))),
101
+ { activity: "executing", label: "Agent Name · Executing..." });
102
+ assert.deepEqual(activity(progress(task("a".repeat(1000), "tool_running", ["b".repeat(1000)]))),
103
+ { activity: "executing", label: `${"a".repeat(32)} · Executing...` });
104
+ assert.equal(activity(progress(task("\u0000\n"))), undefined);
67
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(agent: string, activity: WorkingActivity): ChildWorkingState {
8
+ return { activity, label: `${agent} · ${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,22 +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 childAssignmentLabel(toolName: string, args: unknown): string | undefined {
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
- const label = safeLabel(args.label, 80);
23
- return `${agent} · ${label ? `Task: ${label}` : "Task assigned"}`;
30
+ return childState(agent, "working");
24
31
  }
25
32
 
26
- function observableActivity(value: unknown): string | undefined {
27
- if (!record(value) || typeof value.label !== "string") return undefined;
28
- // Ignore model labels: they may contain response text or imply access to reasoning.
29
- if (value.kind === "thinking") return "Waiting on model";
30
- 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";
31
37
  if (value.kind !== "tool_running" && value.kind !== "tool_completed" && value.kind !== "tool_failed") return undefined;
32
- let tool = "Tool";
38
+ let tool: string | undefined;
33
39
  if (value.tool_names !== undefined) {
34
40
  if (!Array.isArray(value.tool_names) || value.tool_names.length > 16) return undefined;
35
41
  const names: string[] = [];
@@ -38,19 +44,18 @@ function observableActivity(value: unknown): string | undefined {
38
44
  if (!safe) return undefined;
39
45
  names.push(safe);
40
46
  }
41
- tool = names[0] ?? tool;
47
+ tool = names[0];
42
48
  }
43
- if (value.kind === "tool_completed") return `${tool} (done)`;
44
- if (value.kind === "tool_failed") return `${tool} (failed)`;
45
- 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);
46
51
  }
47
52
 
48
53
  /** Consume only public foreground metadata, not content, trails or transcripts. */
49
- export function childActivityLabel(partialResult: unknown): string | undefined {
54
+ export function childActivityState(partialResult: unknown): ChildWorkingState | undefined {
50
55
  if (!record(partialResult) || !record(partialResult.details)) return undefined;
51
56
  const tasks: unknown = partialResult.details.tasks;
52
57
  if (!Array.isArray(tasks) || tasks.length > 64) return undefined;
53
- let first: string | undefined;
58
+ let first: ChildWorkingState | undefined;
54
59
  let running = 0;
55
60
  for (const task of tasks) {
56
61
  if (!record(task) || typeof task.status !== "string") return undefined;
@@ -58,10 +63,10 @@ export function childActivityLabel(partialResult: unknown): string | undefined {
58
63
  const agent = safeLabel(task.agent, 32);
59
64
  const activity = record(task.live_activity) ? observableActivity(task.live_activity.current) : undefined;
60
65
  if (!agent || !activity) return undefined;
61
- first ??= `${agent} · ${activity}`;
66
+ first ??= childState(agent, activity);
62
67
  running++;
63
68
  }
64
69
  if (!first) return undefined;
65
- if (running > 1) return `${first} (+${running - 1})`;
70
+ if (running > 1) return { ...first, label: `${first.label} (+${running - 1})` };
66
71
  return first;
67
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", "scout · 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, `scout · ${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
- assert.equal(working.label, "scout · read");
148
+ assertDisplay(working, "exploring", "scout · 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
- assert.equal(working.label, "writer · edit");
155
+ assertDisplay(working, "working", "writer · 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"); assert.equal(working.label, "writer · edit");
127
- c.onToolEnd("b"); assert.equal(working.label, "scout · grep");
159
+ c.onToolEnd("ordinary"); assertDisplay(working, "working", "writer · Working...");
160
+ c.onToolEnd("b"); assertDisplay(working, "exploring", "scout · Exploring...");
128
161
  c.onToolUpdate("subagent_run", "b", childProgress("late"));
129
- assert.equal(working.label, "scout · grep");
162
+ assertDisplay(working, "exploring", "scout · 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"));
@@ -150,16 +183,20 @@ void test("agent/session disposal clears child progress and rejects late updates
150
183
  }
151
184
  });
152
185
 
153
- void test("assignments appear at start and survive unavailable or terminal live progress", (t) => {
186
+ void test("agent fallback appears at start and resets after unavailable, malformed or terminal live progress", (t) => {
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
- assert.equal(working.label, "explore · Task: map footer data");
190
+ assertDisplay(working, "working", "explore · Working...");
158
191
  c.onToolUpdate("subagent_run", "a", childProgress("explore"));
159
- assert.equal(working.label, "explore · read");
160
- for (const update of [undefined, {}, { details: { tasks: null } }, { details: { tasks: [{ agent: "explore", status: "running" }] } }, childProgress("explore", "read", "completed")]) {
192
+ assertDisplay(working, "exploring", "explore · 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" } } }] } }]) {
196
+ c.onToolUpdate("subagent_run", "a", childProgress("explore"));
197
+ assertDisplay(working, "exploring", "explore · Exploring...");
161
198
  c.onToolUpdate("subagent_run", "a", update);
162
- assert.equal(working.label, "explore · Task: map footer data");
199
+ assertDisplay(working, "working", "explore · Working...");
163
200
  }
164
201
  c.onToolEnd("a"); assert.equal(working.label, "Thinking...");
165
202
  });
@@ -168,37 +205,67 @@ void test("continuations use generic identity without guessing and preserve call
168
205
  t.mock.timers.enable({ apis: ["setInterval"] });
169
206
  const { working, controller: c } = fixture();
170
207
  c.onToolStart("subagent_run", "a", { agent: "scout" });
171
- assert.equal(working.label, "scout · Task assigned");
208
+ assert.equal(working.label, "scout · Working...");
172
209
  c.onToolStart("subagent_continue", "b", { id: "a", label: "check footer" });
173
- assert.equal(working.label, "Subagent · Task: check footer");
210
+ assert.equal(working.label, "Subagent · Working...");
174
211
  c.onToolStart("subagent_continue", "a", { agent: "wrong", label: "wrong" });
175
212
  c.onToolUpdate("subagent_run", "b", childProgress("wrong"));
176
213
  c.onToolUpdate("subagent_continue", "missing", childProgress("wrong"));
177
- assert.equal(working.label, "Subagent · Task: check footer");
214
+ assert.equal(working.label, "Subagent · Working...");
178
215
  c.onToolUpdate("subagent_continue", "b", childProgress("writer", "edit"));
179
- assert.equal(working.label, "writer · edit");
216
+ assertDisplay(working, "working", "writer · Working...");
180
217
  c.onToolUpdate("subagent_run", "a", childProgress("scout"));
181
- assert.equal(working.label, "writer · edit");
218
+ assertDisplay(working, "working", "writer · Working...");
182
219
  c.onToolUpdate("subagent_continue", "b", {});
183
- assert.equal(working.label, "Subagent · Task: check footer");
184
- c.onToolEnd("b"); assert.equal(working.label, "scout · read");
220
+ assert.equal(working.label, "Subagent · Working...");
221
+ c.onToolEnd("b"); assertDisplay(working, "exploring", "scout · Exploring...");
185
222
  c.onToolUpdate("subagent_continue", "b", childProgress("late"));
186
- c.onToolUpdate("subagent_run", "a", {}); assert.equal(working.label, "scout · Task assigned");
223
+ c.onToolUpdate("subagent_run", "a", {}); assert.equal(working.label, "scout · Working...");
187
224
  c.onToolEnd("a");
188
225
  c.onToolStart("subagent_continue", "c", { agent: "writer", label: "finish" });
189
- assert.equal(working.label, "writer · Task: finish");
226
+ assert.equal(working.label, "writer · Working...");
190
227
  c.onToolEnd("c");
191
228
  c.onToolStart("subagent_continue", "d", { id: "c", task: "SECRET" });
192
- assert.equal(working.label, "Subagent · Task assigned");
229
+ assert.equal(working.label, "Subagent · Working...");
193
230
  c.onToolEnd("d");
194
231
  });
195
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", "public · 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", "Subagent · 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", "first · Exploring... (+1)");
255
+ c.onToolStart("subagent_run", "continue", { agent: "wrong" });
256
+ c.onToolUpdate("subagent_run", "continue", childProgress("wrong"));
257
+ assertDisplay(working, "exploring", "first · Exploring... (+1)");
258
+ c.onToolUpdate("subagent_continue", "continue", {});
259
+ assertDisplay(working, "working", "Subagent · Working...");
260
+ c.onShutdown();
261
+ });
262
+
196
263
  void test("assignment metadata is cleared on agent and session cleanup", (t) => {
197
264
  t.mock.timers.enable({ apis: ["setInterval"] });
198
265
  const { working, controller: c } = fixture();
199
266
  for (const cleanup of [() => c.onAgentEnd(), () => c.onShutdown()]) {
200
267
  c.onToolStart("subagent_run", "a", { agent: "scout", label: "map" });
201
- assert.equal(working.label, "scout · Task: map");
268
+ assert.equal(working.label, "scout · Working...");
202
269
  cleanup(); c.onToolUpdate("subagent_run", "a", childProgress("late"));
203
270
  c.refreshWorking(); assert.equal(working.label, "Thinking...");
204
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 { childActivityLabel, childAssignmentLabel } from "./working-child-activity.js";
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 = { activity: WorkingActivity; toolName: string; childLabel: string | undefined; assignmentLabel: string | undefined };
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
- setActivity(current?.activity ?? "thinking");
51
- const label = current?.childLabel ?? current?.assignmentLabel;
52
- if (label) workingState.label = label;
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
- childLabel: undefined, assignmentLabel: childAssignmentLabel(toolName, args) });
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.childLabel = childActivityLabel(partialResult);
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.6",
3
+ "version": "1.11.8",
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": "58395349a28ff9d86413b6fee0d964e477543b3e",
86
+ "releaseSource": "f0af5964701aaf9022bb79d1ff2d25dbc389b7dc",
87
87
  "repository": {
88
88
  "type": "git",
89
89
  "url": "git+https://github.com/OsdyOrtiz/Osdy-Pi.git"