@bastani/atomic 0.8.1 → 0.8.2-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/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/config.ts +3 -4
- package/dist/builtin/intercom/index.ts +6 -6
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/agent-dir.ts +11 -2
- package/dist/builtin/mcp/cli.js +12 -6
- package/dist/builtin/mcp/config.ts +31 -22
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/agents/agents.ts +63 -23
- package/dist/builtin/subagents/src/agents/skills.ts +21 -21
- package/dist/builtin/subagents/src/extension/index.ts +9 -8
- package/dist/builtin/subagents/src/runs/shared/run-history.ts +13 -10
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +3 -3
- package/dist/builtin/subagents/src/shared/artifacts.ts +18 -17
- package/dist/builtin/subagents/src/shared/types.ts +4 -4
- package/dist/builtin/web-access/config-paths.ts +11 -0
- package/dist/builtin/web-access/exa.ts +3 -2
- package/dist/builtin/web-access/gemini-api.ts +2 -1
- package/dist/builtin/web-access/gemini-search.ts +2 -1
- package/dist/builtin/web-access/gemini-web-config.ts +2 -1
- package/dist/builtin/web-access/github-extract.ts +2 -1
- package/dist/builtin/web-access/index.ts +11 -8
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/web-access/perplexity.ts +2 -1
- package/dist/builtin/web-access/video-extract.ts +2 -1
- package/dist/builtin/web-access/youtube-extract.ts +2 -1
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +4 -0
- package/dist/builtin/workflows/builtin/open-claude-design.ts +39 -22
- package/dist/builtin/workflows/builtin/ralph.ts +7 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/workflow/SKILL.md +28 -20
- package/dist/builtin/workflows/skills/workflow/references/design-checklist.md +8 -4
- package/dist/builtin/workflows/skills/workflow/references/running-workflows.md +52 -23
- package/dist/builtin/workflows/skills/workflow/references/sdk-authoring.md +41 -12
- package/dist/builtin/workflows/src/extension/config-loader.ts +13 -14
- package/dist/builtin/workflows/src/extension/discovery.ts +4 -6
- package/dist/builtin/workflows/src/extension/index.ts +675 -524
- package/dist/builtin/workflows/src/extension/runtime.ts +40 -16
- package/dist/builtin/workflows/src/extension/wiring.ts +3 -0
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +43 -33
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +34 -10
- package/dist/builtin/workflows/src/shared/types.ts +1 -5
- package/dist/builtin/workflows/src/tui/graph-view.ts +245 -75
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +23 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +259 -149
- package/dist/builtin/workflows/src/tui/status-helpers.ts +3 -3
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +99 -10
- package/dist/builtin/workflows/src/tui/switcher.ts +4 -5
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +29 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +11 -8
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +59 -4
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +3 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +31 -8
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +9 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +11 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts +3 -2
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +25 -8
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-manager.d.ts +3 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +97 -58
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +37 -36
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +5 -4
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +2 -2
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +7 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +29 -8
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/telemetry.d.ts.map +1 -1
- package/dist/core/telemetry.js +2 -2
- package/dist/core/telemetry.js.map +1 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +2 -2
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +8 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/todos.d.ts.map +1 -1
- package/dist/core/tools/todos.js +3 -3
- package/dist/core/tools/todos.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -6
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/atomic-banner.d.ts +4 -0
- package/dist/modes/interactive/components/atomic-banner.d.ts.map +1 -0
- package/dist/modes/interactive/components/atomic-banner.js +34 -0
- package/dist/modes/interactive/components/atomic-banner.js.map +1 -0
- package/dist/modes/interactive/components/chat-message-renderer.d.ts +99 -0
- package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -0
- package/dist/modes/interactive/components/chat-message-renderer.js +450 -0
- package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -0
- package/dist/modes/interactive/components/chat-transcript.d.ts +69 -0
- package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -0
- package/dist/modes/interactive/components/chat-transcript.js +183 -0
- package/dist/modes/interactive/components/chat-transcript.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts +16 -4
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +110 -137
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +2 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +2 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +9 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +192 -137
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/catppuccin-mocha.json +5 -5
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +11 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +2 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-check.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Component, Container } from "@earendil-works/pi-tui";
|
|
2
|
+
/**
|
|
3
|
+
* Roles that participate in pi's chat spacing contract.
|
|
4
|
+
*
|
|
5
|
+
* Assistant turns own their leading whitespace internally, and tool rows attach
|
|
6
|
+
* directly under the assistant/tool-call row they belong to. User-like rows get
|
|
7
|
+
* one blank line when they are not the first row in the transcript.
|
|
8
|
+
*/
|
|
9
|
+
export type ChatTranscriptRole = "assistant" | "thinking" | "tool" | "user" | "custom" | "notice" | "system" | "summary";
|
|
10
|
+
export interface ChatTranscriptEntryLike {
|
|
11
|
+
readonly role: ChatTranscriptRole;
|
|
12
|
+
}
|
|
13
|
+
export type ChatTranscriptRenderer<TEntry extends ChatTranscriptEntryLike> = (entry: TEntry) => Component;
|
|
14
|
+
export declare function addChatTranscriptEntry(container: Container, component: Component, role: ChatTranscriptRole): void;
|
|
15
|
+
/**
|
|
16
|
+
* Reusable pi chat transcript scaffold for extension surfaces.
|
|
17
|
+
*
|
|
18
|
+
* This intentionally mirrors InteractiveMode.addMessageToChat spacing without
|
|
19
|
+
* coupling consumers to a full AgentSession. Extension UIs can bring their own
|
|
20
|
+
* message model while still rendering inside the same Container/Spacer rhythm
|
|
21
|
+
* as the main chat.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ChatTranscriptComponent<TEntry extends ChatTranscriptEntryLike> implements Component {
|
|
24
|
+
private readonly entries;
|
|
25
|
+
private readonly renderEntry;
|
|
26
|
+
constructor(entries: readonly TEntry[], renderEntry: ChatTranscriptRenderer<TEntry>);
|
|
27
|
+
render(width: number): string[];
|
|
28
|
+
invalidate(): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Sticky-bottom, scrollable viewport for chat-like component stacks.
|
|
32
|
+
*
|
|
33
|
+
* Pi's main interactive chat gets terminal scrollback for free. Extension
|
|
34
|
+
* overlays render into a fixed rectangle, so they need an explicit viewport
|
|
35
|
+
* with the same sticky-bottom default plus keyboard and mouse history controls.
|
|
36
|
+
*/
|
|
37
|
+
export declare class ScrollableComponentViewport implements Component {
|
|
38
|
+
private components;
|
|
39
|
+
private visibleRows;
|
|
40
|
+
private scrollFromBottom;
|
|
41
|
+
private lastLineCount;
|
|
42
|
+
private lastWidth;
|
|
43
|
+
private maxScroll;
|
|
44
|
+
setComponents(components: readonly Component[]): void;
|
|
45
|
+
setVisibleRows(rows: number): void;
|
|
46
|
+
getScrollFromBottom(): number;
|
|
47
|
+
getMaxScroll(): number;
|
|
48
|
+
scrollToBottom(): void;
|
|
49
|
+
scrollToTop(): void;
|
|
50
|
+
scrollBy(deltaRows: number): void;
|
|
51
|
+
handleInput(data: string): boolean;
|
|
52
|
+
render(width: number): string[];
|
|
53
|
+
invalidate(): void;
|
|
54
|
+
private pageSize;
|
|
55
|
+
private clampScroll;
|
|
56
|
+
}
|
|
57
|
+
export declare class ScrollableChatTranscriptComponent<TEntry extends ChatTranscriptEntryLike> implements Component {
|
|
58
|
+
private readonly viewport;
|
|
59
|
+
private readonly transcript;
|
|
60
|
+
constructor(entries: readonly TEntry[], renderEntry: ChatTranscriptRenderer<TEntry>);
|
|
61
|
+
setVisibleRows(rows: number): void;
|
|
62
|
+
handleInput(data: string): boolean;
|
|
63
|
+
render(width: number): string[];
|
|
64
|
+
invalidate(): void;
|
|
65
|
+
getScrollFromBottom(): number;
|
|
66
|
+
getMaxScroll(): number;
|
|
67
|
+
scrollToBottom(): void;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=chat-transcript.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-transcript.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/chat-transcript.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EACd,SAAS,EAEV,MAAM,wBAAwB,CAAC;AAEhC;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAC1B,WAAW,GACX,UAAU,GACV,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,SAAS,CAAC;AAEd,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;CACnC;AAED,MAAM,MAAM,sBAAsB,CAAC,MAAM,SAAS,uBAAuB,IAAI,CAC3E,KAAK,EAAE,MAAM,KACV,SAAS,CAAC;AAEf,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,kBAAkB,GACvB,IAAI,CAKN;AAYD;;;;;;;GAOG;AACH,qBAAa,uBAAuB,CAAC,MAAM,SAAS,uBAAuB,CACzE,YAAW,SAAS;IAGlB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAF9B,YACmB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,WAAW,EAAE,sBAAsB,CAAC,MAAM,CAAC,EAC1D;IAEJ,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAM9B;IAED,UAAU,IAAI,IAAI,CAAG;CACtB;AAID;;;;;;GAMG;AACH,qBAAa,2BAA4B,YAAW,SAAS;IAC3D,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAK;IAEtB,aAAa,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,GAAG,IAAI,CAEpD;IAED,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAGjC;IAED,mBAAmB,IAAI,MAAM,CAE5B;IAED,YAAY,IAAI,MAAM,CAErB;IAED,cAAc,IAAI,IAAI,CAErB;IAED,WAAW,IAAI,IAAI,CAElB;IAED,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAMhC;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAwBjC;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAe9B;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,WAAW;CAGpB;AAED,qBAAa,iCAAiC,CAAC,MAAM,SAAS,uBAAuB,CACnF,YAAW,SAAS;IAEpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAE7D,YACE,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,WAAW,EAAE,sBAAsB,CAAC,MAAM,CAAC,EAI5C;IAED,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEjC;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjC;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9B;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,mBAAmB,IAAI,MAAM,CAE5B;IAED,YAAY,IAAI,MAAM,CAErB;IAED,cAAc,IAAI,IAAI,CAErB;CACF","sourcesContent":["import {\n matchesKey,\n type Component,\n Container,\n Spacer,\n} from \"@earendil-works/pi-tui\";\n\n/**\n * Roles that participate in pi's chat spacing contract.\n *\n * Assistant turns own their leading whitespace internally, and tool rows attach\n * directly under the assistant/tool-call row they belong to. User-like rows get\n * one blank line when they are not the first row in the transcript.\n */\nexport type ChatTranscriptRole =\n | \"assistant\"\n | \"thinking\"\n | \"tool\"\n | \"user\"\n | \"custom\"\n | \"notice\"\n | \"system\"\n | \"summary\";\n\nexport interface ChatTranscriptEntryLike {\n readonly role: ChatTranscriptRole;\n}\n\nexport type ChatTranscriptRenderer<TEntry extends ChatTranscriptEntryLike> = (\n entry: TEntry,\n) => Component;\n\nexport function addChatTranscriptEntry(\n container: Container,\n component: Component,\n role: ChatTranscriptRole,\n): void {\n if (needsLeadingSpacer(role) && container.children.length > 0) {\n container.addChild(new Spacer(1));\n }\n container.addChild(component);\n}\n\nfunction needsLeadingSpacer(role: ChatTranscriptRole): boolean {\n return (\n role === \"user\" ||\n role === \"custom\" ||\n role === \"notice\" ||\n role === \"system\" ||\n role === \"summary\"\n );\n}\n\n/**\n * Reusable pi chat transcript scaffold for extension surfaces.\n *\n * This intentionally mirrors InteractiveMode.addMessageToChat spacing without\n * coupling consumers to a full AgentSession. Extension UIs can bring their own\n * message model while still rendering inside the same Container/Spacer rhythm\n * as the main chat.\n */\nexport class ChatTranscriptComponent<TEntry extends ChatTranscriptEntryLike>\n implements Component\n{\n constructor(\n private readonly entries: readonly TEntry[],\n private readonly renderEntry: ChatTranscriptRenderer<TEntry>,\n ) {}\n\n render(width: number): string[] {\n const container = new Container();\n for (const entry of this.entries) {\n addChatTranscriptEntry(container, this.renderEntry(entry), entry.role);\n }\n return container.render(width);\n }\n\n invalidate(): void {}\n}\n\nconst DEFAULT_SCROLL_STEP_ROWS = 4;\n\n/**\n * Sticky-bottom, scrollable viewport for chat-like component stacks.\n *\n * Pi's main interactive chat gets terminal scrollback for free. Extension\n * overlays render into a fixed rectangle, so they need an explicit viewport\n * with the same sticky-bottom default plus keyboard and mouse history controls.\n */\nexport class ScrollableComponentViewport implements Component {\n private components: readonly Component[] = [];\n private visibleRows = 1;\n private scrollFromBottom = 0;\n private lastLineCount = 0;\n private lastWidth = 0;\n private maxScroll = 0;\n\n setComponents(components: readonly Component[]): void {\n this.components = components;\n }\n\n setVisibleRows(rows: number): void {\n this.visibleRows = Math.max(1, Math.floor(rows));\n this.clampScroll();\n }\n\n getScrollFromBottom(): number {\n return this.scrollFromBottom;\n }\n\n getMaxScroll(): number {\n return this.maxScroll;\n }\n\n scrollToBottom(): void {\n this.scrollFromBottom = 0;\n }\n\n scrollToTop(): void {\n this.scrollFromBottom = this.maxScroll;\n }\n\n scrollBy(deltaRows: number): void {\n // Positive deltas move toward newer content; negative deltas move up\n // into older history. Store the offset from the sticky bottom so new\n // streaming output can keep following when the offset is zero.\n this.scrollFromBottom -= deltaRows;\n this.clampScroll();\n }\n\n handleInput(data: string): boolean {\n const wheelDeltaRows = mouseWheelDeltaRows(data);\n if (wheelDeltaRows !== 0) {\n this.scrollBy(wheelDeltaRows);\n return true;\n }\n if (isMouseSequence(data)) return true;\n if (matchesKey(data, \"pageUp\")) {\n this.scrollBy(-this.pageSize());\n return true;\n }\n if (matchesKey(data, \"pageDown\")) {\n this.scrollBy(this.pageSize());\n return true;\n }\n if (matchesKey(data, \"home\")) {\n this.scrollToTop();\n return true;\n }\n if (matchesKey(data, \"end\")) {\n this.scrollToBottom();\n return true;\n }\n return false;\n }\n\n render(width: number): string[] {\n const allLines = this.components.flatMap((component) => component.render(width));\n const maxScroll = Math.max(0, allLines.length - this.visibleRows);\n if (this.scrollFromBottom > 0 && this.lastWidth === width && allLines.length > this.lastLineCount) {\n this.scrollFromBottom += allLines.length - this.lastLineCount;\n }\n this.lastLineCount = allLines.length;\n this.lastWidth = width;\n this.maxScroll = maxScroll;\n this.clampScroll();\n\n const start = Math.max(0, maxScroll - this.scrollFromBottom);\n const visible = allLines.slice(start, start + this.visibleRows);\n while (visible.length < this.visibleRows) visible.push(\" \".repeat(width));\n return visible;\n }\n\n invalidate(): void {\n for (const component of this.components) component.invalidate();\n }\n\n private pageSize(): number {\n return Math.max(4, this.visibleRows - 2);\n }\n\n private clampScroll(): void {\n this.scrollFromBottom = Math.max(0, Math.min(this.maxScroll, this.scrollFromBottom));\n }\n}\n\nexport class ScrollableChatTranscriptComponent<TEntry extends ChatTranscriptEntryLike>\n implements Component\n{\n private readonly viewport = new ScrollableComponentViewport();\n private readonly transcript: ChatTranscriptComponent<TEntry>;\n\n constructor(\n entries: readonly TEntry[],\n renderEntry: ChatTranscriptRenderer<TEntry>,\n ) {\n this.transcript = new ChatTranscriptComponent(entries, renderEntry);\n this.viewport.setComponents([this.transcript]);\n }\n\n setVisibleRows(rows: number): void {\n this.viewport.setVisibleRows(rows);\n }\n\n handleInput(data: string): boolean {\n return this.viewport.handleInput(data);\n }\n\n render(width: number): string[] {\n return this.viewport.render(width);\n }\n\n invalidate(): void {\n this.viewport.invalidate();\n }\n\n getScrollFromBottom(): number {\n return this.viewport.getScrollFromBottom();\n }\n\n getMaxScroll(): number {\n return this.viewport.getMaxScroll();\n }\n\n scrollToBottom(): void {\n this.viewport.scrollToBottom();\n }\n}\n\nfunction mouseWheelDeltaRows(data: string): number {\n const sgr = data.match(/^\\x1b\\[<(\\d+);\\d+;\\d+M$/);\n if (sgr) return wheelDeltaForButtonCode(Number.parseInt(sgr[1]!, 10));\n if (data.startsWith(\"\\x1b[M\") && data.length >= 6) {\n return wheelDeltaForButtonCode(data.charCodeAt(3) - 32);\n }\n return 0;\n}\n\nfunction wheelDeltaForButtonCode(code: number): number {\n if ((code & 64) === 0) return 0;\n const direction = code & 3;\n if (direction === 0) return -DEFAULT_SCROLL_STEP_ROWS;\n if (direction === 1) return DEFAULT_SCROLL_STEP_ROWS;\n return 0;\n}\n\nfunction isMouseSequence(data: string): boolean {\n return /^\\x1b\\[<\\d+;\\d+;\\d+[mM]$/.test(data) || data.startsWith(\"\\x1b[M\");\n}\n"]}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { matchesKey, Container, Spacer, } from "@earendil-works/pi-tui";
|
|
2
|
+
export function addChatTranscriptEntry(container, component, role) {
|
|
3
|
+
if (needsLeadingSpacer(role) && container.children.length > 0) {
|
|
4
|
+
container.addChild(new Spacer(1));
|
|
5
|
+
}
|
|
6
|
+
container.addChild(component);
|
|
7
|
+
}
|
|
8
|
+
function needsLeadingSpacer(role) {
|
|
9
|
+
return (role === "user" ||
|
|
10
|
+
role === "custom" ||
|
|
11
|
+
role === "notice" ||
|
|
12
|
+
role === "system" ||
|
|
13
|
+
role === "summary");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reusable pi chat transcript scaffold for extension surfaces.
|
|
17
|
+
*
|
|
18
|
+
* This intentionally mirrors InteractiveMode.addMessageToChat spacing without
|
|
19
|
+
* coupling consumers to a full AgentSession. Extension UIs can bring their own
|
|
20
|
+
* message model while still rendering inside the same Container/Spacer rhythm
|
|
21
|
+
* as the main chat.
|
|
22
|
+
*/
|
|
23
|
+
export class ChatTranscriptComponent {
|
|
24
|
+
constructor(entries, renderEntry) {
|
|
25
|
+
this.entries = entries;
|
|
26
|
+
this.renderEntry = renderEntry;
|
|
27
|
+
}
|
|
28
|
+
render(width) {
|
|
29
|
+
const container = new Container();
|
|
30
|
+
for (const entry of this.entries) {
|
|
31
|
+
addChatTranscriptEntry(container, this.renderEntry(entry), entry.role);
|
|
32
|
+
}
|
|
33
|
+
return container.render(width);
|
|
34
|
+
}
|
|
35
|
+
invalidate() { }
|
|
36
|
+
}
|
|
37
|
+
const DEFAULT_SCROLL_STEP_ROWS = 4;
|
|
38
|
+
/**
|
|
39
|
+
* Sticky-bottom, scrollable viewport for chat-like component stacks.
|
|
40
|
+
*
|
|
41
|
+
* Pi's main interactive chat gets terminal scrollback for free. Extension
|
|
42
|
+
* overlays render into a fixed rectangle, so they need an explicit viewport
|
|
43
|
+
* with the same sticky-bottom default plus keyboard and mouse history controls.
|
|
44
|
+
*/
|
|
45
|
+
export class ScrollableComponentViewport {
|
|
46
|
+
constructor() {
|
|
47
|
+
this.components = [];
|
|
48
|
+
this.visibleRows = 1;
|
|
49
|
+
this.scrollFromBottom = 0;
|
|
50
|
+
this.lastLineCount = 0;
|
|
51
|
+
this.lastWidth = 0;
|
|
52
|
+
this.maxScroll = 0;
|
|
53
|
+
}
|
|
54
|
+
setComponents(components) {
|
|
55
|
+
this.components = components;
|
|
56
|
+
}
|
|
57
|
+
setVisibleRows(rows) {
|
|
58
|
+
this.visibleRows = Math.max(1, Math.floor(rows));
|
|
59
|
+
this.clampScroll();
|
|
60
|
+
}
|
|
61
|
+
getScrollFromBottom() {
|
|
62
|
+
return this.scrollFromBottom;
|
|
63
|
+
}
|
|
64
|
+
getMaxScroll() {
|
|
65
|
+
return this.maxScroll;
|
|
66
|
+
}
|
|
67
|
+
scrollToBottom() {
|
|
68
|
+
this.scrollFromBottom = 0;
|
|
69
|
+
}
|
|
70
|
+
scrollToTop() {
|
|
71
|
+
this.scrollFromBottom = this.maxScroll;
|
|
72
|
+
}
|
|
73
|
+
scrollBy(deltaRows) {
|
|
74
|
+
// Positive deltas move toward newer content; negative deltas move up
|
|
75
|
+
// into older history. Store the offset from the sticky bottom so new
|
|
76
|
+
// streaming output can keep following when the offset is zero.
|
|
77
|
+
this.scrollFromBottom -= deltaRows;
|
|
78
|
+
this.clampScroll();
|
|
79
|
+
}
|
|
80
|
+
handleInput(data) {
|
|
81
|
+
const wheelDeltaRows = mouseWheelDeltaRows(data);
|
|
82
|
+
if (wheelDeltaRows !== 0) {
|
|
83
|
+
this.scrollBy(wheelDeltaRows);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (isMouseSequence(data))
|
|
87
|
+
return true;
|
|
88
|
+
if (matchesKey(data, "pageUp")) {
|
|
89
|
+
this.scrollBy(-this.pageSize());
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (matchesKey(data, "pageDown")) {
|
|
93
|
+
this.scrollBy(this.pageSize());
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (matchesKey(data, "home")) {
|
|
97
|
+
this.scrollToTop();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (matchesKey(data, "end")) {
|
|
101
|
+
this.scrollToBottom();
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
render(width) {
|
|
107
|
+
const allLines = this.components.flatMap((component) => component.render(width));
|
|
108
|
+
const maxScroll = Math.max(0, allLines.length - this.visibleRows);
|
|
109
|
+
if (this.scrollFromBottom > 0 && this.lastWidth === width && allLines.length > this.lastLineCount) {
|
|
110
|
+
this.scrollFromBottom += allLines.length - this.lastLineCount;
|
|
111
|
+
}
|
|
112
|
+
this.lastLineCount = allLines.length;
|
|
113
|
+
this.lastWidth = width;
|
|
114
|
+
this.maxScroll = maxScroll;
|
|
115
|
+
this.clampScroll();
|
|
116
|
+
const start = Math.max(0, maxScroll - this.scrollFromBottom);
|
|
117
|
+
const visible = allLines.slice(start, start + this.visibleRows);
|
|
118
|
+
while (visible.length < this.visibleRows)
|
|
119
|
+
visible.push(" ".repeat(width));
|
|
120
|
+
return visible;
|
|
121
|
+
}
|
|
122
|
+
invalidate() {
|
|
123
|
+
for (const component of this.components)
|
|
124
|
+
component.invalidate();
|
|
125
|
+
}
|
|
126
|
+
pageSize() {
|
|
127
|
+
return Math.max(4, this.visibleRows - 2);
|
|
128
|
+
}
|
|
129
|
+
clampScroll() {
|
|
130
|
+
this.scrollFromBottom = Math.max(0, Math.min(this.maxScroll, this.scrollFromBottom));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export class ScrollableChatTranscriptComponent {
|
|
134
|
+
constructor(entries, renderEntry) {
|
|
135
|
+
this.viewport = new ScrollableComponentViewport();
|
|
136
|
+
this.transcript = new ChatTranscriptComponent(entries, renderEntry);
|
|
137
|
+
this.viewport.setComponents([this.transcript]);
|
|
138
|
+
}
|
|
139
|
+
setVisibleRows(rows) {
|
|
140
|
+
this.viewport.setVisibleRows(rows);
|
|
141
|
+
}
|
|
142
|
+
handleInput(data) {
|
|
143
|
+
return this.viewport.handleInput(data);
|
|
144
|
+
}
|
|
145
|
+
render(width) {
|
|
146
|
+
return this.viewport.render(width);
|
|
147
|
+
}
|
|
148
|
+
invalidate() {
|
|
149
|
+
this.viewport.invalidate();
|
|
150
|
+
}
|
|
151
|
+
getScrollFromBottom() {
|
|
152
|
+
return this.viewport.getScrollFromBottom();
|
|
153
|
+
}
|
|
154
|
+
getMaxScroll() {
|
|
155
|
+
return this.viewport.getMaxScroll();
|
|
156
|
+
}
|
|
157
|
+
scrollToBottom() {
|
|
158
|
+
this.viewport.scrollToBottom();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function mouseWheelDeltaRows(data) {
|
|
162
|
+
const sgr = data.match(/^\x1b\[<(\d+);\d+;\d+M$/);
|
|
163
|
+
if (sgr)
|
|
164
|
+
return wheelDeltaForButtonCode(Number.parseInt(sgr[1], 10));
|
|
165
|
+
if (data.startsWith("\x1b[M") && data.length >= 6) {
|
|
166
|
+
return wheelDeltaForButtonCode(data.charCodeAt(3) - 32);
|
|
167
|
+
}
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
function wheelDeltaForButtonCode(code) {
|
|
171
|
+
if ((code & 64) === 0)
|
|
172
|
+
return 0;
|
|
173
|
+
const direction = code & 3;
|
|
174
|
+
if (direction === 0)
|
|
175
|
+
return -DEFAULT_SCROLL_STEP_ROWS;
|
|
176
|
+
if (direction === 1)
|
|
177
|
+
return DEFAULT_SCROLL_STEP_ROWS;
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
function isMouseSequence(data) {
|
|
181
|
+
return /^\x1b\[<\d+;\d+;\d+[mM]$/.test(data) || data.startsWith("\x1b[M");
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=chat-transcript.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-transcript.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/chat-transcript.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n matchesKey,\n type Component,\n Container,\n Spacer,\n} from \"@earendil-works/pi-tui\";\n\n/**\n * Roles that participate in pi's chat spacing contract.\n *\n * Assistant turns own their leading whitespace internally, and tool rows attach\n * directly under the assistant/tool-call row they belong to. User-like rows get\n * one blank line when they are not the first row in the transcript.\n */\nexport type ChatTranscriptRole =\n | \"assistant\"\n | \"thinking\"\n | \"tool\"\n | \"user\"\n | \"custom\"\n | \"notice\"\n | \"system\"\n | \"summary\";\n\nexport interface ChatTranscriptEntryLike {\n readonly role: ChatTranscriptRole;\n}\n\nexport type ChatTranscriptRenderer<TEntry extends ChatTranscriptEntryLike> = (\n entry: TEntry,\n) => Component;\n\nexport function addChatTranscriptEntry(\n container: Container,\n component: Component,\n role: ChatTranscriptRole,\n): void {\n if (needsLeadingSpacer(role) && container.children.length > 0) {\n container.addChild(new Spacer(1));\n }\n container.addChild(component);\n}\n\nfunction needsLeadingSpacer(role: ChatTranscriptRole): boolean {\n return (\n role === \"user\" ||\n role === \"custom\" ||\n role === \"notice\" ||\n role === \"system\" ||\n role === \"summary\"\n );\n}\n\n/**\n * Reusable pi chat transcript scaffold for extension surfaces.\n *\n * This intentionally mirrors InteractiveMode.addMessageToChat spacing without\n * coupling consumers to a full AgentSession. Extension UIs can bring their own\n * message model while still rendering inside the same Container/Spacer rhythm\n * as the main chat.\n */\nexport class ChatTranscriptComponent<TEntry extends ChatTranscriptEntryLike>\n implements Component\n{\n constructor(\n private readonly entries: readonly TEntry[],\n private readonly renderEntry: ChatTranscriptRenderer<TEntry>,\n ) {}\n\n render(width: number): string[] {\n const container = new Container();\n for (const entry of this.entries) {\n addChatTranscriptEntry(container, this.renderEntry(entry), entry.role);\n }\n return container.render(width);\n }\n\n invalidate(): void {}\n}\n\nconst DEFAULT_SCROLL_STEP_ROWS = 4;\n\n/**\n * Sticky-bottom, scrollable viewport for chat-like component stacks.\n *\n * Pi's main interactive chat gets terminal scrollback for free. Extension\n * overlays render into a fixed rectangle, so they need an explicit viewport\n * with the same sticky-bottom default plus keyboard and mouse history controls.\n */\nexport class ScrollableComponentViewport implements Component {\n private components: readonly Component[] = [];\n private visibleRows = 1;\n private scrollFromBottom = 0;\n private lastLineCount = 0;\n private lastWidth = 0;\n private maxScroll = 0;\n\n setComponents(components: readonly Component[]): void {\n this.components = components;\n }\n\n setVisibleRows(rows: number): void {\n this.visibleRows = Math.max(1, Math.floor(rows));\n this.clampScroll();\n }\n\n getScrollFromBottom(): number {\n return this.scrollFromBottom;\n }\n\n getMaxScroll(): number {\n return this.maxScroll;\n }\n\n scrollToBottom(): void {\n this.scrollFromBottom = 0;\n }\n\n scrollToTop(): void {\n this.scrollFromBottom = this.maxScroll;\n }\n\n scrollBy(deltaRows: number): void {\n // Positive deltas move toward newer content; negative deltas move up\n // into older history. Store the offset from the sticky bottom so new\n // streaming output can keep following when the offset is zero.\n this.scrollFromBottom -= deltaRows;\n this.clampScroll();\n }\n\n handleInput(data: string): boolean {\n const wheelDeltaRows = mouseWheelDeltaRows(data);\n if (wheelDeltaRows !== 0) {\n this.scrollBy(wheelDeltaRows);\n return true;\n }\n if (isMouseSequence(data)) return true;\n if (matchesKey(data, \"pageUp\")) {\n this.scrollBy(-this.pageSize());\n return true;\n }\n if (matchesKey(data, \"pageDown\")) {\n this.scrollBy(this.pageSize());\n return true;\n }\n if (matchesKey(data, \"home\")) {\n this.scrollToTop();\n return true;\n }\n if (matchesKey(data, \"end\")) {\n this.scrollToBottom();\n return true;\n }\n return false;\n }\n\n render(width: number): string[] {\n const allLines = this.components.flatMap((component) => component.render(width));\n const maxScroll = Math.max(0, allLines.length - this.visibleRows);\n if (this.scrollFromBottom > 0 && this.lastWidth === width && allLines.length > this.lastLineCount) {\n this.scrollFromBottom += allLines.length - this.lastLineCount;\n }\n this.lastLineCount = allLines.length;\n this.lastWidth = width;\n this.maxScroll = maxScroll;\n this.clampScroll();\n\n const start = Math.max(0, maxScroll - this.scrollFromBottom);\n const visible = allLines.slice(start, start + this.visibleRows);\n while (visible.length < this.visibleRows) visible.push(\" \".repeat(width));\n return visible;\n }\n\n invalidate(): void {\n for (const component of this.components) component.invalidate();\n }\n\n private pageSize(): number {\n return Math.max(4, this.visibleRows - 2);\n }\n\n private clampScroll(): void {\n this.scrollFromBottom = Math.max(0, Math.min(this.maxScroll, this.scrollFromBottom));\n }\n}\n\nexport class ScrollableChatTranscriptComponent<TEntry extends ChatTranscriptEntryLike>\n implements Component\n{\n private readonly viewport = new ScrollableComponentViewport();\n private readonly transcript: ChatTranscriptComponent<TEntry>;\n\n constructor(\n entries: readonly TEntry[],\n renderEntry: ChatTranscriptRenderer<TEntry>,\n ) {\n this.transcript = new ChatTranscriptComponent(entries, renderEntry);\n this.viewport.setComponents([this.transcript]);\n }\n\n setVisibleRows(rows: number): void {\n this.viewport.setVisibleRows(rows);\n }\n\n handleInput(data: string): boolean {\n return this.viewport.handleInput(data);\n }\n\n render(width: number): string[] {\n return this.viewport.render(width);\n }\n\n invalidate(): void {\n this.viewport.invalidate();\n }\n\n getScrollFromBottom(): number {\n return this.viewport.getScrollFromBottom();\n }\n\n getMaxScroll(): number {\n return this.viewport.getMaxScroll();\n }\n\n scrollToBottom(): void {\n this.viewport.scrollToBottom();\n }\n}\n\nfunction mouseWheelDeltaRows(data: string): number {\n const sgr = data.match(/^\\x1b\\[<(\\d+);\\d+;\\d+M$/);\n if (sgr) return wheelDeltaForButtonCode(Number.parseInt(sgr[1]!, 10));\n if (data.startsWith(\"\\x1b[M\") && data.length >= 6) {\n return wheelDeltaForButtonCode(data.charCodeAt(3) - 32);\n }\n return 0;\n}\n\nfunction wheelDeltaForButtonCode(code: number): number {\n if ((code & 64) === 0) return 0;\n const direction = code & 3;\n if (direction === 0) return -DEFAULT_SCROLL_STEP_ROWS;\n if (direction === 1) return DEFAULT_SCROLL_STEP_ROWS;\n return 0;\n}\n\nfunction isMouseSequence(data: string): boolean {\n return /^\\x1b\\[<\\d+;\\d+;\\d+[mM]$/.test(data) || data.startsWith(\"\\x1b[M\");\n}\n"]}
|
|
@@ -2,16 +2,28 @@ import { type Component } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import type { AgentSession } from "../../../core/agent-session.js";
|
|
3
3
|
import type { ReadonlyFooterDataProvider } from "../../../core/footer-data-provider.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Right-aligned usage meter that sits above the composer, matching the approved
|
|
6
|
+
* prototype's separate token/cost/context ribbon.
|
|
7
|
+
*/
|
|
8
|
+
export declare class UsageMeterComponent implements Component {
|
|
9
|
+
private session;
|
|
10
|
+
private autoCompactEnabled;
|
|
11
|
+
constructor(session: AgentSession);
|
|
12
|
+
setSession(session: AgentSession): void;
|
|
13
|
+
setAutoCompactEnabled(enabled: boolean): void;
|
|
14
|
+
invalidate(): void;
|
|
15
|
+
render(width: number): string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sparse statusline below the composer. It mirrors the preview: model + cwd
|
|
19
|
+
* when idle, or one semantic dot with short recovery copy while work is live.
|
|
7
20
|
*/
|
|
8
21
|
export declare class FooterComponent implements Component {
|
|
9
22
|
private session;
|
|
10
23
|
private footerData;
|
|
11
|
-
private autoCompactEnabled;
|
|
12
24
|
constructor(session: AgentSession, footerData: ReadonlyFooterDataProvider);
|
|
13
25
|
setSession(session: AgentSession): void;
|
|
14
|
-
setAutoCompactEnabled(
|
|
26
|
+
setAutoCompactEnabled(_enabled: boolean): void;
|
|
15
27
|
/**
|
|
16
28
|
* No-op: git branch caching now handled by provider.
|
|
17
29
|
* Kept for compatibility with existing call sites in interactive-mode.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EAGf,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AA4HxF;;;GAGG;AACH,qBAAa,mBAAoB,YAAW,SAAS;IAGvC,OAAO,CAAC,OAAO;IAF3B,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YAAoB,OAAO,EAAE,YAAY,EAAI;IAE7C,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9B;CACF;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAE7C,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IAFpB,YACU,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EAC5C;IAEJ,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAE7C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAsB9B;CACF","sourcesContent":["import {\n type Component,\n truncateToWidth,\n visibleWidth,\n} from \"@earendil-works/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n // Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n return text\n .replace(/[\\r\\n\\t]/g, \" \")\n .replace(/ +/g, \" \")\n .trim();\n}\n\n/**\n * Format token counts (similar to web-ui)\n */\nfunction formatTokens(count: number): string {\n if (count < 1000) return count.toString();\n if (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n if (count < 1000000) return `${Math.round(count / 1000)}k`;\n if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n return `${Math.round(count / 1000000)}M`;\n}\n\nfunction replaceHome(input: string): string {\n const home = process.env.HOME || process.env.USERPROFILE;\n if (home && input.startsWith(home)) {\n return `~${input.slice(home.length)}`;\n }\n return input;\n}\n\nfunction rightAlign(line: string, width: number): string {\n const lineWidth = visibleWidth(line);\n if (lineWidth >= width) {\n return truncateToWidth(line, width, theme.fg(\"dim\", \"...\"));\n }\n return `${\" \".repeat(width - lineWidth)}${line}`;\n}\n\nfunction getUsageLine(\n session: AgentSession,\n autoCompactEnabled: boolean,\n width: number,\n): string {\n const state = session.state;\n\n // Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n let totalInput = 0;\n let totalOutput = 0;\n let totalCacheRead = 0;\n let totalCacheWrite = 0;\n let totalCost = 0;\n\n for (const entry of session.sessionManager.getEntries()) {\n if (entry.type === \"message\" && entry.message.role === \"assistant\") {\n totalInput += entry.message.usage.input;\n totalOutput += entry.message.usage.output;\n totalCacheRead += entry.message.usage.cacheRead;\n totalCacheWrite += entry.message.usage.cacheWrite;\n totalCost += entry.message.usage.cost.total;\n }\n }\n\n // Calculate context usage from session (handles compaction correctly).\n // After compaction, tokens are unknown until the next LLM response.\n const contextUsage = session.getContextUsage();\n const contextWindow =\n contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n const contextPercentValue = contextUsage?.percent ?? 0;\n const contextPercent =\n contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n const usageParts = [];\n if (totalInput)\n usageParts.push(\n `${theme.fg(\"dim\", \"↑\")}${theme.fg(\"muted\", formatTokens(totalInput))}`,\n );\n if (totalOutput)\n usageParts.push(\n `${theme.fg(\"dim\", \"↓\")}${theme.fg(\"muted\", formatTokens(totalOutput))}`,\n );\n if (totalCacheRead)\n usageParts.push(\n `${theme.fg(\"dim\", \"R\")}${theme.fg(\"muted\", formatTokens(totalCacheRead))}`,\n );\n if (totalCacheWrite)\n usageParts.push(\n `${theme.fg(\"dim\", \"W\")}${theme.fg(\"muted\", formatTokens(totalCacheWrite))}`,\n );\n\n // Show cost with \"(sub)\" indicator if using OAuth subscription\n const usingSubscription = state.model\n ? session.modelRegistry.isUsingOAuth(state.model)\n : false;\n if (totalCost || usingSubscription) {\n usageParts.push(\n `${theme.fg(\"muted\", `$${totalCost.toFixed(3)}`)}${usingSubscription ? ` ${theme.fg(\"dim\", \"(sub)\")}` : \"\"}`,\n );\n }\n\n const autoIndicator = autoCompactEnabled ? \" (auto)\" : \"\";\n const contextPercentDisplay =\n contextPercent === \"?\"\n ? `?/${formatTokens(contextWindow)}${autoIndicator}`\n : `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n if (contextPercentValue > 90) {\n usageParts.push(theme.fg(\"error\", contextPercentDisplay));\n } else if (contextPercentValue > 70) {\n usageParts.push(theme.fg(\"warning\", contextPercentDisplay));\n } else {\n usageParts.push(theme.fg(\"muted\", contextPercentDisplay));\n }\n\n const separator = theme.fg(\"dim\", \" • \");\n const usageText =\n usageParts.length > 0\n ? usageParts.join(separator)\n : theme.fg(\"muted\", contextPercentDisplay);\n return rightAlign(usageText, width);\n}\n\n/**\n * Right-aligned usage meter that sits above the composer, matching the approved\n * prototype's separate token/cost/context ribbon.\n */\nexport class UsageMeterComponent implements Component {\n private autoCompactEnabled = true;\n\n constructor(private session: AgentSession) {}\n\n setSession(session: AgentSession): void {\n this.session = session;\n }\n\n setAutoCompactEnabled(enabled: boolean): void {\n this.autoCompactEnabled = enabled;\n }\n\n invalidate(): void {\n // Render pulls live session data.\n }\n\n render(width: number): string[] {\n return [getUsageLine(this.session, this.autoCompactEnabled, width)];\n }\n}\n\n/**\n * Sparse statusline below the composer. It mirrors the preview: model + cwd\n * when idle, or one semantic dot with short recovery copy while work is live.\n */\nexport class FooterComponent implements Component {\n constructor(\n private session: AgentSession,\n private footerData: ReadonlyFooterDataProvider,\n ) {}\n\n setSession(session: AgentSession): void {\n this.session = session;\n }\n\n setAutoCompactEnabled(_enabled: boolean): void {\n // Usage state lives in UsageMeterComponent. Kept for compatibility with existing call sites.\n }\n\n /**\n * No-op: git branch caching now handled by provider.\n * Kept for compatibility with existing call sites in interactive-mode.\n */\n invalidate(): void {\n // No-op: git branch is cached/invalidated by provider\n }\n\n /**\n * Clean up resources.\n * Git watcher cleanup now handled by provider.\n */\n dispose(): void {\n // Git watcher cleanup handled by provider\n }\n\n render(width: number): string[] {\n const state = this.session.state;\n const pwd = replaceHome(this.session.sessionManager.getCwd());\n\n const modelName = state.model?.id || \"no-model\";\n let modelLabel = modelName;\n if (state.model?.reasoning) {\n const thinkingLevel = state.thinkingLevel || \"off\";\n modelLabel =\n thinkingLevel === \"off\" ? modelName : `${modelName} ${thinkingLevel}`;\n }\n if (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n modelLabel = `(${state.model.provider}) ${modelLabel}`;\n }\n\n const liveState = this.session.isStreaming\n ? theme.fg(\"muted\", \"Esc to interrupt\")\n : undefined;\n const statusText =\n liveState ??\n `${theme.fg(\"dim\", modelLabel)} ${theme.fg(\"dim\", \"•\")} ${theme.fg(\"muted\", pwd)}`;\n return [truncateToWidth(statusText, width, theme.fg(\"dim\", \"...\"))];\n }\n}\n"]}
|