osdy-pi 1.11.5 → 1.11.7

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,6 +488,19 @@ 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 fallback is not live activity.** While a `subagent_run` or `subagent_continue` parent call is active, the indicator distinguishes the two:
492
+
493
+ | Source | Example | Meaning |
494
+ | --- | --- | --- |
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 · read` | Observable child activity; takes precedence over generic fallback copy. |
497
+
498
+ The fallback is `<agent> · Working...` regardless of optional `label` metadata; 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.
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 agent's generic fallback, 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
+
502
+ **Background limitation:** agent fallbacks appear only while their parent call is active. There is no persistent background live feed, polling or task cache.
503
+
491
504
  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.
492
505
 
493
506
  A two-color wave travels across the label: the current letter uses the active theme's `accent`, the trailing letter uses `warning`, and the rest use `text`. Only the current `accent` letter tries the active theme's bold styling; this is a terminal-dependent visual trial, not a change to font size. The trailing `warning` letter and glyph stay plain. The glyph and label update when the theme changes. Osdy Pi hides Pi's built-in working row while enabled to avoid a duplicate indicator.
@@ -1222,6 +1222,9 @@ export async function registerOsdyPi(
1222
1222
  pi.on("tool_execution_start", (event) =>
1223
1223
  controller.onToolStart(event.toolName, event.toolCallId, event.args),
1224
1224
  );
1225
+ pi.on("tool_execution_update", (event) =>
1226
+ controller.onToolUpdate(event.toolName, event.toolCallId, event.partialResult),
1227
+ );
1225
1228
  pi.on("tool_execution_end", (event, ctx) => {
1226
1229
  controller.onToolEnd(event.toolCallId);
1227
1230
  audioRouter.onToolExecutionEnd(event.isError === true, ctx);
@@ -1253,6 +1256,7 @@ export async function registerOsdyPi(
1253
1256
  sessionContext = undefined;
1254
1257
  });
1255
1258
  pi.on("session_start", async (_event, ctx) => {
1259
+ controller.onShutdown();
1256
1260
  idleUsageRefresh.stop();
1257
1261
  codexUsageAbort?.abort();
1258
1262
  codexUsageAbort = undefined;
@@ -204,6 +204,14 @@ void test("combining marks and ZWJ emoji remain whole graphemes in the traveling
204
204
  }
205
205
  });
206
206
 
207
+ void test("compact child labels with wide characters fit the existing renderer", () => {
208
+ for (const width of [1, 2, 5, 12, 40, 100]) {
209
+ const current = { ...state("探索👩‍💻 · read (+2)"), activity: "delegating" as const };
210
+ const line = renderWorkingWidget(current, palette, width)[0] ?? "";
211
+ assert.ok(visibleWidth(line) <= width, `child label width ${width}`);
212
+ }
213
+ });
214
+
207
215
  void test("inactive widget renders nothing; narrow output fits and full output stays centered", () => {
208
216
  assert.deepEqual(renderWorkingWidget(state("Working...", 18, false), palette, 20), []);
209
217
  for (const activity of activities) {
@@ -0,0 +1,68 @@
1
+ import assert from "node:assert/strict";
2
+ import { registerHooks } from "node:module";
3
+ import test from "node:test";
4
+
5
+ registerHooks({ resolve(specifier, context, nextResolve) {
6
+ if (specifier.startsWith(".") && specifier.endsWith(".js") && context.parentURL?.endsWith(".ts"))
7
+ return nextResolve(`${specifier.slice(0, -3)}.ts`, context);
8
+ return nextResolve(specifier, context);
9
+ } });
10
+ const { childActivityLabel, childAssignmentLabel: assignment } = await import("./working-child-activity.js");
11
+
12
+ void test("fallback uses only validated public agent identity regardless of optional label", () => {
13
+ assert.equal(assignment("subagent_run", { agent: "explore", label: "map footer data" }), "explore · Working...");
14
+ for (const args of [undefined, null, [], "scout", {}, { agent: 1 }, { agent: "\u0000\n" }, { agent: "a".repeat(4097) }, { label: "secret" }])
15
+ assert.equal(assignment("subagent_run", args), undefined);
16
+ 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 · Working...");
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 · Working...");
21
+ assert.equal(assignment("subagent_run", { agent: "scout" }), "scout · Working...");
22
+ assert.equal(assignment("read", { agent: "scout", label: "map" }), undefined);
23
+ assert.equal(assignment("subagent_continue", { id: "scout", label: "resume" }), "Subagent · Working...");
24
+ assert.equal(assignment("subagent_continue", { agent: 42, label: "resume" }), undefined);
25
+ assert.equal(assignment("subagent_run", { agent: "\u001b[31m探索\u001b[0m\n\u202eagent", label: "\u001b]0;title\u0007map\t👩‍💻 footer\u0000" }), "探索 agent · Working...");
26
+ const bounded = assignment("subagent_run", { agent: "🙂".repeat(1000), label: "界".repeat(1000) });
27
+ assert.equal(Array.from(bounded ?? "").length, 32 + " · Working...".length);
28
+ });
29
+
30
+ function task(agent = "scout", kind = "tool_running", label = "Reading files", tool_names: unknown = ["read"]) {
31
+ return { agent, status: "running", live_activity: { current: { kind, label, tool_names } } };
32
+ }
33
+ const progress = (...tasks: unknown[]) => ({ details: { tasks } });
34
+
35
+ void test("running child shows name and current tool; successive snapshots replace activity", () => {
36
+ assert.equal(childActivityLabel(progress(task())), "scout · read");
37
+ assert.equal(childActivityLabel(progress(task("scout", "tool_running", "Checking", ["bash"]))), "scout · bash");
38
+ assert.equal(childActivityLabel(progress(task("scout", "tool_completed", "Read completed", undefined))), "scout · read (done)");
39
+ assert.equal(childActivityLabel(progress(task("scout", "tool_failed", "Failed", ["bash"]))), "scout · bash (failed)");
40
+ assert.equal(childActivityLabel(progress({ agent: "scout", status: "running", live_activity: { current: { kind: "tool_running", label: "UNTRUSTED TEXT" } } })), "scout · Tool running");
41
+ });
42
+
43
+ void test("multiple children select the first running child in snapshot order with bounded count", () => {
44
+ assert.equal(childActivityLabel(progress({ agent: "old", status: "completed" }, task("first"), task("second"), task("third"))), "first · read (+2)");
45
+ assert.equal(childActivityLabel(progress(...Array.from({ length: 65 }, () => task()))), undefined);
46
+ });
47
+
48
+ void test("terminal, missing, malformed and unsupported progress falls back", () => {
49
+ for (const status of ["completed", "failed", "cancelled", "aborted", "pending", "interrupted", "stopping", "queued"]) {
50
+ assert.equal(childActivityLabel(progress({ ...task(), status })), undefined);
51
+ }
52
+ 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]))]) {
53
+ assert.equal(childActivityLabel(value), undefined, JSON.stringify(value));
54
+ }
55
+ });
56
+
57
+ void test("model activity never exposes thinking labels or response text", () => {
58
+ assert.equal(childActivityLabel(progress(task("scout", "thinking", "PRIVATE REASONING"))), "scout · Waiting on model");
59
+ assert.equal(childActivityLabel(progress(task("scout", "streaming_response", "PRIVATE RESPONSE"))), "scout · Responding");
60
+ });
61
+
62
+ void test("external labels are single-line, control-free and bounded", () => {
63
+ const dirty = "\u001b[31mAgent\u001b[0m\n\t\u202eName\u0007";
64
+ assert.equal(childActivityLabel(progress(task(dirty, "tool_running", "unused", ["\u001b]0;title\u0007read\nfile"]))), "Agent Name · read file");
65
+ const label = childActivityLabel(progress(task("a".repeat(1000), "tool_running", "unused", ["b".repeat(1000)])));
66
+ assert.ok(label && label.length <= 100);
67
+ assert.equal(childActivityLabel(progress(task("\u0000\n"))), undefined);
68
+ });
@@ -0,0 +1,66 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+
3
+ function record(value: unknown): value is Record<string, unknown> {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+
7
+ /** Bound untrusted metadata before rendering; retain ZWJ emoji, never terminal controls. */
8
+ function safeLabel(value: unknown, limit: number): string | undefined {
9
+ if (typeof value !== "string" || value.length > 4096) return undefined;
10
+ const clean = stripVTControlCharacters(value)
11
+ .replace(/[\p{Cc}\p{Cf}]/gu, character => character === "\u200d" ? character : " ")
12
+ .replace(/\s+/gu, " ").trim();
13
+ return Array.from(clean).slice(0, limit).join("") || undefined;
14
+ }
15
+
16
+ /** Assignment only: never infer activity or identity from task, prompt or context. */
17
+ export function childAssignmentLabel(toolName: string, args: unknown): string | undefined {
18
+ if (toolName !== "subagent_run" && toolName !== "subagent_continue") return undefined;
19
+ if (!record(args)) return undefined;
20
+ const agent = args.agent === undefined && toolName === "subagent_continue" ? "Subagent" : safeLabel(args.agent, 32);
21
+ if (!agent) return undefined;
22
+ return `${agent} · Working...`;
23
+ }
24
+
25
+ function observableActivity(value: unknown): string | undefined {
26
+ if (!record(value) || typeof value.label !== "string") return undefined;
27
+ // Ignore model labels: they may contain response text or imply access to reasoning.
28
+ if (value.kind === "thinking") return "Waiting on model";
29
+ if (value.kind === "streaming_response") return "Responding";
30
+ if (value.kind !== "tool_running" && value.kind !== "tool_completed" && value.kind !== "tool_failed") return undefined;
31
+ let tool = "Tool";
32
+ if (value.tool_names !== undefined) {
33
+ if (!Array.isArray(value.tool_names) || value.tool_names.length > 16) return undefined;
34
+ const names: string[] = [];
35
+ for (const name of value.tool_names) {
36
+ const safe = safeLabel(name, 40);
37
+ if (!safe) return undefined;
38
+ names.push(safe);
39
+ }
40
+ tool = names[0] ?? tool;
41
+ }
42
+ if (value.kind === "tool_completed") return `${tool} (done)`;
43
+ if (value.kind === "tool_failed") return `${tool} (failed)`;
44
+ return tool === "Tool" ? "Tool running" : tool;
45
+ }
46
+
47
+ /** Consume only public foreground metadata, not content, trails or transcripts. */
48
+ export function childActivityLabel(partialResult: unknown): string | undefined {
49
+ if (!record(partialResult) || !record(partialResult.details)) return undefined;
50
+ const tasks: unknown = partialResult.details.tasks;
51
+ if (!Array.isArray(tasks) || tasks.length > 64) return undefined;
52
+ let first: string | undefined;
53
+ let running = 0;
54
+ for (const task of tasks) {
55
+ if (!record(task) || typeof task.status !== "string") return undefined;
56
+ if (task.status !== "running") continue;
57
+ const agent = safeLabel(task.agent, 32);
58
+ const activity = record(task.live_activity) ? observableActivity(task.live_activity.current) : undefined;
59
+ if (!agent || !activity) return undefined;
60
+ first ??= `${agent} · ${activity}`;
61
+ running++;
62
+ }
63
+ if (!first) return undefined;
64
+ if (running > 1) return `${first} (+${running - 1})`;
65
+ return first;
66
+ }
@@ -103,6 +103,110 @@ void test("MCP wrappers explore only exact Context7 documentation selectors", (t
103
103
  c.onToolEnd("case");
104
104
  });
105
105
 
106
+ const childProgress = (agent: string, tool = "read", status = "running") => ({ details: { tasks: [
107
+ { agent, status, live_activity: { current: { kind: "tool_running", label: "Reading", tool_names: [tool] } } },
108
+ ] } });
109
+
110
+ void test("child updates respect tool identity and latest remaining tool ownership", (t) => {
111
+ t.mock.timers.enable({ apis: ["setInterval"] });
112
+ const { working, controller: c } = fixture();
113
+ c.onToolStart("subagent_run", "a");
114
+ c.onToolUpdate("subagent_run", "a", childProgress("scout"));
115
+ assert.equal(working.label, "scout · read");
116
+ c.onToolStart("subagent_run", "b");
117
+ c.onToolUpdate("subagent_run", "b", childProgress("writer", "edit"));
118
+ c.onToolUpdate("subagent_run", "a", childProgress("scout", "grep"));
119
+ c.onToolUpdate("read", "b", childProgress("wrong"));
120
+ c.onToolUpdate("subagent_run", "missing", childProgress("late"));
121
+ c.onToolEnd("missing");
122
+ assert.equal(working.label, "writer · edit");
123
+ c.onToolStart("read", "ordinary");
124
+ c.onToolUpdate("subagent_run", "ordinary", childProgress("wrong"));
125
+ 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");
128
+ c.onToolUpdate("subagent_run", "b", childProgress("late"));
129
+ assert.equal(working.label, "scout · grep");
130
+ c.onToolUpdate("subagent_run", "a", childProgress("scout", "read", "completed"));
131
+ assert.equal(working.label, "Delegating...");
132
+ c.onToolUpdate("subagent_run", "a", childProgress("scout"));
133
+ c.onToolUpdate("subagent_run", "a", { details: { tasks: null } });
134
+ assert.equal(working.label, "Delegating...");
135
+ c.onToolEnd("a"); assert.equal(working.active, false);
136
+ });
137
+
138
+ void test("agent/session disposal clears child progress and rejects late updates", (t) => {
139
+ t.mock.timers.enable({ apis: ["setInterval"] });
140
+ const { working, controller: c } = fixture();
141
+ for (const cleanup of [() => c.onAgentEnd(), () => c.onShutdown()]) {
142
+ c.onToolStart("subagent_run", "a");
143
+ c.onToolUpdate("subagent_run", "a", childProgress("scout"));
144
+ cleanup();
145
+ c.onToolUpdate("subagent_run", "a", childProgress("late"));
146
+ assert.equal(working.active, false); assert.equal(working.label, "Thinking...");
147
+ assert.equal(working.timer, undefined);
148
+ c.onAgentStart(); assert.equal(working.label, "Thinking...");
149
+ c.onAgentEnd();
150
+ }
151
+ });
152
+
153
+ void test("agent fallback appears at start and resets after unavailable, malformed or terminal live progress", (t) => {
154
+ t.mock.timers.enable({ apis: ["setInterval"] });
155
+ const { working, controller: c } = fixture();
156
+ c.onToolStart("subagent_run", "a", { agent: "explore", label: "map footer data", task: "SECRET TASK", prompt: "SECRET PROMPT" });
157
+ assert.equal(working.label, "explore · Working...");
158
+ 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")]) {
161
+ c.onToolUpdate("subagent_run", "a", childProgress("explore"));
162
+ assert.equal(working.label, "explore · read");
163
+ c.onToolUpdate("subagent_run", "a", update);
164
+ assert.equal(working.label, "explore · Working...");
165
+ }
166
+ c.onToolEnd("a"); assert.equal(working.label, "Thinking...");
167
+ });
168
+
169
+ void test("continuations use generic identity without guessing and preserve call ownership", (t) => {
170
+ t.mock.timers.enable({ apis: ["setInterval"] });
171
+ const { working, controller: c } = fixture();
172
+ c.onToolStart("subagent_run", "a", { agent: "scout" });
173
+ assert.equal(working.label, "scout · Working...");
174
+ c.onToolStart("subagent_continue", "b", { id: "a", label: "check footer" });
175
+ assert.equal(working.label, "Subagent · Working...");
176
+ c.onToolStart("subagent_continue", "a", { agent: "wrong", label: "wrong" });
177
+ c.onToolUpdate("subagent_run", "b", childProgress("wrong"));
178
+ c.onToolUpdate("subagent_continue", "missing", childProgress("wrong"));
179
+ assert.equal(working.label, "Subagent · Working...");
180
+ c.onToolUpdate("subagent_continue", "b", childProgress("writer", "edit"));
181
+ assert.equal(working.label, "writer · edit");
182
+ c.onToolUpdate("subagent_run", "a", childProgress("scout"));
183
+ assert.equal(working.label, "writer · edit");
184
+ c.onToolUpdate("subagent_continue", "b", {});
185
+ assert.equal(working.label, "Subagent · Working...");
186
+ c.onToolEnd("b"); assert.equal(working.label, "scout · read");
187
+ c.onToolUpdate("subagent_continue", "b", childProgress("late"));
188
+ c.onToolUpdate("subagent_run", "a", {}); assert.equal(working.label, "scout · Working...");
189
+ c.onToolEnd("a");
190
+ c.onToolStart("subagent_continue", "c", { agent: "writer", label: "finish" });
191
+ assert.equal(working.label, "writer · Working...");
192
+ c.onToolEnd("c");
193
+ c.onToolStart("subagent_continue", "d", { id: "c", task: "SECRET" });
194
+ assert.equal(working.label, "Subagent · Working...");
195
+ c.onToolEnd("d");
196
+ });
197
+
198
+ void test("assignment metadata is cleared on agent and session cleanup", (t) => {
199
+ t.mock.timers.enable({ apis: ["setInterval"] });
200
+ const { working, controller: c } = fixture();
201
+ for (const cleanup of [() => c.onAgentEnd(), () => c.onShutdown()]) {
202
+ c.onToolStart("subagent_run", "a", { agent: "scout", label: "map" });
203
+ assert.equal(working.label, "scout · Working...");
204
+ cleanup(); c.onToolUpdate("subagent_run", "a", childProgress("late"));
205
+ c.refreshWorking(); assert.equal(working.label, "Thinking...");
206
+ assert.equal(working.active, false); assert.equal(working.timer, undefined);
207
+ }
208
+ });
209
+
106
210
  void test("bash accepts only simple anchored explicit check commands", (t) => {
107
211
  t.mock.timers.enable({ apis: ["setInterval"] });
108
212
  const { working, controller: c } = fixture();
@@ -1,10 +1,12 @@
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
4
 
4
5
  export type WorkingController = {
5
6
  onAgentStart(): void;
6
7
  onAgentEnd(): void;
7
8
  onToolStart(toolName: string, toolCallId: string, args?: unknown): void;
9
+ onToolUpdate(toolName: string, toolCallId: string, partialResult: unknown): void;
8
10
  onToolEnd(toolCallId: string): void;
9
11
  onShutdown(): void;
10
12
  refreshWorking(): void;
@@ -17,7 +19,8 @@ export function createWorkingController(
17
19
  ): WorkingController {
18
20
  let activeAgent = false;
19
21
  // Map insertion order is start order; duplicate starts must not reorder it.
20
- const activeTools = new Map<string, WorkingActivity>();
22
+ type ActiveTool = { activity: WorkingActivity; toolName: string; childLabel: string | undefined; assignmentLabel: string | undefined };
23
+ const activeTools = new Map<string, ActiveTool>();
21
24
 
22
25
  const requestWorkingRender = () => workingState.tui?.requestRender();
23
26
  const clearWorkingTimer = () => {
@@ -42,9 +45,11 @@ export function createWorkingController(
42
45
  requestWorkingRender();
43
46
  };
44
47
  const refreshWorking = () => {
45
- let activity: WorkingActivity = "thinking";
46
- for (const remaining of activeTools.values()) activity = remaining;
47
- setActivity(activity);
48
+ let current: ActiveTool | undefined;
49
+ 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;
48
53
  if (!state.enabled || (!activeAgent && activeTools.size === 0)) {
49
54
  stopWorking();
50
55
  return;
@@ -70,7 +75,14 @@ export function createWorkingController(
70
75
  },
71
76
  onToolStart(toolName: string, toolCallId: string, args?: unknown): void {
72
77
  if (activeTools.has(toolCallId)) return;
73
- activeTools.set(toolCallId, classifyWorkingActivity(toolName, args));
78
+ activeTools.set(toolCallId, { activity: classifyWorkingActivity(toolName, args), toolName,
79
+ childLabel: undefined, assignmentLabel: childAssignmentLabel(toolName, args) });
80
+ refreshWorking();
81
+ },
82
+ onToolUpdate(toolName: string, toolCallId: string, partialResult: unknown): void {
83
+ const tool = activeTools.get(toolCallId);
84
+ if ((toolName !== "subagent_run" && toolName !== "subagent_continue") || tool?.toolName !== toolName) return;
85
+ tool.childLabel = childActivityLabel(partialResult);
74
86
  refreshWorking();
75
87
  },
76
88
  onToolEnd(toolCallId: string): void {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osdy-pi",
3
- "version": "1.11.5",
3
+ "version": "1.11.7",
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": "312ca263d7eaa2a151db46121380b97fc60f0831",
86
+ "releaseSource": "fa08213880378e71ddcadea11cd71da3406d7cf6",
87
87
  "repository": {
88
88
  "type": "git",
89
89
  "url": "git+https://github.com/OsdyOrtiz/Osdy-Pi.git"