@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.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 +87 -1
- package/README.md +1 -1
- package/dist/cli.js +5495 -5021
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +30 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +350 -50
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/live/controller.d.ts +40 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +48 -0
- package/dist/types/live/visualizer.d.ts +33 -0
- package/dist/types/lsp/client.d.ts +18 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/mcp/tool-bridge.d.ts +7 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/settings-defs.d.ts +8 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +314 -0
- package/dist/types/session/agent-session.d.ts +120 -750
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +49 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +22 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +143 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +14 -0
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +95 -1
- package/package.json +13 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +2 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/usage-cli.ts +11 -2
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +50 -2
- package/src/config/model-resolver.ts +0 -1
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +250 -54
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +89 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/audio-worklet.txt +59 -0
- package/src/live/browser-runtime.txt +221 -0
- package/src/live/controller.ts +464 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +490 -0
- package/src/live/visualizer.ts +234 -0
- package/src/lsp/client.ts +52 -9
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +44 -11
- package/src/main.ts +9 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/render.ts +94 -35
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/mcp/tool-bridge.ts +107 -11
- package/src/mcp/types.ts +7 -0
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +27 -10
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/settings-defs.ts +28 -3
- package/src/modes/components/settings-selector.ts +178 -6
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/components/tool-execution.ts +11 -2
- package/src/modes/controllers/command-controller.ts +70 -22
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/live-command-controller.ts +178 -0
- package/src/modes/controllers/mcp-command-controller.ts +80 -41
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +18 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +160 -38
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +3 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/task.md +12 -2
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +255 -139
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +334 -0
- package/src/session/agent-session.ts +1910 -12213
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +212 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +263 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +47 -5
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +10 -51
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +943 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +92 -11
- package/src/slash-commands/helpers/usage-report.ts +25 -4
- package/src/system-prompt.ts +41 -17
- package/src/task/executor.ts +288 -70
- package/src/task/index.ts +148 -111
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +4 -1
- package/src/task/types.ts +16 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +26 -0
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +12 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +7 -1
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +67 -3
- package/src/tts/streaming-player.ts +62 -4
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +8 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { beforeAll, describe, expect, it } from "bun:test";
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, it } from "bun:test";
|
|
2
2
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
4
|
-
import { Settings } from "../../config/settings";
|
|
4
|
+
import { Settings, settings } from "../../config/settings";
|
|
5
|
+
import { renderMCPResult } from "../../mcp/render";
|
|
6
|
+
import type { MCPToolDetails } from "../../mcp/tool-bridge";
|
|
5
7
|
import { getThemeByName, setThemeInstance, theme } from "../theme/theme";
|
|
6
8
|
import { ToolExecutionComponent, type ToolExecutionUi } from "./tool-execution";
|
|
7
9
|
|
|
@@ -26,6 +28,10 @@ describe("ToolExecutionComponent custom renderer failures", () => {
|
|
|
26
28
|
setThemeInstance(loaded);
|
|
27
29
|
});
|
|
28
30
|
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
settings.set("mcp.renderMarkdownResults", true);
|
|
33
|
+
});
|
|
34
|
+
|
|
29
35
|
it("falls back to the custom tool label when a renderCall child component throws during render", () => {
|
|
30
36
|
const tool: AgentTool = {
|
|
31
37
|
name: "graphify_graph",
|
|
@@ -99,3 +105,58 @@ describe("ToolExecutionComponent custom renderer failures", () => {
|
|
|
99
105
|
expect(text).toContain(rawResultText);
|
|
100
106
|
});
|
|
101
107
|
});
|
|
108
|
+
|
|
109
|
+
describe("MCP result Markdown rendering", () => {
|
|
110
|
+
const details: MCPToolDetails = {
|
|
111
|
+
serverName: "context-mode",
|
|
112
|
+
mcpToolName: "ctx_search",
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
beforeAll(async () => {
|
|
116
|
+
await Settings.init({ inMemory: true });
|
|
117
|
+
const loaded = await getThemeByName("dark");
|
|
118
|
+
if (!loaded) throw new Error("theme unavailable");
|
|
119
|
+
setThemeInstance(loaded);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
settings.set("mcp.renderMarkdownResults", true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("renders inline Markdown by default", () => {
|
|
127
|
+
const component = renderMCPResult(
|
|
128
|
+
{ content: [{ type: "text", text: "**bold result** and `code`" }], details },
|
|
129
|
+
{ expanded: true, isPartial: false },
|
|
130
|
+
theme,
|
|
131
|
+
);
|
|
132
|
+
const rendered = visibleText(component.render(80));
|
|
133
|
+
|
|
134
|
+
expect(rendered).toContain("bold result and code");
|
|
135
|
+
expect(rendered).not.toContain("**bold result**");
|
|
136
|
+
expect(rendered).not.toContain("`code`");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("keeps Markdown syntax literal when the setting is disabled", () => {
|
|
140
|
+
settings.set("mcp.renderMarkdownResults", false);
|
|
141
|
+
const component = renderMCPResult(
|
|
142
|
+
{ content: [{ type: "text", text: "**bold result**" }], details },
|
|
143
|
+
{ expanded: true, isPartial: false },
|
|
144
|
+
theme,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(visibleText(component.render(80))).toContain("**bold result**");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("preserves structured JSON rendering when Markdown is enabled", () => {
|
|
151
|
+
settings.set("mcp.renderMarkdownResults", true);
|
|
152
|
+
const component = renderMCPResult(
|
|
153
|
+
{ content: [{ type: "text", text: '{"status":"**ok**"}' }], details },
|
|
154
|
+
{ expanded: true, isPartial: false },
|
|
155
|
+
theme,
|
|
156
|
+
);
|
|
157
|
+
const rendered = visibleText(component.render(80));
|
|
158
|
+
|
|
159
|
+
expect(rendered).toContain("status");
|
|
160
|
+
expect(rendered).toContain("**ok**");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -782,9 +782,18 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
782
782
|
return this.isTranscriptBlockFinalized() ? undefined : 0;
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
-
/**
|
|
785
|
+
/**
|
|
786
|
+
* Keeps in-flight TV-wall frames out of immutable native scrollback: the
|
|
787
|
+
* `vibe_wait` wall and displaceable snapshots (`hub` waiting polls, `todo`
|
|
788
|
+
* lists). Their frames replace each other rather than append, and their
|
|
789
|
+
* rows mutate every spinner tick — an unpinned commit records a per-tick
|
|
790
|
+
* frozen snapshot AND force-seals the block (see TranscriptContainer's
|
|
791
|
+
* committed-snapshot seal), so the next poll stacks a new frame instead of
|
|
792
|
+
* displacing this one.
|
|
793
|
+
*/
|
|
786
794
|
isNativeScrollbackLiveRegionPinned(): boolean {
|
|
787
|
-
|
|
795
|
+
if (this.isTranscriptBlockFinalized()) return false;
|
|
796
|
+
return this.#toolName === "vibe_wait" || this.#displaceableByToolName !== undefined;
|
|
788
797
|
}
|
|
789
798
|
|
|
790
799
|
/**
|
|
@@ -15,6 +15,7 @@ import { formatDuration, Snowflake, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
|
15
15
|
import { shouldEnableAppendOnlyContext } from "../../config/append-only-context-mode";
|
|
16
16
|
import { type BashResult, isPersistentShellCdCommand } from "../../exec/bash-executor";
|
|
17
17
|
import { type LoadedCustomShare, loadCustomShare } from "../../export/custom-share";
|
|
18
|
+
import { parseExportArgs } from "../../export/html/args";
|
|
18
19
|
import { shareSession } from "../../export/share";
|
|
19
20
|
import type { CompactOptions } from "../../extensibility/extensions/types";
|
|
20
21
|
import {
|
|
@@ -76,16 +77,14 @@ export class CommandController {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
async handleExportCommand(text: string): Promise<void> {
|
|
79
|
-
const parts = text.split(/\s+/);
|
|
80
|
-
const arg = parts.length > 1 ? parts[1] : undefined;
|
|
81
|
-
|
|
82
|
-
if (arg === "--copy" || arg === "clipboard" || arg === "copy") {
|
|
83
|
-
this.ctx.showWarning("Use /dump to copy the session to clipboard.");
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
80
|
try {
|
|
88
|
-
const
|
|
81
|
+
const { outputPath, useUserThemes } = parseExportArgs(text.slice("/export".length));
|
|
82
|
+
if (outputPath === "--copy" || outputPath === "clipboard" || outputPath === "copy") {
|
|
83
|
+
this.ctx.showWarning("Use /dump to copy the session to clipboard.");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const filePath = await this.ctx.session.exportToHtml(outputPath, useUserThemes);
|
|
89
88
|
this.ctx.showStatus(`Session exported to: ${filePath}`);
|
|
90
89
|
this.openInBrowser(filePath);
|
|
91
90
|
} catch (error: unknown) {
|
|
@@ -550,8 +549,14 @@ export class CommandController {
|
|
|
550
549
|
this.ctx.session.sessionId,
|
|
551
550
|
)
|
|
552
551
|
: undefined;
|
|
553
|
-
const
|
|
554
|
-
|
|
552
|
+
const usageModelSelectors = this.ctx.session.getUsageReportingModelSelectors(usageReports);
|
|
553
|
+
const output = renderUsageReports(
|
|
554
|
+
usageReports,
|
|
555
|
+
theme,
|
|
556
|
+
Date.now(),
|
|
557
|
+
availableWidth,
|
|
558
|
+
provider => (provider === currentProvider ? activeAccount : undefined),
|
|
559
|
+
usageModelSelectors,
|
|
555
560
|
);
|
|
556
561
|
this.ctx.present([new Spacer(1), new Text(output, 1, 0)]);
|
|
557
562
|
}
|
|
@@ -1068,7 +1073,7 @@ export class CommandController {
|
|
|
1068
1073
|
}
|
|
1069
1074
|
|
|
1070
1075
|
try {
|
|
1071
|
-
await this.ctx.
|
|
1076
|
+
await this.ctx.session.moveSession(resolvedPath);
|
|
1072
1077
|
} catch (err) {
|
|
1073
1078
|
this.ctx.showError(`Move failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1074
1079
|
return;
|
|
@@ -1593,11 +1598,28 @@ function padColumn(text: string, width: number): string {
|
|
|
1593
1598
|
return `${text}${padding(width - visible)}`;
|
|
1594
1599
|
}
|
|
1595
1600
|
|
|
1596
|
-
|
|
1601
|
+
type AggregateDisplayStatus = NonNullable<UsageLimit["status"]> | "neutral";
|
|
1602
|
+
|
|
1603
|
+
function isUsedOnlyAbsoluteAmount(limit: UsageLimit): boolean {
|
|
1604
|
+
const amount = limit.amount;
|
|
1605
|
+
return (
|
|
1606
|
+
amount.unit !== "percent" &&
|
|
1607
|
+
amount.unit !== "unknown" &&
|
|
1608
|
+
amount.used !== undefined &&
|
|
1609
|
+
Number.isFinite(amount.used) &&
|
|
1610
|
+
amount.limit === undefined &&
|
|
1611
|
+
amount.remaining === undefined &&
|
|
1612
|
+
resolveUsedFraction(limit) === undefined
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function resolveAggregateStatus(limits: UsageLimit[]): AggregateDisplayStatus {
|
|
1597
1617
|
const hasOk = limits.some(limit => limit.status === "ok");
|
|
1598
1618
|
const hasWarning = limits.some(limit => limit.status === "warning");
|
|
1599
1619
|
const hasExhausted = limits.some(limit => limit.status === "exhausted");
|
|
1600
|
-
if (!hasOk && !hasWarning && !hasExhausted)
|
|
1620
|
+
if (!hasOk && !hasWarning && !hasExhausted) {
|
|
1621
|
+
return limits.length > 0 && limits.every(isUsedOnlyAbsoluteAmount) ? "neutral" : "unknown";
|
|
1622
|
+
}
|
|
1601
1623
|
if (hasOk) {
|
|
1602
1624
|
return hasWarning || hasExhausted ? "warning" : "ok";
|
|
1603
1625
|
}
|
|
@@ -1625,6 +1647,8 @@ function formatAggregateAmount(limits: UsageLimit[]): string {
|
|
|
1625
1647
|
return `${formatNumber(remainingPct)}% free`;
|
|
1626
1648
|
}
|
|
1627
1649
|
|
|
1650
|
+
if (limits.length > 0 && limits.every(isUsedOnlyAbsoluteAmount)) return "";
|
|
1651
|
+
|
|
1628
1652
|
// Count unique accounts from limit scopes — not limits.length.
|
|
1629
1653
|
const uniqueAccountIds = new Set(
|
|
1630
1654
|
limits.map(limit => limit.scope.accountId).filter((id): id is string => typeof id === "string" && id.length > 0),
|
|
@@ -1636,17 +1660,24 @@ function formatAggregateAmount(limits: UsageLimit[]): string {
|
|
|
1636
1660
|
}
|
|
1637
1661
|
|
|
1638
1662
|
function resolveResetRange(limits: UsageLimit[], nowMs: number): string | null {
|
|
1639
|
-
const
|
|
1640
|
-
.map(limit => limit.window
|
|
1641
|
-
.filter(
|
|
1642
|
-
|
|
1643
|
-
|
|
1663
|
+
const windows = limits
|
|
1664
|
+
.map(limit => limit.window)
|
|
1665
|
+
.filter(
|
|
1666
|
+
(window): window is NonNullable<UsageLimit["window"]> =>
|
|
1667
|
+
window?.resetsAt !== undefined && Number.isFinite(window.resetsAt) && window.resetsAt > nowMs,
|
|
1668
|
+
);
|
|
1669
|
+
if (windows.length === 0) return null;
|
|
1670
|
+
// Use the shared verb when every contributing window agrees (e.g. all "tick");
|
|
1671
|
+
// mixed or absent labels fall back to the generic "resets".
|
|
1672
|
+
const labels = new Set(windows.map(window => window.resetLabel ?? "resets"));
|
|
1673
|
+
const verb = labels.size === 1 ? [...labels][0]! : "resets";
|
|
1674
|
+
const offsets = windows.map(window => window.resetsAt! - nowMs);
|
|
1644
1675
|
const minReset = Math.min(...offsets);
|
|
1645
1676
|
const maxReset = Math.max(...offsets);
|
|
1646
1677
|
if (maxReset - minReset > 60_000) {
|
|
1647
|
-
return
|
|
1678
|
+
return `${verb} in ${formatDuration(minReset)}–${formatDuration(maxReset)}`;
|
|
1648
1679
|
}
|
|
1649
|
-
return
|
|
1680
|
+
return `${verb} in ${formatDuration(minReset)}`;
|
|
1650
1681
|
}
|
|
1651
1682
|
/**
|
|
1652
1683
|
* Compact one-line quota summary for a single advisor's provider.
|
|
@@ -1700,7 +1731,8 @@ export function formatCompactQuota(
|
|
|
1700
1731
|
return `Quota: ${lines.join(" │ ")}`;
|
|
1701
1732
|
}
|
|
1702
1733
|
|
|
1703
|
-
function resolveStatusIcon(status:
|
|
1734
|
+
function resolveStatusIcon(status: AggregateDisplayStatus, uiTheme: typeof theme): string {
|
|
1735
|
+
if (status === "neutral") return uiTheme.fg("dim", uiTheme.status.info);
|
|
1704
1736
|
if (status === "exhausted") return uiTheme.fg("error", uiTheme.status.error);
|
|
1705
1737
|
if (status === "warning") return uiTheme.fg("warning", uiTheme.status.warning);
|
|
1706
1738
|
if (status === "ok") return uiTheme.fg("success", uiTheme.status.success);
|
|
@@ -1715,6 +1747,14 @@ function resolveStatusColor(status: UsageLimit["status"]): "success" | "warning"
|
|
|
1715
1747
|
}
|
|
1716
1748
|
|
|
1717
1749
|
function renderUsageBar(limit: UsageLimit, uiTheme: typeof theme, barWidth: number): string {
|
|
1750
|
+
const usedAmount = limit.amount.used;
|
|
1751
|
+
if (usedAmount !== undefined && isUsedOnlyAbsoluteAmount(limit)) {
|
|
1752
|
+
const used =
|
|
1753
|
+
limit.amount.unit === "usd"
|
|
1754
|
+
? `$${usedAmount.toFixed(2)}`
|
|
1755
|
+
: `${formatNumber(usedAmount, 2)} ${limit.amount.unit}`;
|
|
1756
|
+
return uiTheme.fg("dim", truncateJobLabel(`${used} used`, barWidth));
|
|
1757
|
+
}
|
|
1718
1758
|
const fraction = resolveUsedFraction(limit);
|
|
1719
1759
|
if (fraction === undefined) {
|
|
1720
1760
|
return uiTheme.fg("dim", "·".repeat(barWidth));
|
|
@@ -1752,6 +1792,7 @@ export function renderUsageReports(
|
|
|
1752
1792
|
nowMs: number,
|
|
1753
1793
|
availableWidth: number,
|
|
1754
1794
|
resolveActiveAccount?: (provider: string) => OAuthAccountIdentity | undefined,
|
|
1795
|
+
usageModelSelectors: readonly string[] = [],
|
|
1755
1796
|
): string {
|
|
1756
1797
|
const lines: string[] = [];
|
|
1757
1798
|
const latestFetchedAt = Math.max(...reports.map(report => report.fetchedAt ?? 0));
|
|
@@ -1805,6 +1846,13 @@ export function renderUsageReports(
|
|
|
1805
1846
|
if (activeAccountLabel) {
|
|
1806
1847
|
lines.push(` ${uiTheme.fg("accent", "in use by this session:")} ${activeAccountLabel}`);
|
|
1807
1848
|
}
|
|
1849
|
+
const reportingModels = usageModelSelectors.filter(selector => selector.startsWith(`${provider}/`));
|
|
1850
|
+
if (reportingModels.length > 0) {
|
|
1851
|
+
lines.push(` ${uiTheme.fg("accent", "Models with usage data")}`);
|
|
1852
|
+
for (const selector of reportingModels) {
|
|
1853
|
+
lines.push(` ${replaceTabs(truncateToWidth(sanitizeText(selector), availableWidth - 4))}`);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1808
1856
|
|
|
1809
1857
|
// Provider-wide disclaimers (e.g. "OMP-observed spend only") render once
|
|
1810
1858
|
// above the per-account sections instead of duplicating onto every limit.
|
|
@@ -23,12 +23,14 @@ import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
|
|
|
23
23
|
import idleRecapPrompt from "../../prompts/system/recap-user.md" with { type: "text" };
|
|
24
24
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
25
25
|
import { isSilentAbort, readQueueChipText, resolveAbortLabel } from "../../session/messages";
|
|
26
|
+
import { type ApprovalMode, resolveApproval } from "../../tools/approval";
|
|
26
27
|
import { previewLine, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
27
28
|
import { PROPOSE_DEVICE_NAME, writeDeviceDispatch } from "../../tools/resolve";
|
|
28
29
|
import { nextActionableTask } from "../../tools/todo";
|
|
29
30
|
import { SpeechEnhancer } from "../../tts/speech-enhancer";
|
|
30
31
|
import { vocalizer } from "../../tts/vocalizer";
|
|
31
32
|
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
33
|
+
import { setTerminalTitleState } from "../../utils/title-generator";
|
|
32
34
|
import { interruptHint } from "../shared";
|
|
33
35
|
import { createAssistantMessageComponent } from "../utils/interactive-context-helpers";
|
|
34
36
|
import {
|
|
@@ -81,6 +83,9 @@ export class EventController {
|
|
|
81
83
|
#renderedCustomMessages = new Set<string>();
|
|
82
84
|
#lastIntent: string | undefined = undefined;
|
|
83
85
|
#backgroundTaskCallIds = new Set<string>();
|
|
86
|
+
/** Tool calls whose approval prompt drove the title into `attention`; cleared
|
|
87
|
+
* at their tool_execution_end so the title returns to `working`. */
|
|
88
|
+
#approvalAttentionToolCallIds = new Set<string>();
|
|
84
89
|
#readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
85
90
|
#readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
86
91
|
#toolTimelineComponents = new Map<string, Component>();
|
|
@@ -93,6 +98,15 @@ export class EventController {
|
|
|
93
98
|
#pinnedErrorComponent: AssistantMessageComponent | undefined = undefined;
|
|
94
99
|
#retrySupersededAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
95
100
|
#retrySupersededAssistantQueue: AssistantMessageComponent[] = [];
|
|
101
|
+
// Set when `auto_retry_start` fires and cleared by `auto_retry_end` (both
|
|
102
|
+
// outcomes) — true for exactly the window a retry is outstanding. Gates
|
|
103
|
+
// `sendErrorNotification`: the wire-level `agent_end` for a retryable
|
|
104
|
+
// failure is coalesced with every other attempt in the same saga while the
|
|
105
|
+
// prompt is in flight (see `AgentSession#emitSessionEvent`), so the single
|
|
106
|
+
// `agent_end` that survives to reach this controller can be either a
|
|
107
|
+
// mid-retry blip or the final settle — only the retry lifecycle events
|
|
108
|
+
// (never deferred) can tell them apart.
|
|
109
|
+
#retryPending = false;
|
|
96
110
|
#idleCompactionTimer?: NodeJS.Timeout;
|
|
97
111
|
#idleRecapTimer?: NodeJS.Timeout;
|
|
98
112
|
// In-flight ephemeral recap turn; aborted by #cancelIdleRecap when any
|
|
@@ -149,7 +163,7 @@ export class EventController {
|
|
|
149
163
|
this.#handlers = {
|
|
150
164
|
agent_start: e => this.#handleAgentStart(e),
|
|
151
165
|
agent_end: e => this.#handleAgentEnd(e),
|
|
152
|
-
turn_start: async () =>
|
|
166
|
+
turn_start: async () => {},
|
|
153
167
|
turn_end: async e => this.#handleTurnEnd(e),
|
|
154
168
|
message_start: e => this.#handleMessageStart(e),
|
|
155
169
|
message_update: e => this.#handleMessageUpdate(e),
|
|
@@ -322,10 +336,12 @@ export class EventController {
|
|
|
322
336
|
this.#toolTimelineComponents.clear();
|
|
323
337
|
this.#postToolAssistantComponents.clear();
|
|
324
338
|
this.#backgroundTaskCallIds.clear();
|
|
339
|
+
this.#approvalAttentionToolCallIds.clear();
|
|
325
340
|
this.#readToolCallArgs.clear();
|
|
326
341
|
this.#readToolCallAssistantComponents.clear();
|
|
327
342
|
this.#lastAssistantComponent = undefined;
|
|
328
343
|
this.#pinnedErrorComponent = undefined;
|
|
344
|
+
this.#retryPending = this.ctx.viewSession.isRetrying;
|
|
329
345
|
this.#cancelIdleCompaction();
|
|
330
346
|
this.#cancelIdleRecap();
|
|
331
347
|
for (const timer of this.#ircExpiryTimers.values()) {
|
|
@@ -428,6 +444,7 @@ export class EventController {
|
|
|
428
444
|
this.ctx.statusLine.markActivityStart();
|
|
429
445
|
this.#setTerminalProgress(true);
|
|
430
446
|
this.ctx.ensureLoadingAnimation();
|
|
447
|
+
setTerminalTitleState("working");
|
|
431
448
|
this.ctx.ui.requestRender();
|
|
432
449
|
}
|
|
433
450
|
|
|
@@ -449,6 +466,7 @@ export class EventController {
|
|
|
449
466
|
}
|
|
450
467
|
this.ctx.ui.requestRender();
|
|
451
468
|
} else if (event.message.role === "user") {
|
|
469
|
+
vocalizer.clear();
|
|
452
470
|
const textContent = this.ctx.getUserMessageText(event.message);
|
|
453
471
|
const imageBlocks =
|
|
454
472
|
typeof event.message.content === "string"
|
|
@@ -644,11 +662,6 @@ export class EventController {
|
|
|
644
662
|
}
|
|
645
663
|
}
|
|
646
664
|
|
|
647
|
-
/** A new turn interrupts any speech still queued/playing from the previous one. */
|
|
648
|
-
#handleTurnStart(): void {
|
|
649
|
-
vocalizer.clear();
|
|
650
|
-
}
|
|
651
|
-
|
|
652
665
|
/**
|
|
653
666
|
* Speak streamed assistant output as a side effect of the turn. The mode
|
|
654
667
|
* decides which deltas feed the vocalizer (the vocalizer re-checks enabled):
|
|
@@ -943,6 +956,10 @@ export class EventController {
|
|
|
943
956
|
async #handleToolExecutionStart(event: Extract<AgentSessionEvent, { type: "tool_execution_start" }>): Promise<void> {
|
|
944
957
|
this.#ensureWorkingLoaderWhileStreaming();
|
|
945
958
|
this.#updateWorkingMessageFromIntent(event.intent);
|
|
959
|
+
if (event.toolName === "ask" || this.#toolWillPromptForApproval(event.toolName, event.args)) {
|
|
960
|
+
this.#approvalAttentionToolCallIds.add(event.toolCallId);
|
|
961
|
+
setTerminalTitleState("attention");
|
|
962
|
+
}
|
|
946
963
|
this.#resolveDisplaceablePoll(event.toolName);
|
|
947
964
|
if (!this.ctx.pendingTools.has(event.toolCallId)) {
|
|
948
965
|
if (event.toolName === "read" && readArgsCollapseIntoGroup(event.args)) {
|
|
@@ -1005,6 +1022,23 @@ export class EventController {
|
|
|
1005
1022
|
}
|
|
1006
1023
|
}
|
|
1007
1024
|
|
|
1025
|
+
/**
|
|
1026
|
+
* Whether this tool call will block on an approval prompt before executing.
|
|
1027
|
+
* The extension wrapper waits on `uiContext.select(...)` after emitting
|
|
1028
|
+
* `tool_execution_start`, so an approval-mode / per-tool `prompt` policy is
|
|
1029
|
+
* user-blocking — the title should read `attention`, not `working`. Mirrors
|
|
1030
|
+
* the wrapper's `resolveApproval` inputs (approvalMode + tools.approval); uses
|
|
1031
|
+
* `resolveApproval` rather than `requiresApproval` so a `deny` policy does not
|
|
1032
|
+
* throw in the render path.
|
|
1033
|
+
*/
|
|
1034
|
+
#toolWillPromptForApproval(toolName: string, args: unknown): boolean {
|
|
1035
|
+
const tool = this.ctx.viewSession.getToolByName(toolName);
|
|
1036
|
+
if (!tool) return false;
|
|
1037
|
+
const mode = (settings.get("tools.approvalMode") ?? "yolo") as ApprovalMode;
|
|
1038
|
+
const userPolicies = (settings.get("tools.approval") ?? {}) as Record<string, unknown>;
|
|
1039
|
+
return resolveApproval(tool, args, mode, userPolicies).policy === "prompt";
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1008
1042
|
async #handleToolExecutionUpdate(
|
|
1009
1043
|
event: Extract<AgentSessionEvent, { type: "tool_execution_update" }>,
|
|
1010
1044
|
): Promise<void> {
|
|
@@ -1040,6 +1074,17 @@ export class EventController {
|
|
|
1040
1074
|
// which only fire `tool_execution_end`, never `_update` — do not leave
|
|
1041
1075
|
// the UI looking idle while the session keeps streaming (#3857).
|
|
1042
1076
|
this.#ensureWorkingLoaderWhileStreaming();
|
|
1077
|
+
// Return to `working` only when the LAST outstanding user-blocking prompt
|
|
1078
|
+
// resolves: with queued approval prompts (always-ask/write), the first tool
|
|
1079
|
+
// to finish must not clear the attention signal while another prompt still
|
|
1080
|
+
// waits. `ask` ids are in the set too (added at tool_execution_start), so
|
|
1081
|
+
// the delete also covers them without leaking ids until turn end.
|
|
1082
|
+
if (
|
|
1083
|
+
this.#approvalAttentionToolCallIds.delete(event.toolCallId) &&
|
|
1084
|
+
this.#approvalAttentionToolCallIds.size === 0
|
|
1085
|
+
) {
|
|
1086
|
+
setTerminalTitleState("working");
|
|
1087
|
+
}
|
|
1043
1088
|
if (event.toolName === "read") {
|
|
1044
1089
|
if (this.#inlineReadToolImages(event.toolCallId, event.result)) {
|
|
1045
1090
|
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
@@ -1137,7 +1182,7 @@ export class EventController {
|
|
|
1137
1182
|
}
|
|
1138
1183
|
}
|
|
1139
1184
|
}
|
|
1140
|
-
async #handleAgentEnd(
|
|
1185
|
+
async #handleAgentEnd(event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
|
1141
1186
|
// A superseded agent_end: the agent is already streaming a fresh turn, so
|
|
1142
1187
|
// this event belongs to a turn that has already been replaced. The session
|
|
1143
1188
|
// dispatches to listeners fire-and-forget across an async extension-emit hop
|
|
@@ -1148,11 +1193,12 @@ export class EventController {
|
|
|
1148
1193
|
// the loader and finalizes it at its own agent_end (isStreaming === false by
|
|
1149
1194
|
// then). Mirrors the collab guest's !isStreaming loader reconciler.
|
|
1150
1195
|
if (this.ctx.session.isStreaming) return;
|
|
1196
|
+
setTerminalTitleState("idle");
|
|
1151
1197
|
|
|
1152
|
-
await this.#finishAgentEnd();
|
|
1198
|
+
await this.#finishAgentEnd(event);
|
|
1153
1199
|
}
|
|
1154
1200
|
|
|
1155
|
-
async #finishAgentEnd(): Promise<void> {
|
|
1201
|
+
async #finishAgentEnd(event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
|
1156
1202
|
this.#setTerminalProgress(false);
|
|
1157
1203
|
this.ctx.statusLine.markActivityEnd();
|
|
1158
1204
|
this.#streamingReveal.stop();
|
|
@@ -1186,6 +1232,7 @@ export class EventController {
|
|
|
1186
1232
|
this.#backgroundTaskCallIds = new Set(
|
|
1187
1233
|
Array.from(this.#backgroundTaskCallIds).filter(toolCallId => this.ctx.pendingTools.has(toolCallId)),
|
|
1188
1234
|
);
|
|
1235
|
+
this.#approvalAttentionToolCallIds.clear();
|
|
1189
1236
|
this.#readToolCallArgs.clear();
|
|
1190
1237
|
this.#readToolCallAssistantComponents.clear();
|
|
1191
1238
|
this.#toolTimelineComponents.clear();
|
|
@@ -1200,7 +1247,8 @@ export class EventController {
|
|
|
1200
1247
|
this.ctx.ui.requestRender();
|
|
1201
1248
|
this.#scheduleIdleCompaction();
|
|
1202
1249
|
this.#scheduleIdleRecap();
|
|
1203
|
-
this.
|
|
1250
|
+
this.sendErrorNotification(event);
|
|
1251
|
+
this.sendCompletionNotification(event);
|
|
1204
1252
|
}
|
|
1205
1253
|
|
|
1206
1254
|
/**
|
|
@@ -1358,6 +1406,7 @@ export class EventController {
|
|
|
1358
1406
|
}
|
|
1359
1407
|
|
|
1360
1408
|
async #handleAutoRetryStart(event: Extract<AgentSessionEvent, { type: "auto_retry_start" }>): Promise<void> {
|
|
1409
|
+
this.#retryPending = true;
|
|
1361
1410
|
this.#trackRetrySupersededAssistantComponent(this.#lastAssistantComponent);
|
|
1362
1411
|
this.#stopWorkingLoader();
|
|
1363
1412
|
this.ctx.statusContainer.disposeChildren();
|
|
@@ -1381,6 +1430,7 @@ export class EventController {
|
|
|
1381
1430
|
}
|
|
1382
1431
|
|
|
1383
1432
|
async #handleAutoRetryEnd(event: Extract<AgentSessionEvent, { type: "auto_retry_end" }>): Promise<void> {
|
|
1433
|
+
this.#retryPending = false;
|
|
1384
1434
|
if (this.ctx.retryLoader) {
|
|
1385
1435
|
this.ctx.retryLoader.stop();
|
|
1386
1436
|
this.ctx.retryLoader = undefined;
|
|
@@ -1568,7 +1618,51 @@ export class EventController {
|
|
|
1568
1618
|
return this.ctx.viewSession.getContextUsage()?.tokens ?? 0;
|
|
1569
1619
|
}
|
|
1570
1620
|
|
|
1571
|
-
|
|
1621
|
+
sendErrorNotification(event: Extract<AgentSessionEvent, { type: "agent_end" }>): void {
|
|
1622
|
+
// A running async job or queued delivery will wake the session again, so
|
|
1623
|
+
// its current agent_end is a scheduling pause rather than a user-visible
|
|
1624
|
+
// terminal failure. AgentSession marks that stable outcome before
|
|
1625
|
+
// dispatching the deferred event; do not infer it from mutable job state.
|
|
1626
|
+
if (event.isTerminal === false) return;
|
|
1627
|
+
|
|
1628
|
+
// `AgentSession` defers and coalesces the wire-level `agent_end` while a
|
|
1629
|
+
// prompt is still in flight (see `#emitSessionEvent` in agent-session.ts):
|
|
1630
|
+
// every mid-retry attempt's own settle is superseded, so this method often
|
|
1631
|
+
// sees only ONE `agent_end` for an entire multi-attempt retry saga — which
|
|
1632
|
+
// can be the final failure, not an intermediate one. Gate purely on the
|
|
1633
|
+
// retry lifecycle (`auto_retry_start`/`auto_retry_end`, which are never
|
|
1634
|
+
// deferred) rather than consuming this flag against whichever `agent_end`
|
|
1635
|
+
// happens to arrive first: `#retryPending` is true only for the actual
|
|
1636
|
+
// window a retry is outstanding, so the settled failure that survives
|
|
1637
|
+
// coalescing is never mistaken for a mid-retry blip.
|
|
1638
|
+
if (this.#retryPending) return;
|
|
1639
|
+
|
|
1640
|
+
// Warp structured OSC 777 already drives native completion UX when the
|
|
1641
|
+
// protocol is negotiated — avoid a second legacy desktop/OSC-9 toast.
|
|
1642
|
+
if (isWarpCliAgentProtocolActive()) return;
|
|
1643
|
+
|
|
1644
|
+
const notify = settings.get("error.notify");
|
|
1645
|
+
if (notify === "off") return;
|
|
1646
|
+
|
|
1647
|
+
// Read the turn's own outcome from `agent_end.messages`, not the mutable
|
|
1648
|
+
// active context: a classifier-refusal failure is final (stopReason ===
|
|
1649
|
+
// "error") but gets pruned from `viewSession`'s active context before this
|
|
1650
|
+
// handler runs (see `#removeAssistantMessageFromActiveContext` in
|
|
1651
|
+
// agent-session.ts), so `getLastAssistantMessage()` would see a stale or
|
|
1652
|
+
// absent assistant and silently drop the notification.
|
|
1653
|
+
const last = event.messages.findLast((message): message is AssistantMessage => message.role === "assistant");
|
|
1654
|
+
if (last?.stopReason !== "error") return;
|
|
1655
|
+
|
|
1656
|
+
const sessionName = this.ctx.sessionManager.getSessionName();
|
|
1657
|
+
TERMINAL.sendNotification({
|
|
1658
|
+
title: sessionName || "Oh My Pi",
|
|
1659
|
+
body: "Stopped with error",
|
|
1660
|
+
type: "error",
|
|
1661
|
+
actions: "focus",
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
sendCompletionNotification(event: Extract<AgentSessionEvent, { type: "agent_end" }>): void {
|
|
1572
1666
|
const notify = settings.get("completion.notify");
|
|
1573
1667
|
if (notify === "off") return;
|
|
1574
1668
|
|
|
@@ -1576,11 +1670,12 @@ export class EventController {
|
|
|
1576
1670
|
// protocol is negotiated — avoid a second legacy desktop/OSC-9 toast.
|
|
1577
1671
|
if (isWarpCliAgentProtocolActive()) return;
|
|
1578
1672
|
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1581
|
-
//
|
|
1582
|
-
//
|
|
1583
|
-
|
|
1673
|
+
// Read the turn's own outcome from `agent_end.messages`, not the mutable
|
|
1674
|
+
// active context (see `sendErrorNotification` above for why `viewSession`'s
|
|
1675
|
+
// snapshot can be stale): an aborted or errored turn is not "Task
|
|
1676
|
+
// complete", and using the same event `sendErrorNotification` just read
|
|
1677
|
+
// keeps the two notifications mutually exclusive for one settled turn.
|
|
1678
|
+
const last = event.messages.findLast((message): message is AssistantMessage => message.role === "assistant");
|
|
1584
1679
|
if (last?.stopReason === "aborted" || last?.stopReason === "error") return;
|
|
1585
1680
|
|
|
1586
1681
|
const sessionName = this.ctx.sessionManager.getSessionName();
|
|
@@ -28,7 +28,7 @@ import { HookSelectorComponent, type HookSelectorSlider } from "../../modes/comp
|
|
|
28
28
|
import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
|
|
29
29
|
import type { InteractiveModeContext, InteractiveSelectorDialogOptions } from "../../modes/types";
|
|
30
30
|
import { normalizeCustomMessagePayload, USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
31
|
-
import {
|
|
31
|
+
import { setExtensionTerminalTitle, setSessionTerminalTitle } from "../../utils/title-generator";
|
|
32
32
|
|
|
33
33
|
const MAX_WIDGET_LINES = 10;
|
|
34
34
|
const ASK_OTHER_OPTION = "Other (type your own)";
|
|
@@ -94,7 +94,7 @@ export class ExtensionUiController {
|
|
|
94
94
|
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
95
95
|
setWorkingMessage: message => this.ctx.setWorkingMessage(message),
|
|
96
96
|
setWidget: (key, content, options) => this.setHookWidget(key, content, options),
|
|
97
|
-
setTitle: title =>
|
|
97
|
+
setTitle: title => setExtensionTerminalTitle(title),
|
|
98
98
|
custom: (factory, options) => this.showHookCustom(factory, options),
|
|
99
99
|
setEditorText: text => {
|
|
100
100
|
this.ctx.editor.setText(text);
|
|
@@ -128,6 +128,16 @@ export class ExtensionUiController {
|
|
|
128
128
|
};
|
|
129
129
|
this.ctx.setToolUIContext(uiContext, true);
|
|
130
130
|
this.#toolUIContext = uiContext;
|
|
131
|
+
this.ctx.session.setUsageFallbackConfirmer?.(confirmation => {
|
|
132
|
+
const reserve =
|
|
133
|
+
confirmation.remainingPercent === undefined
|
|
134
|
+
? "inside the configured reserve margin"
|
|
135
|
+
: `${confirmation.remainingPercent.toFixed(1)}% remaining`;
|
|
136
|
+
return this.showHookConfirm(
|
|
137
|
+
"Coding-plan reserve reached",
|
|
138
|
+
`${confirmation.from} has ${reserve}. Switch to ${confirmation.to}? Choose No to keep using the current plan.`,
|
|
139
|
+
);
|
|
140
|
+
});
|
|
131
141
|
|
|
132
142
|
const extensionRunner = this.ctx.session.extensionRunner;
|
|
133
143
|
if (!extensionRunner) {
|
|
@@ -165,6 +175,8 @@ export class ExtensionUiController {
|
|
|
165
175
|
},
|
|
166
176
|
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
|
167
177
|
setThinkingLevel: level => this.ctx.session.setThinkingLevel(level),
|
|
178
|
+
getServiceTiers: () => this.ctx.session.serviceTierByFamily,
|
|
179
|
+
setServiceTier: (family, tier) => this.ctx.session.setServiceTierFamily(family, tier),
|
|
168
180
|
getCommands: () => getSessionSlashCommands(this.ctx.session),
|
|
169
181
|
getSessionName: () => this.ctx.sessionManager.getSessionName(),
|
|
170
182
|
setSessionName: name => this.#updateSessionName(name),
|
|
@@ -396,6 +408,8 @@ export class ExtensionUiController {
|
|
|
396
408
|
},
|
|
397
409
|
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
|
398
410
|
setThinkingLevel: (level, persist) => this.ctx.session.setThinkingLevel(level, persist),
|
|
411
|
+
getServiceTiers: () => this.ctx.session.serviceTierByFamily,
|
|
412
|
+
setServiceTier: (family, tier) => this.ctx.session.setServiceTierFamily(family, tier),
|
|
399
413
|
getCommands: () => getSessionSlashCommands(this.ctx.session),
|
|
400
414
|
getSessionName: () => this.ctx.sessionManager.getSessionName(),
|
|
401
415
|
setSessionName: name => this.#updateSessionName(name),
|
|
@@ -532,7 +546,7 @@ export class ExtensionUiController {
|
|
|
532
546
|
* Show a tool error in the chat.
|
|
533
547
|
*/
|
|
534
548
|
showToolError(toolName: string, error: string): void {
|
|
535
|
-
const errorText = new Text(
|
|
549
|
+
const errorText = new Text(`Tool "${toolName}" error: ${error}`, 1, 0).setStyleFn(t => theme.fg("error", t));
|
|
536
550
|
this.ctx.present(errorText);
|
|
537
551
|
}
|
|
538
552
|
|
|
@@ -1092,7 +1106,9 @@ export class ExtensionUiController {
|
|
|
1092
1106
|
}
|
|
1093
1107
|
|
|
1094
1108
|
showExtensionError(extensionPath: string, error: string): void {
|
|
1095
|
-
const errorText = new Text(
|
|
1109
|
+
const errorText = new Text(`Extension "${extensionPath}" error: ${error}`, 1, 0).setStyleFn(t =>
|
|
1110
|
+
theme.fg("error", t),
|
|
1111
|
+
);
|
|
1096
1112
|
this.ctx.present(errorText);
|
|
1097
1113
|
}
|
|
1098
1114
|
async #handleInteractiveCompact(instructionsOrOptions: string | CompactOptions | undefined): Promise<void> {
|