@hheei/pi-ext-ui 0.5.2 → 0.7.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 CHANGED
@@ -8,8 +8,8 @@ The shared view API owns fold levels, compact sections, status glyphs, copy feed
8
8
 
9
9
  Background Bash and subagent tools own their display formats through ext-core `ToolPresentation` providers. This resolver consumes those providers automatically, including nested codemode calls, and adds shared folding and copy controls. Without `pi-ext-ui`, the same tools retain native Pi renderers. No direct dependency on `pi-ext-ui` is required by the tool extensions.
10
10
 
11
- Activity groups are on by default for Pi **>=1.0.1** TUI sessions. Turn them off with the Tool UI setting `toolGroups.enabled` in `ext_settings.json` (the ext-core settings page). The change applies to the current session. A heading such as `▸ codemode ×2 · grep ×3` keeps one live summary row per tool beneath a single outer rail. Each thinking round and notification also keeps a summary row; notifications are not counted. Groups form while tools run and can span consecutive tool/thinking rounds. Prose, user messages, unknown components, images, and unknown tool renderers break groups. Any MessageView transcript notice stays inside the group, so bg-bash, subagent, memory, optimizer, and codemode-guard notices do not need their own allowlist. Notification rows show their text directly. Streaming arguments, partial results, completion, and errors update the existing row.
11
+ Activity groups are on by default for Pi **>=1.0.1** TUI sessions. Turn them off with the Tool UI setting `toolGroups.enabled` in `ext_settings.json` (the ext-core settings page). The change applies to the current session. A heading such as `▸ codemode ×2 · grep ×3` keeps one live summary row per tool beneath a single outer rail. Each thinking round and notification also keeps a summary row; notifications are not counted. A thinking item shows a content preview while Pi hides thinking and opens the full text on demand; while thinking is visible the category prints it flat. Groups form while tools run and can span consecutive tool/thinking rounds. Prose, user messages, unknown components, images, and unknown tool renderers break groups. Any MessageView transcript notice stays inside the group, so bg-bash, subagent, memory, optimizer, and codemode-guard notices do not need their own allowlist. Notification rows show their own text once, indented and width-wrapped; only a MessageView with a real body gets an expand pin. Streaming arguments, partial results, completion, and errors update the existing row.
12
12
 
13
- Fullscreen: click the group heading to restore the original component layouts and interactions beneath the outer rail. Both terminal modes: Ctrl+O forces groups open; closing global expansion restores each group’s local choice. Appending members preserves that choice. Original per-tool fold choices remain intact. Groups are presentation only and are rebuilt on history navigation; session data, model context, and HTML export remain unchanged. Thinking summaries keep a label; expanding respects native thinking visibility. Each `ctx.ui.notify` call retains its full text for expansion. Notifications remain session-local, as in Pi.
13
+ Fullscreen: click the group heading to restore the original component layouts and interactions beneath the outer rail. Both terminal modes: Ctrl+O forces groups open; closing global expansion restores each group’s local choice. Appending members preserves that choice. Original per-tool fold choices remain intact. Groups are presentation only and are rebuilt on history navigation; session data, model context, and HTML export remain unchanged. Thinking keeps its label; the hidden state shows a preview instead of dropping the text, and the visible state has no per-item arrow. Each `ctx.ui.notify` keeps its full text inline. Notifications remain session-local, as in Pi.
14
14
 
15
15
  Grouping uses two centralized, version-checked host prototype hooks: tool lookup and extension notifications. Thinking uses the existing assistant message, with no extra hook. Unsupported hosts or incompatible layouts report a warning and retain normal rendering. Cleanup restores the original methods; if another extension replaces the same methods, restart Pi to unload the conflicting patch. For workspace development use `pnpm run pi-dev`: running the bundled CLI beside physical host devDependencies can resolve a second host class, unlike the published installation.
