@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1
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 +78 -0
- package/README.md +1 -1
- package/dist/cli.js +4685 -4614
- 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/model-registry.d.ts +3 -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 +374 -47
- 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 +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +48 -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 +35 -0
- package/dist/types/live/visualizer.d.ts +30 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -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/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -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 +321 -0
- package/dist/types/session/agent-session.d.ts +135 -753
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- 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 +51 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +24 -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-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 +158 -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/streaming-output.d.ts +8 -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/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -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/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- 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 +7 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- 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 +10 -29
- 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/utils/tools-manager.d.ts +1 -2
- 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/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- 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 +3 -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/setup-cli.ts +2 -14
- package/src/cli/usage-cli.ts +10 -1
- package/src/cli.ts +8 -0
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +16 -2
- 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 +270 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +90 -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/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- 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/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/attestation.ts +91 -0
- package/src/live/controller.ts +517 -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 +422 -0
- package/src/live/visualizer.ts +214 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- 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 +1 -1
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- 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.ts +28 -112
- package/src/modes/controllers/command-controller.ts +56 -15
- 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/input-controller.ts +47 -39
- package/src/modes/controllers/live-command-controller.ts +255 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +80 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +176 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +4 -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/system/computer-safety.md +14 -0
- 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/computer.md +26 -0
- 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 +215 -136
- 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 +343 -0
- package/src/session/agent-session.ts +1940 -12218
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/auth-storage.ts +1 -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 +217 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +266 -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.ts +6 -3
- 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 +0 -46
- 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 +980 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/streaming-output.ts +52 -5
- 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 +257 -20
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +45 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +116 -86
- 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/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +14 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +9 -1
- 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 +121 -29
- package/src/tts/streaming-player.ts +82 -283
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- 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/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
package/src/tools/read.ts
CHANGED
|
@@ -11,12 +11,14 @@ import type {
|
|
|
11
11
|
ToolTier,
|
|
12
12
|
} from "@oh-my-pi/pi-agent-core";
|
|
13
13
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
14
|
-
import {
|
|
14
|
+
import { type SummaryResult, summarizeCode } from "@oh-my-pi/pi-natives";
|
|
15
15
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
16
16
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
17
17
|
import {
|
|
18
18
|
getRemoteDir,
|
|
19
19
|
type ImageMetadata,
|
|
20
|
+
isEexist,
|
|
21
|
+
isEnotempty,
|
|
20
22
|
isProbablyBinary,
|
|
21
23
|
logger,
|
|
22
24
|
prompt,
|
|
@@ -40,7 +42,7 @@ import { InternalUrlRouter, resolveLocalUrlToFile, resolveLocalUrlToPath } from
|
|
|
40
42
|
import { type ResolvedArtifactFile, resolveArtifactFile } from "../internal-urls/artifact-protocol";
|
|
41
43
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
42
44
|
import type { InternalUrl } from "../internal-urls/types";
|
|
43
|
-
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
45
|
+
import { getLanguageFromPath, isMarkdownPath, type Theme } from "../modes/theme/theme";
|
|
44
46
|
import readDescription from "../prompts/tools/read.md" with { type: "text" };
|
|
45
47
|
import type { ToolSession } from "../sdk";
|
|
46
48
|
import {
|
|
@@ -94,6 +96,7 @@ import {
|
|
|
94
96
|
} from "./output-meta";
|
|
95
97
|
import {
|
|
96
98
|
expandPath,
|
|
99
|
+
findUniqueWorkspaceSuffix,
|
|
97
100
|
formatPathRelativeToCwd,
|
|
98
101
|
isReadableUrlPath,
|
|
99
102
|
type LineRange,
|
|
@@ -155,12 +158,13 @@ const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
|
155
158
|
const MAX_SUMMARY_LINES = 20_000;
|
|
156
159
|
const MAX_ARTIFACT_RAW_INLINE_BYTES = DEFAULT_MAX_BYTES;
|
|
157
160
|
/**
|
|
158
|
-
*
|
|
159
|
-
* `
|
|
160
|
-
* on disk is unchanged. Shared with the streaming sink path so one setting
|
|
161
|
-
* covers `bash`/`ssh`/`python`/`js eval` and `read` uniformly.
|
|
161
|
+
* Prose files (Markdown flavors and plain text) skip code-block summarization
|
|
162
|
+
* unless `read.summarize.prose` opts them in.
|
|
162
163
|
*/
|
|
163
|
-
|
|
164
|
+
function isProseSummaryPath(filePath: string): boolean {
|
|
165
|
+
return isMarkdownPath(filePath) || path.extname(filePath).toLowerCase() === ".txt";
|
|
166
|
+
}
|
|
167
|
+
|
|
164
168
|
// Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs
|
|
165
169
|
const REMOTE_MOUNT_PREFIX = getRemoteDir() + path.sep;
|
|
166
170
|
|
|
@@ -639,7 +643,6 @@ async function streamLinesFromFile(
|
|
|
639
643
|
|
|
640
644
|
// Maximum image file size (20MB) - larger images will be rejected to prevent OOM during serialization
|
|
641
645
|
const MAX_IMAGE_SIZE = MAX_IMAGE_INPUT_BYTES;
|
|
642
|
-
const GLOB_TIMEOUT_MS = 5000;
|
|
643
646
|
|
|
644
647
|
function isNotFoundError(error: unknown): boolean {
|
|
645
648
|
if (!error || typeof error !== "object") return false;
|
|
@@ -647,61 +650,6 @@ function isNotFoundError(error: unknown): boolean {
|
|
|
647
650
|
return code === "ENOENT" || code === "ENOTDIR";
|
|
648
651
|
}
|
|
649
652
|
|
|
650
|
-
/**
|
|
651
|
-
* Escape glob metacharacters so a literal path (e.g. `foo[1].ts`) interpolated
|
|
652
|
-
* into a suffix-glob pattern matches itself. Each metachar is wrapped in a
|
|
653
|
-
* character class (the native glob engine rewrites `\` to `/`, so backslash
|
|
654
|
-
* escaping is unavailable). `]`/`}` need no escaping once their openers are
|
|
655
|
-
* neutralized — unmatched closers are literal.
|
|
656
|
-
*/
|
|
657
|
-
function escapeGlobMetachars(value: string): string {
|
|
658
|
-
return value.replace(/[*?[{]/g, "[$&]");
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Attempt to resolve a non-existent path by finding a unique suffix match within the workspace.
|
|
663
|
-
* Uses a glob suffix pattern so the native engine handles matching directly.
|
|
664
|
-
* Returns null when 0 or >1 candidates match (ambiguous = no auto-resolution).
|
|
665
|
-
*/
|
|
666
|
-
async function findUniqueSuffixMatch(
|
|
667
|
-
rawPath: string,
|
|
668
|
-
cwd: string,
|
|
669
|
-
signal?: AbortSignal,
|
|
670
|
-
): Promise<{ absolutePath: string; displayPath: string } | null> {
|
|
671
|
-
const normalized = rawPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
672
|
-
if (!normalized) return null;
|
|
673
|
-
const pattern = `**/${escapeGlobMetachars(normalized)}`;
|
|
674
|
-
|
|
675
|
-
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
|
676
|
-
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
677
|
-
|
|
678
|
-
let matches: string[];
|
|
679
|
-
try {
|
|
680
|
-
const result = await untilAborted(combinedSignal, () =>
|
|
681
|
-
glob({
|
|
682
|
-
pattern,
|
|
683
|
-
path: cwd,
|
|
684
|
-
// No fileType filter: matches both files and directories
|
|
685
|
-
hidden: true,
|
|
686
|
-
}),
|
|
687
|
-
);
|
|
688
|
-
matches = result.matches.map(m => m.path);
|
|
689
|
-
} catch (error) {
|
|
690
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
691
|
-
if (!signal?.aborted) return null; // timeout — give up silently
|
|
692
|
-
throw new ToolAbortError();
|
|
693
|
-
}
|
|
694
|
-
return null;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
if (matches.length !== 1) return null;
|
|
698
|
-
|
|
699
|
-
return {
|
|
700
|
-
absolutePath: path.resolve(cwd, matches[0]),
|
|
701
|
-
displayPath: matches[0],
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
|
|
705
653
|
function decodeUtf8Text(bytes: Uint8Array): string | null {
|
|
706
654
|
if (bytes.indexOf(0) !== -1) return null;
|
|
707
655
|
|
|
@@ -721,6 +669,22 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from:
|
|
|
721
669
|
const PDF_IMAGE_PLACEHOLDER_RE = /<!--\s*image:\s*([^\s<>]+)(.*?)-->/g;
|
|
722
670
|
const PDF_IMAGE_MEMBER_RE = /^(.*\.pdf):(.*)$/i;
|
|
723
671
|
const PDF_IMAGE_MEMBER_EXTENSION_RE = /\.png$/i;
|
|
672
|
+
const PDF_IMAGE_CACHE_BASENAME_MAX_LENGTH = 96;
|
|
673
|
+
|
|
674
|
+
interface PdfImageSnapshot {
|
|
675
|
+
directory: string;
|
|
676
|
+
filePath: string;
|
|
677
|
+
digest: string;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
interface PdfImageExtraction {
|
|
681
|
+
controller: AbortController;
|
|
682
|
+
promise: Promise<string>;
|
|
683
|
+
settled: boolean;
|
|
684
|
+
waiters: number;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const pdfImageExtractions = new Map<string, PdfImageExtraction>();
|
|
724
688
|
|
|
725
689
|
function pdfImageMemberPath(pdfPath: string, imageId: string): string {
|
|
726
690
|
const member = PDF_IMAGE_MEMBER_EXTENSION_RE.test(imageId) ? imageId : `${imageId}.png`;
|
|
@@ -779,7 +743,6 @@ export interface ReadToolDetails {
|
|
|
779
743
|
/** Paths recovered from a delimited read argument; used only by the TUI to render one call as multiple read rows. */
|
|
780
744
|
displayReadTargets?: string[];
|
|
781
745
|
}
|
|
782
|
-
|
|
783
746
|
type ReadParams = ReadToolInput;
|
|
784
747
|
|
|
785
748
|
/** Parsed representation of a path-embedded selector. */
|
|
@@ -991,7 +954,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
991
954
|
}
|
|
992
955
|
|
|
993
956
|
/**
|
|
994
|
-
* Memoized {@link
|
|
957
|
+
* Memoized {@link findUniqueWorkspaceSuffix} for a single read call. A missing
|
|
995
958
|
* path with archive/sqlite extensions probes the workspace once per stage
|
|
996
959
|
* (archive candidates, sqlite candidates, plain path) — each glob carries a
|
|
997
960
|
* 5s timeout, so repeated lookups of the same string stack into a long
|
|
@@ -1004,7 +967,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1004
967
|
): Promise<{ absolutePath: string; displayPath: string } | null> {
|
|
1005
968
|
const hit = cache.get(rawPath);
|
|
1006
969
|
if (hit !== undefined) return hit;
|
|
1007
|
-
const result = await
|
|
970
|
+
const result = await findUniqueWorkspaceSuffix(rawPath, this.session.cwd, signal);
|
|
1008
971
|
cache.set(rawPath, result);
|
|
1009
972
|
return result;
|
|
1010
973
|
}
|
|
@@ -1106,7 +1069,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1106
1069
|
return null;
|
|
1107
1070
|
}
|
|
1108
1071
|
|
|
1109
|
-
#pdfImageCacheDir(absolutePdfPath: string): string {
|
|
1072
|
+
#pdfImageCacheDir(absolutePdfPath: string, contentDigest: string): string {
|
|
1110
1073
|
const artifactsDir = this.session.getArtifactsDir?.();
|
|
1111
1074
|
let root = artifactsDir ?? undefined;
|
|
1112
1075
|
if (root === undefined) {
|
|
@@ -1115,8 +1078,28 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1115
1078
|
? sessionFile.slice(0, -6)
|
|
1116
1079
|
: path.join(os.tmpdir(), "omp-read-pdf-images");
|
|
1117
1080
|
}
|
|
1118
|
-
const basename = path
|
|
1119
|
-
|
|
1081
|
+
const basename = path
|
|
1082
|
+
.basename(absolutePdfPath)
|
|
1083
|
+
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
1084
|
+
.slice(0, PDF_IMAGE_CACHE_BASENAME_MAX_LENGTH);
|
|
1085
|
+
const pathDigest = Bun.hash(absolutePdfPath).toString(36);
|
|
1086
|
+
return path.join(root, "read-pdf-images", `${basename}-${pathDigest}-${contentDigest}`);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async #snapshotPdfSource(absolutePdfPath: string, signal?: AbortSignal): Promise<PdfImageSnapshot> {
|
|
1090
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "omp-read-pdf-"));
|
|
1091
|
+
try {
|
|
1092
|
+
const bytes = await untilAborted(signal, () => Bun.file(absolutePdfPath).bytes());
|
|
1093
|
+
signal?.throwIfAborted();
|
|
1094
|
+
const digest = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
1095
|
+
const filePath = path.join(directory, "source.pdf");
|
|
1096
|
+
await Bun.write(filePath, bytes);
|
|
1097
|
+
signal?.throwIfAborted();
|
|
1098
|
+
return { directory, filePath, digest };
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1120
1103
|
}
|
|
1121
1104
|
|
|
1122
1105
|
async #listPdfImageMembers(imageDir: string): Promise<string[]> {
|
|
@@ -1133,8 +1116,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1133
1116
|
}
|
|
1134
1117
|
}
|
|
1135
1118
|
|
|
1136
|
-
async #
|
|
1137
|
-
const imageDir = this.#pdfImageCacheDir(absolutePdfPath);
|
|
1119
|
+
async #extractPdfImages(snapshot: PdfImageSnapshot, imageDir: string, signal: AbortSignal): Promise<string> {
|
|
1138
1120
|
const markerPath = path.join(imageDir, ".extracted");
|
|
1139
1121
|
try {
|
|
1140
1122
|
await fs.stat(markerPath);
|
|
@@ -1143,15 +1125,74 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1143
1125
|
if (!isNotFoundError(error)) throw error;
|
|
1144
1126
|
}
|
|
1145
1127
|
|
|
1146
|
-
await fs.
|
|
1147
|
-
await fs.
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
await
|
|
1151
|
-
|
|
1128
|
+
await fs.mkdir(path.dirname(imageDir), { recursive: true });
|
|
1129
|
+
const stagingDir = await fs.mkdtemp(`${imageDir}.tmp-`);
|
|
1130
|
+
let published = false;
|
|
1131
|
+
try {
|
|
1132
|
+
const result = await convertFileWithMarkit(snapshot.filePath, signal, { imageDir: stagingDir });
|
|
1133
|
+
if (!result.ok) {
|
|
1134
|
+
throw new ToolError(`Cannot extract images from PDF: ${result.error ?? "conversion failed"}`);
|
|
1135
|
+
}
|
|
1136
|
+
await Bun.write(path.join(stagingDir, ".extracted"), "ok");
|
|
1137
|
+
try {
|
|
1138
|
+
await fs.rename(stagingDir, imageDir);
|
|
1139
|
+
published = true;
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
if (!isEexist(error) && !isEnotempty(error)) throw error;
|
|
1142
|
+
try {
|
|
1143
|
+
await fs.stat(markerPath);
|
|
1144
|
+
} catch (markerError) {
|
|
1145
|
+
if (isNotFoundError(markerError)) throw error;
|
|
1146
|
+
throw markerError;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return imageDir;
|
|
1150
|
+
} finally {
|
|
1151
|
+
if (!published) await fs.rm(stagingDir, { recursive: true, force: true });
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
#createPdfImageExtraction(snapshot: PdfImageSnapshot, imageDir: string): PdfImageExtraction {
|
|
1156
|
+
const controller = new AbortController();
|
|
1157
|
+
const promise = this.#extractPdfImages(snapshot, imageDir, controller.signal).finally(() =>
|
|
1158
|
+
fs.rm(snapshot.directory, { recursive: true, force: true }),
|
|
1159
|
+
);
|
|
1160
|
+
const extraction: PdfImageExtraction = { controller, promise, settled: false, waiters: 0 };
|
|
1161
|
+
const settle = () => {
|
|
1162
|
+
extraction.settled = true;
|
|
1163
|
+
if (pdfImageExtractions.get(imageDir) === extraction) pdfImageExtractions.delete(imageDir);
|
|
1164
|
+
};
|
|
1165
|
+
void promise.then(settle, settle);
|
|
1166
|
+
return extraction;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async #waitForPdfImageExtraction(extraction: PdfImageExtraction, signal: AbortSignal | undefined): Promise<string> {
|
|
1170
|
+
extraction.waiters++;
|
|
1171
|
+
try {
|
|
1172
|
+
return await untilAborted(signal, extraction.promise);
|
|
1173
|
+
} finally {
|
|
1174
|
+
extraction.waiters--;
|
|
1175
|
+
if (extraction.waiters === 0 && !extraction.settled) {
|
|
1176
|
+
extraction.controller.abort();
|
|
1177
|
+
try {
|
|
1178
|
+
await extraction.promise;
|
|
1179
|
+
} catch {}
|
|
1180
|
+
}
|
|
1152
1181
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
async #ensurePdfImageCache(absolutePdfPath: string, signal?: AbortSignal): Promise<string> {
|
|
1185
|
+
const snapshot = await this.#snapshotPdfSource(absolutePdfPath, signal);
|
|
1186
|
+
const imageDir = this.#pdfImageCacheDir(absolutePdfPath, snapshot.digest);
|
|
1187
|
+
const existing = pdfImageExtractions.get(imageDir);
|
|
1188
|
+
if (existing && !existing.settled && !existing.controller.signal.aborted) {
|
|
1189
|
+
await fs.rm(snapshot.directory, { recursive: true, force: true });
|
|
1190
|
+
return this.#waitForPdfImageExtraction(existing, signal);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const extraction = this.#createPdfImageExtraction(snapshot, imageDir);
|
|
1194
|
+
pdfImageExtractions.set(imageDir, extraction);
|
|
1195
|
+
return this.#waitForPdfImageExtraction(extraction, signal);
|
|
1155
1196
|
}
|
|
1156
1197
|
|
|
1157
1198
|
async #readPdfImageMember(
|
|
@@ -1598,7 +1639,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1598
1639
|
try {
|
|
1599
1640
|
const bridgeText = await bridgePromise;
|
|
1600
1641
|
const bridgeResult = this.#buildInMemoryMultiRangeResult(bridgeText, ranges, {
|
|
1601
|
-
details: { resolvedPath: absolutePath, suffixResolution },
|
|
1642
|
+
details: this.#markMarkdownContentType({ resolvedPath: absolutePath, suffixResolution }, absolutePath),
|
|
1602
1643
|
sourcePath: absolutePath,
|
|
1603
1644
|
entityLabel: "file",
|
|
1604
1645
|
raw: rawSelector,
|
|
@@ -1781,10 +1822,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1781
1822
|
const archive = await openArchive(resolvedArchivePath.absolutePath);
|
|
1782
1823
|
throwIfAborted(signal);
|
|
1783
1824
|
|
|
1784
|
-
const details: ReadToolDetails =
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1825
|
+
const details: ReadToolDetails = this.#markMarkdownContentType(
|
|
1826
|
+
{
|
|
1827
|
+
resolvedPath: resolvedArchivePath.absolutePath,
|
|
1828
|
+
suffixResolution: resolvedArchivePath.suffixResolution,
|
|
1829
|
+
},
|
|
1830
|
+
resolvedArchivePath.archiveSubPath,
|
|
1831
|
+
);
|
|
1788
1832
|
|
|
1789
1833
|
let archiveSubPath = resolvedArchivePath.archiveSubPath;
|
|
1790
1834
|
let sel = parsedSel;
|
|
@@ -1995,6 +2039,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1995
2039
|
return bridge.readTextFile({ path: absolutePath, ...options });
|
|
1996
2040
|
}
|
|
1997
2041
|
|
|
2042
|
+
/**
|
|
2043
|
+
* Tag Markdown reads for the TUI's formatted preview, gated on the opt-in
|
|
2044
|
+
* `read.renderMarkdown` setting. Off by default; when disabled, no local
|
|
2045
|
+
* read is tagged `text/markdown`, so the renderer output is identical to
|
|
2046
|
+
* the pre-setting behavior. Internal-URL reads keep their protocol-supplied
|
|
2047
|
+
* `contentType` and render as Markdown regardless of the setting.
|
|
2048
|
+
*/
|
|
2049
|
+
#markMarkdownContentType(details: ReadToolDetails, filePath: string): ReadToolDetails {
|
|
2050
|
+
if (!details.contentType && this.session.settings.get("read.renderMarkdown") && isMarkdownPath(filePath)) {
|
|
2051
|
+
details.contentType = "text/markdown";
|
|
2052
|
+
}
|
|
2053
|
+
return details;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1998
2056
|
async #trySummarize(absolutePath: string, fileSize: number, signal?: AbortSignal): Promise<SummaryResult | null> {
|
|
1999
2057
|
if (fileSize > MAX_SUMMARY_BYTES) return null;
|
|
2000
2058
|
|
|
@@ -2419,14 +2477,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2419
2477
|
// because only `truncateHead` was being applied.
|
|
2420
2478
|
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
2421
2479
|
return this.#buildInMemoryMultiRangeResult(renderedContent, parsed.ranges, {
|
|
2422
|
-
details: {
|
|
2480
|
+
details: {
|
|
2481
|
+
resolvedPath: absolutePath,
|
|
2482
|
+
contentType: this.session.settings.get("read.renderMarkdown") ? "text/markdown" : undefined,
|
|
2483
|
+
},
|
|
2423
2484
|
sourcePath: absolutePath,
|
|
2424
2485
|
entityLabel: "document",
|
|
2425
2486
|
});
|
|
2426
2487
|
}
|
|
2427
2488
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
2428
2489
|
return this.#buildInMemoryTextResult(renderedContent, offset, limit, {
|
|
2429
|
-
details: {
|
|
2490
|
+
details: {
|
|
2491
|
+
resolvedPath: absolutePath,
|
|
2492
|
+
contentType: this.session.settings.get("read.renderMarkdown") ? "text/markdown" : undefined,
|
|
2493
|
+
},
|
|
2430
2494
|
sourcePath: absolutePath,
|
|
2431
2495
|
entityLabel: "document",
|
|
2432
2496
|
raw: isRawSelector(parsed),
|
|
@@ -2459,7 +2523,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2459
2523
|
if (
|
|
2460
2524
|
parsed.kind === "none" &&
|
|
2461
2525
|
this.session.settings.get("read.summarize.enabled") &&
|
|
2462
|
-
(this.session.settings.get("read.summarize.prose") || !
|
|
2526
|
+
(this.session.settings.get("read.summarize.prose") || !isProseSummaryPath(absolutePath))
|
|
2463
2527
|
) {
|
|
2464
2528
|
const summary = await this.#trySummarize(absolutePath, fileSize, signal);
|
|
2465
2529
|
if (summary?.parsed && summary.elided) {
|
|
@@ -2519,7 +2583,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2519
2583
|
try {
|
|
2520
2584
|
const bridgeText = await bridgePromise;
|
|
2521
2585
|
const bridgeResult = this.#buildInMemoryTextResult(bridgeText, offset, limit, {
|
|
2522
|
-
details:
|
|
2586
|
+
details: this.#markMarkdownContentType(
|
|
2587
|
+
{ resolvedPath: absolutePath, suffixResolution },
|
|
2588
|
+
absolutePath,
|
|
2589
|
+
),
|
|
2523
2590
|
sourcePath: absolutePath,
|
|
2524
2591
|
entityLabel: "file",
|
|
2525
2592
|
raw: isRawSelector(parsed),
|
|
@@ -2818,6 +2885,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2818
2885
|
}
|
|
2819
2886
|
}
|
|
2820
2887
|
|
|
2888
|
+
this.#markMarkdownContentType(details, absolutePath);
|
|
2821
2889
|
if (suffixResolution) {
|
|
2822
2890
|
details.suffixResolution = suffixResolution;
|
|
2823
2891
|
// Inline resolution notice into first text block so the model sees the actual path
|
package/src/tools/renderers.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { astEditToolRenderer } from "./ast-edit";
|
|
|
16
16
|
import { astGrepToolRenderer } from "./ast-grep";
|
|
17
17
|
import { bashToolRenderer } from "./bash";
|
|
18
18
|
import { browserToolRenderer } from "./browser/render";
|
|
19
|
+
import { computerToolRenderer } from "./computer-renderer";
|
|
19
20
|
import { debugToolRenderer } from "./debug";
|
|
20
21
|
import { evalToolRenderer } from "./eval-render";
|
|
21
22
|
import { githubToolRenderer } from "./gh-renderer";
|
|
@@ -82,6 +83,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
82
83
|
ast_edit: astEditToolRenderer as ToolRenderer,
|
|
83
84
|
bash: bashToolRenderer as ToolRenderer,
|
|
84
85
|
browser: browserToolRenderer as ToolRenderer,
|
|
86
|
+
computer: computerToolRenderer as ToolRenderer,
|
|
85
87
|
debug: debugToolRenderer as ToolRenderer,
|
|
86
88
|
eval: evalToolRenderer as ToolRenderer,
|
|
87
89
|
edit: editToolRenderer as ToolRenderer,
|
|
@@ -90,7 +92,13 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
90
92
|
grep: grepToolRenderer as ToolRenderer,
|
|
91
93
|
lsp: lspToolRenderer as ToolRenderer,
|
|
92
94
|
inspect_image: inspectImageToolRenderer as ToolRenderer,
|
|
93
|
-
|
|
95
|
+
// Lazy getter: `hubToolRenderer` lives in a module whose deps (messaging →
|
|
96
|
+
// persisted-agents → vibe/runtime → task/executor → sdk) close an import
|
|
97
|
+
// cycle back here, so reading it at init order-dependently hits its
|
|
98
|
+
// temporal dead zone. Deferring the read to first access sidesteps it.
|
|
99
|
+
get hub(): ToolRenderer {
|
|
100
|
+
return hubToolRenderer as ToolRenderer;
|
|
101
|
+
},
|
|
94
102
|
read: readToolRenderer as ToolRenderer,
|
|
95
103
|
// Keyed by xd:// resolution-device names: the write dispatch delegates here
|
|
96
104
|
// by dispatch tool, and historical `resolve` tool transcripts still render
|
package/src/tools/todo.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
|
|
|
2
2
|
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { prompt } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { isRecord, prompt } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { type } from "arktype";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -18,13 +18,15 @@ import { formatErrorDetail, formatMoreItems, PREVIEW_LIMITS, pluralize } from ".
|
|
|
18
18
|
// Types
|
|
19
19
|
// =============================================================================
|
|
20
20
|
|
|
21
|
-
export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
|
|
21
|
+
export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned" | "blocked";
|
|
22
22
|
/** Operation names accepted by the todo tool and echoed in successful result details. */
|
|
23
|
-
export type TodoOperation = "init" | "start" | "done" | "rm" | "drop" | "append" | "view";
|
|
23
|
+
export type TodoOperation = "init" | "start" | "done" | "rm" | "drop" | "block" | "unblock" | "append" | "view";
|
|
24
24
|
|
|
25
25
|
export interface TodoItem {
|
|
26
26
|
content: string;
|
|
27
27
|
status: TodoStatus;
|
|
28
|
+
/** When `status === "blocked"`, an optional note on what the task is waiting for. */
|
|
29
|
+
blocker?: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface TodoPhase {
|
|
@@ -32,6 +34,21 @@ export interface TodoPhase {
|
|
|
32
34
|
tasks: TodoItem[];
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
/** Whether an unknown value is a persisted todo phase. */
|
|
38
|
+
export function isTodoPhase(value: unknown): value is TodoPhase {
|
|
39
|
+
if (!isRecord(value) || typeof value.name !== "string" || !Array.isArray(value.tasks)) return false;
|
|
40
|
+
return value.tasks.every(
|
|
41
|
+
task =>
|
|
42
|
+
isRecord(task) &&
|
|
43
|
+
typeof task.content === "string" &&
|
|
44
|
+
(task.status === "pending" ||
|
|
45
|
+
task.status === "in_progress" ||
|
|
46
|
+
task.status === "completed" ||
|
|
47
|
+
task.status === "abandoned" ||
|
|
48
|
+
task.status === "blocked"),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
export interface TodoCompletionTransition {
|
|
36
53
|
phase: string;
|
|
37
54
|
content: string;
|
|
@@ -49,7 +66,9 @@ export interface TodoToolDetails {
|
|
|
49
66
|
// Schema
|
|
50
67
|
// =============================================================================
|
|
51
68
|
|
|
52
|
-
const TodoOp = type('"init" | "start" | "done" | "rm" | "drop" | "append" | "view"').describe(
|
|
69
|
+
const TodoOp = type('"init" | "start" | "done" | "rm" | "drop" | "block" | "unblock" | "append" | "view"').describe(
|
|
70
|
+
"operation to apply",
|
|
71
|
+
);
|
|
53
72
|
|
|
54
73
|
const InitListEntry = type({
|
|
55
74
|
phase: type("string").describe("phase name"),
|
|
@@ -65,6 +84,7 @@ const todoSchema = type({
|
|
|
65
84
|
// and both enforce non-empty with op-specific errors. A stray `items: []` on
|
|
66
85
|
// an op that ignores it (e.g. `view`) must not be a hard schema rejection.
|
|
67
86
|
"items?": type("string").describe("task content").array().describe("tasks to append"),
|
|
87
|
+
"reason?": type("string").describe("blocker note (block op)"),
|
|
68
88
|
}).describe("apply a single todo operation");
|
|
69
89
|
|
|
70
90
|
type TodoParams = TodoSchema;
|
|
@@ -89,7 +109,9 @@ function findPhaseByName(phases: TodoPhase[], name: string): TodoPhase | undefin
|
|
|
89
109
|
}
|
|
90
110
|
|
|
91
111
|
function cloneTask(task: TodoItem): TodoItem {
|
|
92
|
-
return
|
|
112
|
+
return task.blocker !== undefined
|
|
113
|
+
? { content: task.content, status: task.status, blocker: task.blocker }
|
|
114
|
+
: { content: task.content, status: task.status };
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
function clonePhases(phases: TodoPhase[]): TodoPhase[] {
|
|
@@ -256,7 +278,9 @@ export function selectCollapsedTodos<T extends { status: TodoStatus }>(
|
|
|
256
278
|
isMatched: (task: T) => boolean,
|
|
257
279
|
cap: number,
|
|
258
280
|
): CollapsedTodoSelection<T> {
|
|
259
|
-
const open = tasks.filter(
|
|
281
|
+
const open = tasks.filter(
|
|
282
|
+
task => task.status === "pending" || task.status === "in_progress" || task.status === "blocked",
|
|
283
|
+
);
|
|
260
284
|
// No open work: fall back to the closed tasks so a settled phase still
|
|
261
285
|
// renders (HUD closed-todo persistence). Closed tasks are never active.
|
|
262
286
|
const base = open.length > 0 ? open : tasks;
|
|
@@ -456,6 +480,41 @@ function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string
|
|
|
456
480
|
}
|
|
457
481
|
return phases;
|
|
458
482
|
}
|
|
483
|
+
case "block": {
|
|
484
|
+
if (!entry.task && !entry.phase) {
|
|
485
|
+
errors.push("block requires a task or phase target");
|
|
486
|
+
return phases;
|
|
487
|
+
}
|
|
488
|
+
// Collapse whitespace runs (incl. newlines) to single spaces: a blocker
|
|
489
|
+
// note rides on one Markdown checklist line (as a trailing HTML comment)
|
|
490
|
+
// and one HUD/summary line, so an embedded newline from a multi-line
|
|
491
|
+
// external error or user question would corrupt the round-trip parse and
|
|
492
|
+
// the rendered line. Normalizing here keeps every consumer one-line-safe.
|
|
493
|
+
const reason = entry.reason?.replace(/\s+/g, " ").trim() || undefined;
|
|
494
|
+
for (const task of getTaskTargets(phases, entry, errors)) {
|
|
495
|
+
// Only actionable open work can be blocked: blocking a phase must not
|
|
496
|
+
// reopen completed/abandoned tasks or erase finished progress. An
|
|
497
|
+
// already-blocked task stays eligible so a later block can refine its
|
|
498
|
+
// blocker note (e.g. first blocked without a reason, then with one).
|
|
499
|
+
if (task.status !== "pending" && task.status !== "in_progress" && task.status !== "blocked") continue;
|
|
500
|
+
task.status = "blocked";
|
|
501
|
+
task.blocker = reason;
|
|
502
|
+
}
|
|
503
|
+
return phases;
|
|
504
|
+
}
|
|
505
|
+
case "unblock": {
|
|
506
|
+
if (!entry.task && !entry.phase) {
|
|
507
|
+
errors.push("unblock requires a task or phase target");
|
|
508
|
+
return phases;
|
|
509
|
+
}
|
|
510
|
+
for (const task of getTaskTargets(phases, entry, errors)) {
|
|
511
|
+
if (task.status === "blocked") {
|
|
512
|
+
task.status = "pending";
|
|
513
|
+
task.blocker = undefined;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return phases;
|
|
517
|
+
}
|
|
459
518
|
case "rm":
|
|
460
519
|
return removeTasks(phases, entry, errors);
|
|
461
520
|
case "append":
|
|
@@ -495,6 +554,7 @@ const STATUS_TO_MARKER: Record<TodoStatus, string> = {
|
|
|
495
554
|
in_progress: "/",
|
|
496
555
|
completed: "x",
|
|
497
556
|
abandoned: "-",
|
|
557
|
+
blocked: "!",
|
|
498
558
|
};
|
|
499
559
|
|
|
500
560
|
export function resolveTodoMarkdownPath(input: string, cwd: string): string {
|
|
@@ -510,7 +570,12 @@ export function phasesToMarkdown(phases: TodoPhase[]): string {
|
|
|
510
570
|
if (i > 0) out.push("");
|
|
511
571
|
out.push(`# ${phases[i].name}`);
|
|
512
572
|
for (const task of phases[i].tasks) {
|
|
513
|
-
|
|
573
|
+
// A blocked task's reason rides in a trailing HTML comment: invisible in
|
|
574
|
+
// rendered markdown, unambiguous to parse back (task content can't
|
|
575
|
+
// contain the comment delimiters), so the note survives `/todo edit` and
|
|
576
|
+
// export/import round-trips.
|
|
577
|
+
const blockerNote = task.status === "blocked" && task.blocker ? ` <!-- blocker: ${task.blocker} -->` : "";
|
|
578
|
+
out.push(`- [${STATUS_TO_MARKER[task.status]}] ${task.content}${blockerNote}`);
|
|
514
579
|
}
|
|
515
580
|
}
|
|
516
581
|
return `${out.join("\n")}\n`;
|
|
@@ -525,6 +590,7 @@ const MARKER_TO_STATUS: Record<string, TodoStatus> = {
|
|
|
525
590
|
">": "in_progress",
|
|
526
591
|
"-": "abandoned",
|
|
527
592
|
"~": "abandoned",
|
|
593
|
+
"!": "blocked",
|
|
528
594
|
};
|
|
529
595
|
|
|
530
596
|
/** Parse a Markdown checklist back into todo phases. */
|
|
@@ -556,10 +622,18 @@ export function markdownToPhases(md: string): { phases: TodoPhase[]; errors: str
|
|
|
556
622
|
const marker = taskMatch[1];
|
|
557
623
|
const status = MARKER_TO_STATUS[marker];
|
|
558
624
|
if (!status) {
|
|
559
|
-
errors.push(`Line ${lineNum + 1}: unknown status marker "[${marker}]" (use [ ], [x], [/], [-])`);
|
|
625
|
+
errors.push(`Line ${lineNum + 1}: unknown status marker "[${marker}]" (use [ ], [x], [/], [-], [!])`);
|
|
560
626
|
continue;
|
|
561
627
|
}
|
|
562
|
-
|
|
628
|
+
// Recover a blocked task's reason from its trailing HTML comment (see
|
|
629
|
+
// phasesToMarkdown), then strip the comment from the visible content.
|
|
630
|
+
const rawContent = taskMatch[2].trim();
|
|
631
|
+
const blockerMatch = /^(.*?)\s*<!--\s*blocker:\s*(.*?)\s*-->$/.exec(rawContent);
|
|
632
|
+
if (status === "blocked" && blockerMatch) {
|
|
633
|
+
currentPhase.tasks.push({ content: blockerMatch[1].trim(), status, blocker: blockerMatch[2].trim() });
|
|
634
|
+
} else {
|
|
635
|
+
currentPhase.tasks.push({ content: rawContent, status });
|
|
636
|
+
}
|
|
563
637
|
continue;
|
|
564
638
|
}
|
|
565
639
|
|
|
@@ -604,6 +678,7 @@ function formatSummary(phases: TodoPhase[], errors: string[], readOnly = false):
|
|
|
604
678
|
}
|
|
605
679
|
// Closed = completed + abandoned, mirroring the per-phase `done` count.
|
|
606
680
|
const closedAll = tasks.filter(task => task.status === "completed" || task.status === "abandoned").length;
|
|
681
|
+
const blockedAll = tasks.filter(task => task.status === "blocked").length;
|
|
607
682
|
// The active phase is the EARLIEST one still holding open work, so the
|
|
608
683
|
// in-progress pointer can sit in a phase whose successors already have
|
|
609
684
|
// completed tasks. Detect that "worked ahead" case to explain the
|
|
@@ -613,7 +688,9 @@ function formatSummary(phases: TodoPhase[], errors: string[], readOnly = false):
|
|
|
613
688
|
(phase, idx) =>
|
|
614
689
|
idx > currentIdx && phase.tasks.some(task => task.status === "completed" || task.status === "abandoned"),
|
|
615
690
|
);
|
|
616
|
-
lines.push(
|
|
691
|
+
lines.push(
|
|
692
|
+
`Overall: ${closedAll}/${tasks.length} done, ${remainingTasks.length} open${blockedAll > 0 ? `, ${blockedAll} blocked` : ""}.`,
|
|
693
|
+
);
|
|
617
694
|
lines.push(
|
|
618
695
|
`Active phase ${currentIdx + 1}/${phases.length} "${current.name}" (${done}/${current.tasks.length})${
|
|
619
696
|
workedAhead
|
|
@@ -625,7 +702,16 @@ function formatSummary(phases: TodoPhase[], errors: string[], readOnly = false):
|
|
|
625
702
|
lines.push(` ${phase.name}:`);
|
|
626
703
|
for (const task of phase.tasks) {
|
|
627
704
|
const checkbox = task.status === "completed" ? "[X]" : "[ ]";
|
|
628
|
-
const tag =
|
|
705
|
+
const tag =
|
|
706
|
+
task.status === "in_progress"
|
|
707
|
+
? " (in progress)"
|
|
708
|
+
: task.status === "abandoned"
|
|
709
|
+
? " (dropped)"
|
|
710
|
+
: task.status === "blocked"
|
|
711
|
+
? task.blocker
|
|
712
|
+
? ` (blocked: ${task.blocker})`
|
|
713
|
+
: " (blocked)"
|
|
714
|
+
: "";
|
|
629
715
|
lines.push(` - ${checkbox} ${task.content}${tag}`);
|
|
630
716
|
}
|
|
631
717
|
}
|
|
@@ -845,6 +931,10 @@ function formatTodoLine(
|
|
|
845
931
|
return uiTheme.fg("accent", `${prefix}${checkbox.unchecked} ${item.content}`);
|
|
846
932
|
case "abandoned":
|
|
847
933
|
return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${strikethroughText(item.content)}`);
|
|
934
|
+
case "blocked": {
|
|
935
|
+
const note = item.blocker ? `blocked: ${item.blocker}` : "blocked";
|
|
936
|
+
return uiTheme.fg("warning", `${prefix}${checkbox.unchecked} ${item.content} (${note})`);
|
|
937
|
+
}
|
|
848
938
|
default:
|
|
849
939
|
// A pending todo lit by a live subagent match renders accent, matching
|
|
850
940
|
// the sticky HUD's convention (#5873).
|
package/src/tools/vibe.ts
CHANGED
|
@@ -23,7 +23,6 @@ import vibeListDescription from "../prompts/tools/vibe-list.md" with { type: "te
|
|
|
23
23
|
import vibeSendDescription from "../prompts/tools/vibe-send.md" with { type: "text" };
|
|
24
24
|
import vibeSpawnDescription from "../prompts/tools/vibe-spawn.md" with { type: "text" };
|
|
25
25
|
import vibeWaitDescription from "../prompts/tools/vibe-wait.md" with { type: "text" };
|
|
26
|
-
import { MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
27
26
|
import { oneLineLabel } from "../task/types";
|
|
28
27
|
import { renderStatusLine } from "../tui";
|
|
29
28
|
import {
|
|
@@ -93,7 +92,7 @@ export interface VibeToolDetails {
|
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
function screensOf(session: ToolSession, ids?: string[]): VibeScreenSnapshot[] {
|
|
96
|
-
return VibeSessionRegistry.global().screens(session
|
|
95
|
+
return VibeSessionRegistry.global().screens(session, ids);
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
function textResult(text: string, details: VibeToolDetails): AgentToolResult<VibeToolDetails> {
|