@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
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
|
@@ -90,7 +90,13 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
90
90
|
grep: grepToolRenderer as ToolRenderer,
|
|
91
91
|
lsp: lspToolRenderer as ToolRenderer,
|
|
92
92
|
inspect_image: inspectImageToolRenderer as ToolRenderer,
|
|
93
|
-
|
|
93
|
+
// Lazy getter: `hubToolRenderer` lives in a module whose deps (messaging →
|
|
94
|
+
// persisted-agents → vibe/runtime → task/executor → sdk) close an import
|
|
95
|
+
// cycle back here, so reading it at init order-dependently hits its
|
|
96
|
+
// temporal dead zone. Deferring the read to first access sidesteps it.
|
|
97
|
+
get hub(): ToolRenderer {
|
|
98
|
+
return hubToolRenderer as ToolRenderer;
|
|
99
|
+
},
|
|
94
100
|
read: readToolRenderer as ToolRenderer,
|
|
95
101
|
// Keyed by xd:// resolution-device names: the write dispatch delegates here
|
|
96
102
|
// by dispatch tool, and historical `resolve` tool transcripts still render
|
|
@@ -5,16 +5,21 @@
|
|
|
5
5
|
* `xd://report_issue`, and the system prompt tells the model to write
|
|
6
6
|
* `<tool>: <concise description>` there when auto-QA is enabled.
|
|
7
7
|
*
|
|
8
|
-
* Enabled by default
|
|
9
|
-
*
|
|
8
|
+
* Enabled by default (`dev.autoqa` defaults to true); `PI_AUTO_QA=0` or an
|
|
9
|
+
* explicit `dev.autoqa: false` short-circuits injection entirely. When the
|
|
10
|
+
* user is only enabled by default (never configured `dev.autoqa` themselves),
|
|
11
|
+
* a persisted `dev.autoqaConsent: "denied"` also disables injection so a "No"
|
|
12
|
+
* in the consent dialog fully turns the feature off.
|
|
10
13
|
* Records grievances to a local SQLite database; never throws from the device
|
|
11
14
|
* dispatch path.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Nothing is written until consent resolves. If the user has never been asked
|
|
17
|
+
* (`dev.autoqaConsent === "unset"`) the process-global consent handler —
|
|
18
|
+
* wired by `InteractiveMode` to a Yes/No popup — is invoked exactly once and
|
|
19
|
+
* the decision is persisted; a denial (or dismissal) drops the pending report
|
|
20
|
+
* without touching the database. Subsequent calls (including from subagents)
|
|
21
|
+
* read the cached decision without prompting. `PI_AUTO_QA_PUSH=1` bypasses
|
|
22
|
+
* the dialog for headless environments.
|
|
18
23
|
*
|
|
19
24
|
* When the user grants consent, push is automatically active against the
|
|
20
25
|
* bundled endpoint (`dev.autoqaPush.endpoint`, default `qa.omp.sh`). Each
|
|
@@ -24,11 +29,13 @@
|
|
|
24
29
|
* the network and never throws.
|
|
25
30
|
*/
|
|
26
31
|
import { Database } from "bun:sqlite";
|
|
32
|
+
import * as fs from "node:fs";
|
|
33
|
+
import * as path from "node:path";
|
|
27
34
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
28
35
|
import type { FetchImpl } from "@oh-my-pi/pi-ai";
|
|
29
36
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
30
37
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
31
|
-
import { $env, $flag,
|
|
38
|
+
import { $env, $flag, getAutoQaDbPath, getInstallId, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
32
39
|
import type { Settings } from "..";
|
|
33
40
|
import type { Theme } from "../modes/theme/theme";
|
|
34
41
|
import { renderStatusLine, truncateToWidth } from "../tui";
|
|
@@ -88,8 +95,23 @@ function parseReportIssueBody(text: string): { tool: string; report: string } {
|
|
|
88
95
|
throw new ToolError(`Invalid report format. ${reportIssueDeviceUsage()}`);
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Whether Auto-QA is active for this session.
|
|
100
|
+
*
|
|
101
|
+
* Precedence: `PI_AUTO_QA` env flag > explicit `dev.autoqa` setting >
|
|
102
|
+
* default-on unless the user previously denied consent. The denial veto only
|
|
103
|
+
* applies to the default: explicitly configuring `dev.autoqa: true` re-enables
|
|
104
|
+
* injection (recording still no-ops until consent is granted).
|
|
105
|
+
*/
|
|
91
106
|
export function isAutoQaEnabled(settings?: Settings): boolean {
|
|
92
|
-
|
|
107
|
+
let fallback = false;
|
|
108
|
+
if (settings) {
|
|
109
|
+
const enabled = !!settings.get("dev.autoqa");
|
|
110
|
+
fallback = settings.isConfigured("dev.autoqa")
|
|
111
|
+
? enabled
|
|
112
|
+
: enabled && settings.get("dev.autoqaConsent") !== "denied";
|
|
113
|
+
}
|
|
114
|
+
return $flag("PI_AUTO_QA", fallback);
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -233,15 +255,18 @@ let cachedDb: Database | null = null;
|
|
|
233
255
|
|
|
234
256
|
/**
|
|
235
257
|
* Open (or return the cached handle for) the auto-QA SQLite database at
|
|
236
|
-
* `~/.omp/
|
|
237
|
-
*
|
|
258
|
+
* `~/.omp/autoqa.db` (XDG: `$XDG_DATA_HOME/omp/autoqa.db`), creating the
|
|
259
|
+
* schema lazily. Returns `null` when the path cannot be resolved or opened.
|
|
238
260
|
*/
|
|
239
261
|
export function openAutoQaDb(): Database | null {
|
|
240
262
|
if (cachedDb) return cachedDb;
|
|
241
|
-
const
|
|
242
|
-
if (!
|
|
263
|
+
const dbPath = getAutoQaDbPath();
|
|
264
|
+
if (!dbPath) return null;
|
|
243
265
|
try {
|
|
244
|
-
|
|
266
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
267
|
+
const db = new Database(dbPath, { create: true });
|
|
268
|
+
// Install the busy handler BEFORE any lock-taking statement. See #2421.
|
|
269
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
245
270
|
db.exec(`
|
|
246
271
|
CREATE TABLE IF NOT EXISTS grievances (
|
|
247
272
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -252,6 +277,17 @@ export function openAutoQaDb(): Database | null {
|
|
|
252
277
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
253
278
|
pushed INTEGER NOT NULL DEFAULT 0
|
|
254
279
|
);
|
|
280
|
+
`);
|
|
281
|
+
// Legacy DBs (May 2026) predate `created_at`. ALTER TABLE only accepts
|
|
282
|
+
// constant defaults, so add it empty and backfill before the index below.
|
|
283
|
+
const hasCreatedAt = db.prepare("SELECT 1 FROM pragma_table_info('grievances') WHERE name = 'created_at'").get();
|
|
284
|
+
if (!hasCreatedAt) {
|
|
285
|
+
db.exec(`
|
|
286
|
+
ALTER TABLE grievances ADD COLUMN created_at TEXT NOT NULL DEFAULT '';
|
|
287
|
+
UPDATE grievances SET created_at = CURRENT_TIMESTAMP WHERE created_at = '';
|
|
288
|
+
`);
|
|
289
|
+
}
|
|
290
|
+
db.exec(`
|
|
255
291
|
CREATE INDEX IF NOT EXISTS grievances_pushed_created_at_idx
|
|
256
292
|
ON grievances (pushed, created_at, id);
|
|
257
293
|
`);
|
|
@@ -471,23 +507,38 @@ export async function flushGrievances(
|
|
|
471
507
|
}
|
|
472
508
|
}
|
|
473
509
|
|
|
474
|
-
/**
|
|
475
|
-
|
|
510
|
+
/**
|
|
511
|
+
* Most recently scheduled record pipeline. Never rejects (the pipeline
|
|
512
|
+
* swallows its own errors); retained so tests can await the fire-and-forget
|
|
513
|
+
* work deterministically via {@link __awaitAutoQaRecordPipelineForTests}.
|
|
514
|
+
*/
|
|
515
|
+
let lastRecordPipeline: Promise<void> = Promise.resolve();
|
|
516
|
+
|
|
517
|
+
/** Test-only: await the last consent → insert → flush pipeline. */
|
|
518
|
+
export function __awaitAutoQaRecordPipelineForTests(): Promise<void> {
|
|
519
|
+
return lastRecordPipeline;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Queue a grievance for recording. The consent → insert → flush pipeline is
|
|
524
|
+
* fire-and-forget: nothing is written until the user grants consent (or
|
|
525
|
+
* `PI_AUTO_QA_PUSH=1` forces headless recording), and the device result
|
|
526
|
+
* returns immediately so the model never waits on the dialog or the network.
|
|
527
|
+
*/
|
|
528
|
+
function recordToolIssue(session: ToolSession, tool: string, report: string): void {
|
|
476
529
|
const canonicalTool = tool.startsWith("proxy_") ? tool.slice("proxy_".length) : tool;
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
db.prepare("INSERT INTO grievances (model, version, tool, report) VALUES (?, ?, ?, ?)").run(
|
|
480
|
-
session.getActiveModelString?.() ?? "unknown",
|
|
481
|
-
VERSION,
|
|
482
|
-
canonicalTool,
|
|
483
|
-
report,
|
|
484
|
-
);
|
|
485
|
-
void (async () => {
|
|
530
|
+
const model = session.getActiveModelString?.() ?? "unknown";
|
|
531
|
+
lastRecordPipeline = (async () => {
|
|
486
532
|
try {
|
|
487
|
-
await resolveAutoQaConsent(session.settings);
|
|
533
|
+
if (!$flag("PI_AUTO_QA_PUSH") && !(await resolveAutoQaConsent(session.settings))) return;
|
|
534
|
+
const db = openAutoQaDb();
|
|
535
|
+
if (!db) return;
|
|
536
|
+
db.prepare(
|
|
537
|
+
"INSERT INTO grievances (model, version, tool, report, created_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)",
|
|
538
|
+
).run(model, VERSION, canonicalTool, report);
|
|
488
539
|
await flushGrievances(db, session.settings);
|
|
489
540
|
} catch (error) {
|
|
490
|
-
logger.debug("autoqa
|
|
541
|
+
logger.debug("autoqa consent pipeline failed", { error: String(error) });
|
|
491
542
|
}
|
|
492
543
|
})();
|
|
493
544
|
}
|
|
@@ -504,7 +555,7 @@ export async function dispatchReportIssueDevice(
|
|
|
504
555
|
try {
|
|
505
556
|
if (isAutoQaEnabled(session.settings)) {
|
|
506
557
|
const { tool, report } = parseReportIssueBody(text);
|
|
507
|
-
|
|
558
|
+
recordToolIssue(session, tool, report);
|
|
508
559
|
}
|
|
509
560
|
} catch (error) {
|
|
510
561
|
if (error instanceof ToolError) throw error;
|