@@ -0,0 +1,11 @@
1
+ import type { Theme, ThemeBg } from "@earendil-works/pi-coding-agent";
2
+ /**
3
+ * Paint one surface's rows. A surface owns its own background and nothing else: after any reset
4
+ * (`0`, `49`, or a compound sequence containing one) it re-asserts *its* background, so a row keeps
5
+ * the fill across the styled spans a theme or child renderer ends with `\x1b[0m`.
6
+ *
7
+ * It never guesses another surface's background. A nested surface that ended its own fill is gone;
8
+ * the rest of the row belongs to the surface painting it. Nested surfaces stay separate because each
9
+ * one paints the rows it draws, not because this function tracks ANSI state.
10
+ */
11
+ export declare function backgroundFill(theme: Theme, token: ThemeBg): (text: string) => string;
@@ -0,0 +1,31 @@
1
+ const MARK = "\u0000";
2
+ /** Any SGR sequence: parameters are `;`-separated, an empty parameter list means reset. */
3
+ const SGR = /\x1b\[([0-9;]*)m/g;
4
+ /**
5
+ * Paint one surface's rows. A surface owns its own background and nothing else: after any reset
6
+ * (`0`, `49`, or a compound sequence containing one) it re-asserts *its* background, so a row keeps
7
+ * the fill across the styled spans a theme or child renderer ends with `\x1b[0m`.
8
+ *
9
+ * It never guesses another surface's background. A nested surface that ended its own fill is gone;
10
+ * the rest of the row belongs to the surface painting it. Nested surfaces stay separate because each
11
+ * one paints the rows it draws, not because this function tracks ANSI state.
12
+ */
13
+ export function backgroundFill(theme, token) {
14
+ const [start, end] = theme.bg(token, MARK).split(MARK);
15
+ if (start === undefined || end === undefined)
16
+ return (text) => text;
17
+ return (text) => {
18
+ let out = start;
19
+ let from = 0;
20
+ for (const match of text.matchAll(SGR)) {
21
+ const params = match[1] ?? "";
22
+ const resets = params === "" || params.split(";").some((code) => code === "0" || code === "49");
23
+ const next = (match.index ?? 0) + match[0].length;
24
+ out += text.slice(from, next);
25
+ if (resets)
26
+ out += start;
27
+ from = next;
28
+ }
29
+ return `${out}${text.slice(from)}${end}`;
30
+ };
31
+ }
package/dist/extension.js CHANGED
@@ -2,12 +2,15 @@ import { getRuntimeSettingsRegistry, messageViewPoint, registerExtensionHook, re
2
2
  import { installHostToolGroups } from "./host-transcript-patch.js";
3
3
  import { createMessageView } from "./message-view.js";
4
4
  import { createOptionalToolRendererFactory, registerCollapsibleToolRenderer } from "./renderer.js";
5
- import { activityGroupsEnabled, createToolGroupSettings } from "./settings.js";
6
- import { clearCapturedNestedCalls } from "./tracker.js";
5
+ import { ToolUiRuntime } from "./runtime.js";
6
+ import { activityGroupsEnabled, createToolGroupSettings, toolBackgroundsEnabled, } from "./settings.js";
7
+ import { TOOL_SPINNER_INTERVAL_MS } from "./tool-view.js";
7
8
  export default function (pi) {
9
+ // One runtime per extension instance: two Pi runtimes in one process never share these facts.
10
+ const runtime = new ToolUiRuntime();
8
11
  let redrawGroups;
9
12
  pi.on("agent_settled", () => redrawGroups?.());
10
- registerCollapsibleToolRenderer(pi);
13
+ registerCollapsibleToolRenderer(pi, runtime);
11
14
  registerExtensionLifecycle(pi, {
12
15
  key: "@hheei/pi-ext-ui",
13
16
  async start(context) {
@@ -31,7 +34,60 @@ export default function (pi) {
31
34
  context.extension.ui.notify(error instanceof Error ? error.message : String(error), "warning");
32
35
  }
33
36
  };
34
- const provider = createToolGroupSettings({ onEnabled: apply });
37
+ const applyBackgrounds = (enabled) => {
38
+ runtime.backgrounds = enabled;
39
+ redrawGroups?.();
40
+ };
41
+ const provider = createToolGroupSettings({
42
+ onEnabled: apply,
43
+ onBackgrounds: applyBackgrounds,
44
+ });
45
+ const onAgentStart = pi.on("agent_start", () => {
46
+ patch?.beginRun();
47
+ redrawGroups?.();
48
+ });
49
+ // A live tool animates its status glyph. The ticker exists only while one is running, and the
50
+ // frame itself comes from one shared clock, so a phase never restarts on a status change.
51
+ let liveTools = 0;
52
+ let ticker;
53
+ const stopTicker = () => {
54
+ if (ticker === undefined)
55
+ return;
56
+ clearInterval(ticker);
57
+ ticker = undefined;
58
+ };
59
+ const onAgentEnd = pi.on("agent_end", () => {
60
+ patch?.endRun();
61
+ // No tool outlives the agent loop, so the animation cannot keep a ticker alive either.
62
+ liveTools = 0;
63
+ stopTicker();
64
+ redrawGroups?.();
65
+ });
66
+ const onToolStart = pi.on("tool_execution_start", () => {
67
+ liveTools += 1;
68
+ if (ticker !== undefined)
69
+ return;
70
+ ticker = setInterval(() => {
71
+ if (liveTools === 0 || redrawGroups === undefined) {
72
+ stopTicker();
73
+ return;
74
+ }
75
+ redrawGroups();
76
+ }, TOOL_SPINNER_INTERVAL_MS);
77
+ });
78
+ const onToolEnd = pi.on("tool_execution_end", () => {
79
+ liveTools = Math.max(0, liveTools - 1);
80
+ if (liveTools === 0)
81
+ stopTicker();
82
+ });
83
+ context.resources.add("run-clock", () => {
84
+ onAgentStart();
85
+ onAgentEnd();
86
+ onToolStart();
87
+ onToolEnd();
88
+ stopTicker();
89
+ liveTools = 0;
90
+ });
35
91
  context.resources.add("tool-group-settings", registerSettings(provider, getRuntimeSettingsRegistry(pi)));
36
92
  context.resources.add("tool-groups", () => {
37
93
  redrawGroups = undefined;
@@ -39,9 +95,14 @@ export default function (pi) {
39
95
  patch = undefined;
40
96
  });
41
97
  context.resources.add("tracker-cleanup", () => {
42
- clearCapturedNestedCalls();
98
+ runtime.tracker.clear();
43
99
  });
44
- const toolRenderers = registerExtensionHook(pi, toolRendererPoint, createOptionalToolRendererFactory(pi));
100
+ // Resume reads the host's own nested-call record for the current branch, never another one.
101
+ runtime.nestedCalls.setSource(context.extension.sessionManager);
102
+ context.resources.add("nested-call-source", () => {
103
+ runtime.nestedCalls.setSource(undefined);
104
+ });
105
+ const toolRenderers = registerExtensionHook(pi, toolRendererPoint, createOptionalToolRendererFactory(pi, runtime));
45
106
  context.resources.add("tool-renderer", () => toolRenderers.dispose());
46
107
  const handle = registerExtensionHook(context.pi, messageViewPoint, createMessageView);
47
108
  context.resources.add("message-view", () => handle.dispose());
@@ -50,14 +111,17 @@ export default function (pi) {
50
111
  sessionId: context.extension.sessionManager.getSessionId(),
51
112
  signal: context.signal,
52
113
  });
53
- if (!context.signal.aborted)
114
+ if (!context.signal.aborted) {
54
115
  apply(activityGroupsEnabled(state));
116
+ applyBackgrounds(toolBackgroundsEnabled(state));
117
+ }
55
118
  }
56
119
  catch (error) {
57
120
  if (!context.signal.aborted) {
58
121
  const detail = error instanceof Error ? error.message : String(error);
59
122
  context.extension.ui.notify(`Tool activity groups settings unreadable: ${detail}`, "warning");
60
123
  apply(true);
124
+ applyBackgrounds(false);
61
125
  }
62
126
  }
63
127
  },
@@ -3,4 +3,6 @@ import { type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  export declare function installHostToolGroups(context: ExtensionContext): {
4
4
  dispose(): void;
5
5
  requestRender(): void;
6
+ beginRun(): void;
7
+ endRun(): void;
6
8
  };