@oh-my-pi/pi-coding-agent 15.10.9 → 15.10.11
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 +117 -0
- package/dist/cli.js +23087 -0
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/async/job-manager.d.ts +18 -0
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/dry-balance-cli.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +1 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
- package/dist/types/cli/usage-cli.d.ts +72 -0
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/commands/read.d.ts +1 -1
- package/dist/types/commands/usage.d.ts +25 -0
- package/dist/types/config/append-only-context-mode.d.ts +2 -1
- package/dist/types/config/model-discovery.d.ts +55 -0
- package/dist/types/config/model-registry.d.ts +20 -219
- package/dist/types/config/model-resolver.d.ts +16 -10
- package/dist/types/config/model-roles.d.ts +28 -0
- package/dist/types/config/models-config-schema.d.ts +523 -42
- package/dist/types/config/models-config.d.ts +385 -0
- package/dist/types/config/settings-schema.d.ts +12 -16
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/log-viewer.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +1 -1
- package/dist/types/debug/terminal-info.d.ts +0 -1
- package/dist/types/eval/backend.d.ts +0 -2
- package/dist/types/eval/idle-timeout.d.ts +0 -4
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -3
- package/dist/types/hindsight/mental-models.d.ts +17 -8
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/lsp/edits.d.ts +9 -0
- package/dist/types/lsp/index.d.ts +2 -2
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +3 -0
- package/dist/types/mcp/json-rpc.d.ts +5 -0
- package/dist/types/mnemopi/state.d.ts +11 -1
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/bash-execution.d.ts +1 -1
- package/dist/types/modes/components/copy-selector.d.ts +1 -1
- package/dist/types/modes/components/dynamic-border.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
- package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
- package/dist/types/modes/components/footer.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +5 -0
- package/dist/types/modes/components/hook-input.d.ts +4 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +1 -1
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +31 -26
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/components/visual-truncate.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +19 -3
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/auth-broker-config.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
- package/dist/types/ssh/connection-manager.d.ts +8 -0
- package/dist/types/task/discovery.d.ts +1 -2
- package/dist/types/task/parallel.d.ts +2 -2
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +4 -0
- package/dist/types/tools/conflict-detect.d.ts +16 -0
- package/dist/types/tools/github-cache.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +3 -0
- package/dist/types/tools/todo.d.ts +2 -0
- package/dist/types/tui/output-block.d.ts +3 -3
- package/dist/types/utils/changelog.d.ts +8 -0
- package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
- package/dist/types/web/scrapers/types.d.ts +12 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +1 -1
- package/examples/extensions/tools.ts +5 -4
- package/package.json +14 -11
- package/scripts/build-binary.ts +18 -23
- package/scripts/bundle-dist.ts +81 -0
- package/scripts/{dev-launch → omp} +1 -1
- package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
- package/src/async/job-manager.ts +57 -3
- package/src/autoresearch/dashboard.ts +1 -1
- package/src/autoresearch/prompt-setup.md +6 -6
- package/src/autoresearch/prompt.md +6 -6
- package/src/capability/fs.ts +10 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -3
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +1 -1
- package/src/cli/gallery-fixtures/types.ts +5 -1
- package/src/cli/list-models.ts +7 -12
- package/src/cli/usage-cli.ts +603 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +69 -5
- package/src/commands/complete.ts +1 -1
- package/src/commands/launch.ts +1 -1
- package/src/commands/read.ts +6 -3
- package/src/commands/usage.ts +35 -0
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/model-selection.ts +1 -1
- package/src/config/append-only-context-mode.ts +6 -12
- package/src/config/model-discovery.ts +554 -0
- package/src/config/model-registry.ts +308 -1025
- package/src/config/model-resolver.ts +113 -156
- package/src/config/model-roles.ts +74 -0
- package/src/config/models-config-schema.ts +57 -8
- package/src/config/models-config.ts +129 -0
- package/src/config/settings-schema.ts +18 -14
- package/src/config/settings.ts +37 -1
- package/src/dap/client.ts +124 -37
- package/src/dap/session.ts +259 -158
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/raw-sse.ts +1 -1
- package/src/debug/terminal-info.ts +0 -3
- package/src/edit/diff.ts +95 -18
- package/src/edit/hashline/block-resolver.ts +20 -1
- package/src/edit/hashline/diff.ts +36 -1
- package/src/edit/hashline/execute.ts +8 -2
- package/src/edit/index.ts +16 -1
- package/src/edit/modes/patch.ts +52 -0
- package/src/edit/modes/replace.ts +56 -22
- package/src/edit/notebook.ts +22 -2
- package/src/edit/renderer.ts +36 -10
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/backend.ts +0 -2
- package/src/eval/completion-bridge.ts +2 -1
- package/src/eval/idle-timeout.ts +2 -9
- package/src/eval/js/context-manager.ts +6 -8
- package/src/eval/js/executor.ts +6 -2
- package/src/eval/js/index.ts +0 -2
- package/src/eval/js/shared/helpers.ts +5 -6
- package/src/eval/js/shared/local-module-loader.ts +1 -1
- package/src/eval/js/shared/prelude.txt +62 -1
- package/src/eval/js/shared/rewrite-imports.ts +49 -23
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +0 -2
- package/src/eval/py/kernel.ts +19 -0
- package/src/eval/py/runner.py +107 -3
- package/src/exec/bash-executor.ts +3 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/extensibility/extensions/types.ts +3 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
- package/src/hindsight/mental-models.ts +59 -12
- package/src/hindsight/state.ts +6 -1
- package/src/internal-urls/artifact-protocol.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/internal-urls/issue-pr-protocol.ts +12 -5
- package/src/internal-urls/router.ts +1 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/lib/xai-http.ts +1 -1
- package/src/lsp/client.ts +118 -38
- package/src/lsp/clients/biome-client.ts +101 -39
- package/src/lsp/edits.ts +143 -95
- package/src/lsp/index.ts +31 -22
- package/src/lsp/render.ts +1 -1
- package/src/lsp/types.ts +2 -0
- package/src/lsp/utils.ts +28 -10
- package/src/main.ts +165 -17
- package/src/mcp/json-rpc.ts +35 -5
- package/src/mcp/transports/stdio.ts +7 -1
- package/src/memories/index.ts +2 -1
- package/src/mnemopi/backend.ts +25 -3
- package/src/mnemopi/state.ts +38 -2
- package/src/modes/components/agent-dashboard.ts +10 -7
- package/src/modes/components/assistant-message.ts +19 -13
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/copy-selector.ts +1 -1
- package/src/modes/components/diff.ts +13 -2
- package/src/modes/components/dynamic-border.ts +12 -3
- package/src/modes/components/extensions/extension-dashboard.ts +8 -5
- package/src/modes/components/extensions/extension-list.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +1 -1
- package/src/modes/components/footer.ts +1 -1
- package/src/modes/components/history-search.ts +1 -1
- package/src/modes/components/hook-editor.ts +8 -0
- package/src/modes/components/hook-input.ts +8 -0
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-selector.ts +66 -54
- package/src/modes/components/plan-review-overlay.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-selector.ts +5 -1
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/transcript-container.ts +373 -141
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +17 -5
- package/src/modes/components/visual-truncate.ts +1 -1
- package/src/modes/components/welcome.ts +108 -26
- package/src/modes/controllers/command-controller.ts +10 -3
- package/src/modes/controllers/event-controller.ts +73 -49
- package/src/modes/controllers/input-controller.ts +5 -5
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -5
- package/src/modes/controllers/streaming-reveal.ts +85 -18
- package/src/modes/interactive-mode.ts +5 -19
- package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
- package/src/modes/setup-wizard/scenes/providers.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
- package/src/modes/setup-wizard/scenes/theme.ts +1 -1
- package/src/modes/setup-wizard/scenes/types.ts +1 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +2 -1
- package/src/prompts/agents/explore.md +2 -2
- package/src/prompts/agents/librarian.md +1 -2
- package/src/prompts/agents/oracle.md +1 -1
- package/src/prompts/agents/plan.md +5 -5
- package/src/prompts/agents/task.md +5 -5
- package/src/prompts/ci-green-request.md +5 -7
- package/src/prompts/goals/goal-budget-limit.md +2 -2
- package/src/prompts/goals/goal-continuation.md +4 -4
- package/src/prompts/goals/goal-mode-active.md +1 -1
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_system.md +2 -2
- package/src/prompts/review-custom-request.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/auto-continue.md +1 -1
- package/src/prompts/system/background-tan-dispatch.md +1 -1
- package/src/prompts/system/btw-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +13 -1
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/eager-todo.md +2 -2
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/system/manual-continue.md +1 -1
- package/src/prompts/system/omfg-user.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +9 -9
- package/src/prompts/system/plan-mode-active.md +4 -4
- package/src/prompts/system/plan-mode-subagent.md +4 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/system-prompt.md +15 -26
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/ttsr-tool-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +2 -2
- package/src/prompts/tools/bash.md +8 -10
- package/src/prompts/tools/browser.md +7 -7
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/find.md +0 -1
- package/src/prompts/tools/github.md +8 -7
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/image-gen.md +1 -1
- package/src/prompts/tools/inspect-image-system.md +1 -1
- package/src/prompts/tools/irc.md +15 -15
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/read.md +3 -4
- package/src/prompts/tools/recall.md +1 -1
- package/src/prompts/tools/reflect.md +1 -1
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/replace.md +4 -10
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search-tool-bm25.md +1 -9
- package/src/prompts/tools/search.md +0 -1
- package/src/prompts/tools/ssh.md +0 -4
- package/src/prompts/tools/task.md +2 -3
- package/src/prompts/tools/todo.md +6 -2
- package/src/sdk.ts +23 -10
- package/src/session/agent-session.ts +44 -10
- package/src/session/auth-broker-config.ts +30 -1
- package/src/session/session-manager.ts +2 -2
- package/src/session/streaming-output.ts +23 -2
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
- package/src/ssh/connection-manager.ts +27 -0
- package/src/task/commands.ts +2 -1
- package/src/task/discovery.ts +17 -24
- package/src/task/executor.ts +61 -53
- package/src/task/index.ts +137 -60
- package/src/task/parallel.ts +3 -3
- package/src/task/render.ts +2 -2
- package/src/task/worktree.ts +64 -56
- package/src/thinking.ts +2 -1
- package/src/tiny/title-client.ts +32 -14
- package/src/tools/archive-reader.ts +30 -2
- package/src/tools/ask.ts +104 -21
- package/src/tools/ast-edit.ts +25 -5
- package/src/tools/auto-generated-guard.ts +20 -3
- package/src/tools/bash-interactive.ts +27 -7
- package/src/tools/bash.ts +54 -13
- package/src/tools/browser/launch.ts +11 -2
- package/src/tools/browser/readable.ts +19 -2
- package/src/tools/browser/registry.ts +4 -1
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +55 -16
- package/src/tools/conflict-detect.ts +50 -4
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval-render.ts +5 -5
- package/src/tools/eval.ts +0 -2
- package/src/tools/fetch.ts +33 -10
- package/src/tools/gh-cache-invalidation.ts +63 -8
- package/src/tools/gh-renderer.ts +1 -1
- package/src/tools/gh.ts +172 -29
- package/src/tools/github-cache.ts +70 -6
- package/src/tools/image-gen.ts +3 -9
- package/src/tools/irc.ts +5 -1
- package/src/tools/job.ts +1 -1
- package/src/tools/read.ts +202 -61
- package/src/tools/render-utils.ts +3 -3
- package/src/tools/resolve.ts +1 -1
- package/src/tools/search.ts +92 -29
- package/src/tools/sqlite-reader.ts +17 -5
- package/src/tools/ssh.ts +8 -8
- package/src/tools/todo.ts +51 -12
- package/src/tools/write.ts +118 -18
- package/src/tui/output-block.ts +4 -4
- package/src/utils/changelog.ts +27 -1
- package/src/utils/file-mentions.ts +2 -1
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/twitter.ts +2 -1
- package/src/web/scrapers/types.ts +87 -8
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/scrapers/youtube.ts +6 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/providers/anthropic.ts +8 -2
- package/src/web/search/providers/codex.ts +2 -1
- package/src/web/search/providers/gemini.ts +2 -3
- package/src/web/search/render.ts +8 -6
- package/dist/types/config/model-equivalence.d.ts +0 -24
- package/dist/types/config/model-id-affixes.d.ts +0 -12
- package/dist/types/config/model-provider-priority.d.ts +0 -1
- package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
- package/src/config/model-equivalence.ts +0 -875
- package/src/config/model-id-affixes.ts +0 -81
- package/src/config/model-provider-priority.ts +0 -56
- package/src/exec/idle-timeout-watchdog.ts +0 -126
package/src/task/parallel.ts
CHANGED
|
@@ -20,13 +20,13 @@ export interface ParallelResult<R> {
|
|
|
20
20
|
*
|
|
21
21
|
* @param items - Items to process
|
|
22
22
|
* @param concurrency - Maximum concurrent operations
|
|
23
|
-
* @param fn - Async function to execute for each item
|
|
23
|
+
* @param fn - Async function to execute for each item; receives a worker signal that fires on abort or fail-fast so in-flight siblings can cancel
|
|
24
24
|
* @param signal - Optional abort signal to stop scheduling new work
|
|
25
25
|
*/
|
|
26
26
|
export async function mapWithConcurrencyLimit<T, R>(
|
|
27
27
|
items: T[],
|
|
28
28
|
concurrency: number,
|
|
29
|
-
fn: (item: T, index: number) => Promise<R>,
|
|
29
|
+
fn: (item: T, index: number, signal: AbortSignal) => Promise<R>,
|
|
30
30
|
signal?: AbortSignal,
|
|
31
31
|
): Promise<ParallelResult<R>> {
|
|
32
32
|
const normalizedConcurrency = Number.isFinite(concurrency) ? Math.floor(concurrency) : items.length;
|
|
@@ -52,7 +52,7 @@ export async function mapWithConcurrencyLimit<T, R>(
|
|
|
52
52
|
const index = nextIndex++;
|
|
53
53
|
if (index >= items.length) return;
|
|
54
54
|
try {
|
|
55
|
-
results[index] = await fn(items[index], index);
|
|
55
|
+
results[index] = await fn(items[index], index, workerSignal);
|
|
56
56
|
} catch (error) {
|
|
57
57
|
// On abort, the fn itself handles it and returns a result
|
|
58
58
|
// Only propagate non-abort errors
|
package/src/task/render.ts
CHANGED
|
@@ -541,7 +541,7 @@ function renderTaskItemLines(tasks: TaskItem[] | undefined, expanded: boolean, t
|
|
|
541
541
|
* the merged result frame so the brief stays visible for the whole task
|
|
542
542
|
* lifecycle — not just until the first progress snapshot replaces the call view.
|
|
543
543
|
*/
|
|
544
|
-
type TaskRenderSection = { lines: string[] };
|
|
544
|
+
type TaskRenderSection = { lines: readonly string[] };
|
|
545
545
|
type ContextSectionRenderer = (width: number) => TaskRenderSection;
|
|
546
546
|
|
|
547
547
|
// Default output-block layout is: left border + one-cell content inset + right
|
|
@@ -578,7 +578,7 @@ export function renderCall(
|
|
|
578
578
|
const header = renderStatusLine({ icon: "pending", title: "Task", description: args.agent }, theme);
|
|
579
579
|
const contextSectionRenderer = createContextSectionRenderer(args, theme);
|
|
580
580
|
return framedBlock(theme, width => {
|
|
581
|
-
const sections: Array<{ label?: string; lines: string[]; separator?: boolean }> = [];
|
|
581
|
+
const sections: Array<{ label?: string; lines: readonly string[]; separator?: boolean }> = [];
|
|
582
582
|
|
|
583
583
|
if (contextSectionRenderer) sections.push(contextSectionRenderer(width));
|
|
584
584
|
|
package/src/task/worktree.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import * as natives from "@oh-my-pi/pi-natives";
|
|
6
6
|
import { getWorktreeDir, hashPath, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import * as git from "../utils/git";
|
|
8
|
+
import { mapWithConcurrencyLimit } from "./parallel";
|
|
8
9
|
|
|
9
10
|
const { IsoBackendKind } = natives;
|
|
10
11
|
type IsoBackendKind = natives.IsoBackendKind;
|
|
@@ -82,16 +83,16 @@ async function discoverNestedRepos(repoRoot: string): Promise<string[]> {
|
|
|
82
83
|
async function captureUntrackedPatch(repoRoot: string, untracked: readonly string[]): Promise<string> {
|
|
83
84
|
if (untracked.length === 0) return "";
|
|
84
85
|
const nullPath = getGitNoIndexNullPath();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
),
|
|
86
|
+
// Bound concurrent git spawns; large untracked sets would otherwise fork one
|
|
87
|
+
// process per file at once.
|
|
88
|
+
const { results: untrackedDiffs } = await mapWithConcurrencyLimit([...untracked], 8, entry =>
|
|
89
|
+
git.diff(repoRoot, {
|
|
90
|
+
allowFailure: true,
|
|
91
|
+
binary: true,
|
|
92
|
+
noIndex: { left: nullPath, right: entry },
|
|
93
|
+
}),
|
|
93
94
|
);
|
|
94
|
-
return untrackedDiffs.filter(diff => diff
|
|
95
|
+
return untrackedDiffs.filter((diff): diff is string => !!diff?.trim()).join("\n");
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
async function captureRepoBaseline(repoRoot: string): Promise<RepoBaseline> {
|
|
@@ -427,6 +428,8 @@ export interface MergeBranchResult {
|
|
|
427
428
|
merged: string[];
|
|
428
429
|
failed: string[];
|
|
429
430
|
conflict?: string;
|
|
431
|
+
/** Set when cherry-picks landed on HEAD but restoring the stashed working tree failed. */
|
|
432
|
+
stashConflict?: string;
|
|
430
433
|
}
|
|
431
434
|
|
|
432
435
|
/**
|
|
@@ -438,64 +441,69 @@ export async function mergeTaskBranches(
|
|
|
438
441
|
repoRoot: string,
|
|
439
442
|
branches: Array<{ branchName: string; taskId: string; description?: string }>,
|
|
440
443
|
): Promise<MergeBranchResult> {
|
|
441
|
-
|
|
442
|
-
|
|
444
|
+
// Serialize against other in-process git mutations on this repo: concurrent
|
|
445
|
+
// background merges interleaving stash push/pop + cherry-pick would corrupt
|
|
446
|
+
// the working tree (lost uncommitted changes, mixed-up stash entries).
|
|
447
|
+
return git.withRepoLock(repoRoot, async () => {
|
|
448
|
+
const merged: string[] = [];
|
|
449
|
+
const failed: string[] = [];
|
|
443
450
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
451
|
+
// Stash dirty working tree so cherry-pick can operate on a clean HEAD.
|
|
452
|
+
// Without this, cherry-pick refuses to run when uncommitted changes exist.
|
|
453
|
+
const didStash = await git.stash.push(repoRoot, "omp-task-merge");
|
|
447
454
|
|
|
448
|
-
|
|
455
|
+
let conflictResult: MergeBranchResult | undefined;
|
|
449
456
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
try {
|
|
453
|
-
await git.cherryPick(repoRoot, branchName);
|
|
454
|
-
} catch (err) {
|
|
457
|
+
try {
|
|
458
|
+
for (const { branchName } of branches) {
|
|
455
459
|
try {
|
|
456
|
-
await git.cherryPick
|
|
457
|
-
} catch {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
failed
|
|
470
|
-
conflict: `${branchName}: ${stderr}`,
|
|
471
|
-
};
|
|
472
|
-
break;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
merged.push(branchName);
|
|
476
|
-
}
|
|
477
|
-
} finally {
|
|
478
|
-
if (didStash) {
|
|
479
|
-
try {
|
|
480
|
-
await git.stash.pop(repoRoot, { index: true });
|
|
481
|
-
} catch {
|
|
482
|
-
// Stash-pop conflicts mean the replayed changes clash with the user's
|
|
483
|
-
// uncommitted edits. Treat this as a merge failure so the caller preserves
|
|
484
|
-
// recovery branches instead of reporting success and deleting them.
|
|
485
|
-
logger.warn("Failed to restore stashed changes after task merge; stash entry preserved");
|
|
486
|
-
if (!conflictResult) {
|
|
460
|
+
await git.cherryPick(repoRoot, branchName);
|
|
461
|
+
} catch (err) {
|
|
462
|
+
try {
|
|
463
|
+
await git.cherryPick.abort(repoRoot);
|
|
464
|
+
} catch {
|
|
465
|
+
/* no state to abort */
|
|
466
|
+
}
|
|
467
|
+
const stderr =
|
|
468
|
+
err instanceof git.GitCommandError
|
|
469
|
+
? err.result.stderr.trim()
|
|
470
|
+
: err instanceof Error
|
|
471
|
+
? err.message
|
|
472
|
+
: String(err);
|
|
473
|
+
failed.push(branchName);
|
|
487
474
|
conflictResult = {
|
|
488
475
|
merged,
|
|
489
|
-
failed: merged,
|
|
490
|
-
conflict:
|
|
491
|
-
"stash pop: cherry-picked changes conflict with uncommitted edits. Run `git stash pop` and resolve manually.",
|
|
476
|
+
failed: [...failed, ...branches.slice(merged.length + failed.length).map(b => b.branchName)],
|
|
477
|
+
conflict: `${branchName}: ${stderr}`,
|
|
492
478
|
};
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
merged.push(branchName);
|
|
483
|
+
}
|
|
484
|
+
} finally {
|
|
485
|
+
if (didStash) {
|
|
486
|
+
try {
|
|
487
|
+
await git.stash.pop(repoRoot, { index: true });
|
|
488
|
+
} catch {
|
|
489
|
+
// Stash-pop conflicts mean the replayed changes clash with the user's
|
|
490
|
+
// uncommitted edits. The cherry-picked commits are already on HEAD, so
|
|
491
|
+
// the merged branches DID land — report them as merged and surface the
|
|
492
|
+
// stash conflict separately instead of claiming they are unmerged.
|
|
493
|
+
logger.warn("Failed to restore stashed changes after task merge; stash entry preserved");
|
|
494
|
+
const stashConflict =
|
|
495
|
+
"stash pop: cherry-picked changes conflict with uncommitted edits. The merged commits are on HEAD; run `git stash pop` and resolve manually.";
|
|
496
|
+
if (conflictResult) {
|
|
497
|
+
conflictResult.stashConflict = stashConflict;
|
|
498
|
+
} else {
|
|
499
|
+
conflictResult = { merged, failed: [], stashConflict };
|
|
500
|
+
}
|
|
493
501
|
}
|
|
494
502
|
}
|
|
495
503
|
}
|
|
496
|
-
}
|
|
497
504
|
|
|
498
|
-
|
|
505
|
+
return conflictResult ?? { merged, failed };
|
|
506
|
+
});
|
|
499
507
|
}
|
|
500
508
|
|
|
501
509
|
/** Clean up temporary task branches. */
|
package/src/thinking.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ResolvedThinkingLevel, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import { Effort, type Model, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { clampThinkingLevelForModel, getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Metadata used to render thinking selector values in the coding-agent UI.
|
package/src/tiny/title-client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { $env, isCompiledBinary, logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { $env, isBunTestRuntime, isCompiledBinary, logger, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type { Subprocess } from "bun";
|
|
4
4
|
import { settings } from "../config/settings";
|
|
5
5
|
import { tinyModelDeviceSettingToEnv } from "./device";
|
|
@@ -108,21 +108,32 @@ function tinyWorkerEnv(): Record<string, string> {
|
|
|
108
108
|
for (const key in overlay) merged[key] = overlay[key];
|
|
109
109
|
return merged;
|
|
110
110
|
}
|
|
111
|
+
interface TinyWorkerSpawnCommand {
|
|
112
|
+
cmd: string[];
|
|
113
|
+
cwd?: string;
|
|
114
|
+
}
|
|
111
115
|
|
|
112
116
|
/**
|
|
113
|
-
* Resolve the
|
|
114
|
-
* compiled binary the entry point is the binary itself
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
+
* Resolve the command used to relaunch the agent CLI into tiny-worker mode.
|
|
118
|
+
* In a compiled binary the entry point is the binary itself (no script arg).
|
|
119
|
+
* Otherwise re-enter the declared worker-host entry (source cli.ts or
|
|
120
|
+
* npm-bundle cli.js) with a cwd-relative script path — Bun's subprocess IPC
|
|
121
|
+
* is more reliable that way than with an absolute `.ts` entry under
|
|
122
|
+
* `bun test` — and fall back to this package's own `src/cli.ts` when no host
|
|
123
|
+
* entry is declared (bun test, SDK embedding).
|
|
117
124
|
*/
|
|
118
|
-
function tinyWorkerSpawnCmd():
|
|
119
|
-
if (isCompiledBinary()) return [process.execPath, TINY_WORKER_ARG];
|
|
120
|
-
const
|
|
121
|
-
|
|
125
|
+
function tinyWorkerSpawnCmd(): TinyWorkerSpawnCommand {
|
|
126
|
+
if (isCompiledBinary()) return { cmd: [process.execPath, TINY_WORKER_ARG] };
|
|
127
|
+
const hostEntry = workerHostEntry();
|
|
128
|
+
if (hostEntry) {
|
|
129
|
+
return { cmd: [process.execPath, path.basename(hostEntry), TINY_WORKER_ARG], cwd: path.dirname(hostEntry) };
|
|
130
|
+
}
|
|
131
|
+
const packageRoot = path.resolve(import.meta.dir, "..", "..");
|
|
132
|
+
return { cmd: [process.execPath, "src/cli.ts", TINY_WORKER_ARG], cwd: packageRoot };
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
interface SpawnedSubprocess {
|
|
125
|
-
proc: Subprocess<"ignore", "
|
|
136
|
+
proc: Subprocess<"ignore", "ignore", "ignore">;
|
|
126
137
|
inbound: Set<(message: TinyTitleWorkerOutbound) => void>;
|
|
127
138
|
errors: Set<(error: Error) => void>;
|
|
128
139
|
/**
|
|
@@ -143,14 +154,19 @@ export function createTinyTitleSubprocess(): SpawnedSubprocess {
|
|
|
143
154
|
const inbound = new Set<(message: TinyTitleWorkerOutbound) => void>();
|
|
144
155
|
const errors = new Set<(error: Error) => void>();
|
|
145
156
|
const intentionalExit = { value: false };
|
|
157
|
+
const spawnCommand = tinyWorkerSpawnCmd();
|
|
146
158
|
const proc = Bun.spawn({
|
|
147
|
-
cmd:
|
|
159
|
+
cmd: spawnCommand.cmd,
|
|
160
|
+
cwd: spawnCommand.cwd,
|
|
148
161
|
env: tinyWorkerEnv(),
|
|
149
162
|
stdin: "ignore",
|
|
150
|
-
stdout: "
|
|
151
|
-
stderr: "
|
|
163
|
+
stdout: "ignore",
|
|
164
|
+
stderr: "ignore",
|
|
152
165
|
serialization: "advanced",
|
|
153
166
|
windowsHide: true,
|
|
167
|
+
// The worker is an implementation detail of the interactive TUI. Native
|
|
168
|
+
// model runtimes may print progress or decoded text directly; never let
|
|
169
|
+
// those bytes inherit the terminal and corrupt the chat scrollback.
|
|
154
170
|
ipc(message) {
|
|
155
171
|
for (const handler of inbound) handler(message as TinyTitleWorkerOutbound);
|
|
156
172
|
},
|
|
@@ -172,7 +188,9 @@ export function createTinyTitleSubprocess(): SpawnedSubprocess {
|
|
|
172
188
|
});
|
|
173
189
|
// Don't keep the parent event loop alive on account of an idle worker; the
|
|
174
190
|
// agent dispose path calls `terminate()` explicitly when shutting down.
|
|
175
|
-
|
|
191
|
+
// Bun's test runner can starve IPC delivery for unref'd subprocesses, so
|
|
192
|
+
// keep it referenced only under tests that assert the ping/pong contract.
|
|
193
|
+
if (!isBunTestRuntime()) proc.unref();
|
|
176
194
|
return { proc, inbound, errors, intentionalExit };
|
|
177
195
|
}
|
|
178
196
|
|
|
@@ -6,6 +6,19 @@ import { inflateSync, strFromU8 } from "fflate";
|
|
|
6
6
|
import { formatBytes } from "./render-utils";
|
|
7
7
|
import { ToolError } from "./tool-errors";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Cap on the on-disk size of tar/tar.gz archives, which are loaded fully into
|
|
11
|
+
* memory (and decompressed by `Bun.Archive`) just to index entries. ZIP is
|
|
12
|
+
* exempt: it is read via ranged central-directory access.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_TAR_ARCHIVE_BYTES = 256 * 1024 * 1024;
|
|
15
|
+
/**
|
|
16
|
+
* Cap on a single archive member's declared (uncompressed) size. The declared
|
|
17
|
+
* size is attacker-controlled metadata — a crafted ZIP entry can claim
|
|
18
|
+
* multi-GB sizes that would be allocated up front before any data inflates.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_ARCHIVE_MEMBER_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
|
|
9
22
|
export type ArchiveFormat = "zip" | "tar" | "tar.gz";
|
|
10
23
|
|
|
11
24
|
export interface ArchivePathCandidate {
|
|
@@ -646,6 +659,11 @@ export class ArchiveReader {
|
|
|
646
659
|
if (!entry.storage) {
|
|
647
660
|
throw new ToolError(`Archive file '${normalizedPath}' has no readable storage`);
|
|
648
661
|
}
|
|
662
|
+
if (entry.size > MAX_ARCHIVE_MEMBER_BYTES) {
|
|
663
|
+
throw new ToolError(
|
|
664
|
+
`Archive member '${normalizedPath}' is too large to extract in memory (${formatBytes(entry.size)} > ${formatBytes(MAX_ARCHIVE_MEMBER_BYTES)} limit)`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
649
667
|
|
|
650
668
|
const bytes =
|
|
651
669
|
entry.storage.type === "tar"
|
|
@@ -668,8 +686,18 @@ export async function openArchive(filePath: string): Promise<ArchiveReader> {
|
|
|
668
686
|
throw new ToolError(`Unsupported archive format: ${filePath}`);
|
|
669
687
|
}
|
|
670
688
|
|
|
671
|
-
|
|
672
|
-
|
|
689
|
+
if (format === "zip") {
|
|
690
|
+
return new ArchiveReader(format, await readZipEntries(filePath));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const file = Bun.file(filePath);
|
|
694
|
+
const archiveSize = file.size;
|
|
695
|
+
if (archiveSize > MAX_TAR_ARCHIVE_BYTES) {
|
|
696
|
+
throw new ToolError(
|
|
697
|
+
`Archive is too large to read in memory (${formatBytes(archiveSize)} > ${formatBytes(MAX_TAR_ARCHIVE_BYTES)} limit)`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
const entries = await readTarEntries(await file.bytes());
|
|
673
701
|
return new ArchiveReader(format, entries);
|
|
674
702
|
}
|
|
675
703
|
|
package/src/tools/ask.ts
CHANGED
|
@@ -59,6 +59,8 @@ export interface QuestionResult {
|
|
|
59
59
|
multi: boolean;
|
|
60
60
|
selectedOptions: string[];
|
|
61
61
|
customInput?: string;
|
|
62
|
+
/** True when the answer was auto-selected because the dialog timed out. */
|
|
63
|
+
timedOut?: boolean;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export interface AskToolDetails {
|
|
@@ -67,6 +69,8 @@ export interface AskToolDetails {
|
|
|
67
69
|
multi?: boolean;
|
|
68
70
|
selectedOptions?: string[];
|
|
69
71
|
customInput?: string;
|
|
72
|
+
/** True when the answer was auto-selected because the dialog timed out. */
|
|
73
|
+
timedOut?: boolean;
|
|
70
74
|
/** Multi-part question mode */
|
|
71
75
|
results?: QuestionResult[];
|
|
72
76
|
}
|
|
@@ -94,6 +98,10 @@ function toSelectOption(option: AskOption, label = option.label): ExtensionUISel
|
|
|
94
98
|
|
|
95
99
|
const OTHER_OPTION = "Other (type your own)";
|
|
96
100
|
const RECOMMENDED_SUFFIX = " (Recommended)";
|
|
101
|
+
// Window after the timeout deadline within which an `undefined` selection is
|
|
102
|
+
// attributed to a UI-enforced timeout (for surfaces that close the dialog at
|
|
103
|
+
// the deadline but never invoke `onTimeout`). Cancels beyond it are user Esc.
|
|
104
|
+
const TIMEOUT_DETECTION_TOLERANCE_MS = 1_000;
|
|
97
105
|
|
|
98
106
|
function getDoneOptionLabel(): string {
|
|
99
107
|
return `${theme.symbol("tool.ask")} Done selecting`;
|
|
@@ -230,7 +238,12 @@ async function askSingleQuestion(
|
|
|
230
238
|
? await untilAborted(signal, () => ui.select(prompt, optionsToShow, dialogOptions))
|
|
231
239
|
: await ui.select(prompt, optionsToShow, dialogOptions);
|
|
232
240
|
if (!timeoutTriggered && choice === undefined && typeof timeout === "number") {
|
|
233
|
-
|
|
241
|
+
// Fallback for UI surfaces that enforce `timeout` without invoking
|
|
242
|
+
// `onTimeout`: their auto-cancel resolves right at the deadline. A
|
|
243
|
+
// cancel arriving well past the deadline is a deliberate user Esc on
|
|
244
|
+
// a surface that kept the dialog open — keep treating it as a cancel.
|
|
245
|
+
const elapsed = Date.now() - startMs;
|
|
246
|
+
timeoutTriggered = elapsed >= timeout && elapsed <= timeout + TIMEOUT_DETECTION_TOLERANCE_MS;
|
|
234
247
|
}
|
|
235
248
|
return { choice, timedOut: timeoutTriggered, navigation: navigationAction };
|
|
236
249
|
};
|
|
@@ -380,9 +393,10 @@ function formatQuestionResult(result: QuestionResult): string {
|
|
|
380
393
|
return `${result.id}: "${result.customInput}"`;
|
|
381
394
|
}
|
|
382
395
|
if (result.selectedOptions.length > 0) {
|
|
396
|
+
const suffix = result.timedOut ? " (auto-selected after timeout)" : "";
|
|
383
397
|
return result.multi
|
|
384
|
-
? `${result.id}: [${result.selectedOptions.join(", ")}]`
|
|
385
|
-
: `${result.id}: ${result.selectedOptions[0]}`;
|
|
398
|
+
? `${result.id}: [${result.selectedOptions.join(", ")}]${suffix}`
|
|
399
|
+
: `${result.id}: ${result.selectedOptions[0]}${suffix}`;
|
|
386
400
|
}
|
|
387
401
|
return `${result.id}: (cancelled)`;
|
|
388
402
|
}
|
|
@@ -519,13 +533,15 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
519
533
|
multi: q.multi ?? false,
|
|
520
534
|
selectedOptions,
|
|
521
535
|
customInput,
|
|
536
|
+
timedOut: timedOut || undefined,
|
|
522
537
|
};
|
|
523
538
|
|
|
524
539
|
const responseParts: string[] = [];
|
|
525
540
|
if (selectedOptions.length > 0) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
541
|
+
const selectedText = q.multi
|
|
542
|
+
? `User selected: ${selectedOptions.join(", ")}`
|
|
543
|
+
: `User selected: ${selectedOptions[0]}`;
|
|
544
|
+
responseParts.push(timedOut ? `${selectedText} (auto-selected after timeout)` : selectedText);
|
|
529
545
|
}
|
|
530
546
|
if (customInput !== undefined) {
|
|
531
547
|
responseParts.push(
|
|
@@ -573,6 +589,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
573
589
|
multi: q.multi ?? false,
|
|
574
590
|
selectedOptions,
|
|
575
591
|
customInput,
|
|
592
|
+
timedOut: timedOut || undefined,
|
|
576
593
|
};
|
|
577
594
|
|
|
578
595
|
if (navAction === "back") {
|
|
@@ -624,6 +641,56 @@ interface AskRenderArgs {
|
|
|
624
641
|
}>;
|
|
625
642
|
}
|
|
626
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Coerce an untrusted option list (streamed or model-mangled call args) into
|
|
646
|
+
* well-formed render options. Bare strings become labels; entries without a
|
|
647
|
+
* string label are dropped.
|
|
648
|
+
*/
|
|
649
|
+
function normalizeRenderOptions(raw: unknown): AskRenderOption[] | undefined {
|
|
650
|
+
if (!Array.isArray(raw)) return undefined;
|
|
651
|
+
const out: AskRenderOption[] = [];
|
|
652
|
+
for (const entry of raw) {
|
|
653
|
+
if (typeof entry === "string") {
|
|
654
|
+
out.push({ label: entry });
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (!entry || typeof entry !== "object") continue;
|
|
658
|
+
const { label, description } = entry as Partial<AskRenderOption>;
|
|
659
|
+
if (typeof label !== "string") continue;
|
|
660
|
+
out.push(typeof description === "string" ? { label, description } : { label });
|
|
661
|
+
}
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Coerce untrusted `questions` call args into a renderable array. Models
|
|
667
|
+
* occasionally double-encode the array as a JSON string — a bare string passes
|
|
668
|
+
* a truthy `.length` check but has no `.map`, which used to crash the TUI
|
|
669
|
+
* render loop. Partially streamed args can also be missing fields.
|
|
670
|
+
*/
|
|
671
|
+
function normalizeRenderQuestions(raw: unknown): NonNullable<AskRenderArgs["questions"]> | undefined {
|
|
672
|
+
if (typeof raw === "string") {
|
|
673
|
+
try {
|
|
674
|
+
raw = JSON.parse(raw);
|
|
675
|
+
} catch {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (!Array.isArray(raw)) return undefined;
|
|
680
|
+
const out: NonNullable<AskRenderArgs["questions"]> = [];
|
|
681
|
+
for (const entry of raw) {
|
|
682
|
+
if (!entry || typeof entry !== "object") continue;
|
|
683
|
+
const q = entry as Partial<NonNullable<AskRenderArgs["questions"]>[number]>;
|
|
684
|
+
out.push({
|
|
685
|
+
id: typeof q.id === "string" ? q.id : "?",
|
|
686
|
+
question: typeof q.question === "string" ? q.question : "",
|
|
687
|
+
options: normalizeRenderOptions(q.options) ?? [],
|
|
688
|
+
multi: q.multi === true,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
|
|
627
694
|
/** Render a custom free-text answer as a status line plus indented continuation rows. */
|
|
628
695
|
function renderCustomInputLines(uiTheme: Theme, customInput: string): string[] {
|
|
629
696
|
const lines = customInput.split("\n");
|
|
@@ -707,8 +774,10 @@ export const askToolRenderer = {
|
|
|
707
774
|
new Markdown(text, 1, 0, mdTheme, accentStyle).render(Math.max(1, width - 3 + 1));
|
|
708
775
|
|
|
709
776
|
// Multi-part questions: one divider-labelled section per question.
|
|
710
|
-
|
|
711
|
-
|
|
777
|
+
// Call args are untrusted (partially streamed or model-mangled) and a
|
|
778
|
+
// throw here takes down the whole TUI render loop — normalize first.
|
|
779
|
+
const questions = normalizeRenderQuestions(args.questions);
|
|
780
|
+
if (questions && questions.length > 0) {
|
|
712
781
|
const header = `${label} ${uiTheme.fg("muted", `${questions.length} questions`)}`;
|
|
713
782
|
return framedBlock(uiTheme, width => {
|
|
714
783
|
const sections = questions.map(q => {
|
|
@@ -716,8 +785,11 @@ export const askToolRenderer = {
|
|
|
716
785
|
if (q.multi) meta.push("multi");
|
|
717
786
|
if (q.options?.length) meta.push(`options:${q.options.length}`);
|
|
718
787
|
const metaStr = meta.length > 0 ? uiTheme.fg("dim", ` · ${meta.join(" · ")}`) : "";
|
|
719
|
-
|
|
720
|
-
|
|
788
|
+
// md() returns a shared cached array (module-level Markdown LRU) — copy before appending.
|
|
789
|
+
const mdLines = md(q.question, width);
|
|
790
|
+
const lines = q.options?.length
|
|
791
|
+
? [...mdLines, ...renderQuestionOptionLines(uiTheme, mdTheme, q.options, q.multi)]
|
|
792
|
+
: mdLines;
|
|
721
793
|
return { label: `${uiTheme.fg("dim", `[${q.id}]`)}${metaStr}`, lines };
|
|
722
794
|
});
|
|
723
795
|
return { header, sections, state: "pending", borderColor: "borderMuted", width };
|
|
@@ -725,7 +797,7 @@ export const askToolRenderer = {
|
|
|
725
797
|
}
|
|
726
798
|
|
|
727
799
|
// Single question
|
|
728
|
-
if (!args.question) {
|
|
800
|
+
if (typeof args.question !== "string" || !args.question) {
|
|
729
801
|
const errorLine = formatErrorMessage("No question provided", uiTheme);
|
|
730
802
|
return framedBlock(uiTheme, width => ({
|
|
731
803
|
header: errorLine,
|
|
@@ -739,14 +811,16 @@ export const askToolRenderer = {
|
|
|
739
811
|
const question = args.question;
|
|
740
812
|
const meta: string[] = [];
|
|
741
813
|
if (args.multi) meta.push("multi");
|
|
742
|
-
|
|
814
|
+
const questionOptions = normalizeRenderOptions(args.options);
|
|
815
|
+
if (questionOptions?.length) meta.push(`options:${questionOptions.length}`);
|
|
743
816
|
const header = `${label}${formatMeta(meta, uiTheme)}`;
|
|
744
|
-
const questionOptions = args.options;
|
|
745
817
|
const multi = args.multi;
|
|
746
818
|
return framedBlock(uiTheme, width => {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
819
|
+
// md() returns a shared cached array (module-level Markdown LRU) — copy before appending.
|
|
820
|
+
const mdLines = md(question, width);
|
|
821
|
+
const bodyLines = questionOptions?.length
|
|
822
|
+
? [...mdLines, ...renderQuestionOptionLines(uiTheme, mdTheme, questionOptions, multi)]
|
|
823
|
+
: mdLines;
|
|
750
824
|
return {
|
|
751
825
|
header,
|
|
752
826
|
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
|
@@ -792,10 +866,11 @@ export const askToolRenderer = {
|
|
|
792
866
|
);
|
|
793
867
|
return framedBlock(uiTheme, width => {
|
|
794
868
|
const sections = results.map(r => {
|
|
795
|
-
|
|
796
|
-
lines
|
|
869
|
+
// md() returns a shared cached array (module-level Markdown LRU) — copy before appending.
|
|
870
|
+
const lines = [
|
|
871
|
+
...md(r.question, width),
|
|
797
872
|
...renderAnswerOptionLines(uiTheme, mdTheme, r.options, r.selectedOptions, r.multi, r.customInput),
|
|
798
|
-
|
|
873
|
+
];
|
|
799
874
|
return { label: uiTheme.fg("dim", `[${r.id}]`), lines };
|
|
800
875
|
});
|
|
801
876
|
return {
|
|
@@ -828,9 +903,17 @@ export const askToolRenderer = {
|
|
|
828
903
|
const dSelected = details.selectedOptions;
|
|
829
904
|
const dMulti = details.multi;
|
|
830
905
|
const dCustom = details.customInput;
|
|
906
|
+
const dTimedOut = details.timedOut;
|
|
831
907
|
return framedBlock(uiTheme, width => {
|
|
832
|
-
|
|
833
|
-
bodyLines
|
|
908
|
+
// md() returns a shared cached array (module-level Markdown LRU) — copy before appending.
|
|
909
|
+
const bodyLines = [
|
|
910
|
+
...md(question, width),
|
|
911
|
+
...renderAnswerOptionLines(uiTheme, mdTheme, dOptions, dSelected, dMulti, dCustom),
|
|
912
|
+
];
|
|
913
|
+
if (dTimedOut) {
|
|
914
|
+
// Distinguish auto-selection from a real user choice in the transcript.
|
|
915
|
+
bodyLines.push(uiTheme.fg("dim", "auto-selected after timeout — not a user choice"));
|
|
916
|
+
}
|
|
834
917
|
return {
|
|
835
918
|
header,
|
|
836
919
|
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { replaceTabs, Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import * as z from "zod/v4";
|
|
9
|
-
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
9
|
+
import { canonicalSnapshotKey, getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
10
10
|
import { normalizeToLF } from "../edit/normalize";
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
12
|
import type { Theme } from "../modes/theme/theme";
|
|
@@ -295,7 +295,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
295
295
|
const absolutePath = path.resolve(this.session.cwd, relativePath);
|
|
296
296
|
try {
|
|
297
297
|
const fullText = normalizeToLF(await Bun.file(absolutePath).text());
|
|
298
|
-
const tag = snapshotStore.record(absolutePath, fullText);
|
|
298
|
+
const tag = snapshotStore.record(canonicalSnapshotKey(absolutePath), fullText);
|
|
299
299
|
hashContexts.set(relativePath, { tag });
|
|
300
300
|
} catch {
|
|
301
301
|
// Best-effort: if a file disappears between ast-edit and rendering, emit plain line output.
|
|
@@ -402,6 +402,23 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
402
402
|
for (const change of applyResult.changes) {
|
|
403
403
|
recordAppliedFile(formatPath(change.path));
|
|
404
404
|
}
|
|
405
|
+
// The preview minted tags from pre-apply content; the rewrite just
|
|
406
|
+
// invalidated them. Re-record post-apply snapshots (canonical keys)
|
|
407
|
+
// so the model's next hashline edit anchors against fresh tags.
|
|
408
|
+
const freshTagLines: string[] = [];
|
|
409
|
+
if (useHashLines) {
|
|
410
|
+
const snapshotStore = getFileSnapshotStore(this.session);
|
|
411
|
+
for (const relativePath of appliedFileList) {
|
|
412
|
+
const appliedAbsolutePath = path.resolve(this.session.cwd, relativePath);
|
|
413
|
+
try {
|
|
414
|
+
const fullText = normalizeToLF(await Bun.file(appliedAbsolutePath).text());
|
|
415
|
+
const freshTag = snapshotStore.record(canonicalSnapshotKey(appliedAbsolutePath), fullText);
|
|
416
|
+
freshTagLines.push(formatHashlineHeader(relativePath, freshTag));
|
|
417
|
+
} catch {
|
|
418
|
+
// File disappeared between apply and re-read; skip its tag.
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
405
422
|
const appliedFileReplacements = appliedFileList.map(filePath => ({
|
|
406
423
|
path: filePath,
|
|
407
424
|
count: appliedFileReplacementCounts.get(filePath) ?? 0,
|
|
@@ -429,17 +446,20 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
429
446
|
filePath => fileReplacementCounts.get(filePath) !== appliedFileReplacementCounts.get(filePath),
|
|
430
447
|
);
|
|
431
448
|
if (stalePreview) {
|
|
432
|
-
const
|
|
449
|
+
const staleText =
|
|
433
450
|
applyResult.totalReplacements === 0
|
|
434
451
|
? `Preview is stale / no longer matches; no replacements were applied. Preview expected ${result.totalReplacements} replacement${previewReplacementPlural} in ${result.filesTouched} file${previewFilePlural}.`
|
|
435
452
|
: applyResult.totalReplacements < result.totalReplacements
|
|
436
453
|
? `Preview is stale / no longer matches; only ${applyResult.totalReplacements} of ${result.totalReplacements} replacements were applied in ${applyResult.filesTouched} of ${result.filesTouched} files.`
|
|
437
454
|
: `Preview is stale / no longer matches; applied ${applyResult.totalReplacements} replacements but preview expected ${result.totalReplacements}.`;
|
|
438
|
-
|
|
455
|
+
const staleWithTags =
|
|
456
|
+
freshTagLines.length > 0 ? `${staleText}\n${freshTagLines.join("\n")}` : staleText;
|
|
457
|
+
return { ...toolResult(appliedDetails).text(staleWithTags).done(), isError: true };
|
|
439
458
|
}
|
|
440
459
|
const appliedReplacementPlural = applyResult.totalReplacements !== 1 ? "s" : "";
|
|
441
460
|
const appliedFilePlural = applyResult.filesTouched !== 1 ? "s" : "";
|
|
442
|
-
const
|
|
461
|
+
const appliedText = `Applied ${applyResult.totalReplacements} replacement${appliedReplacementPlural} in ${applyResult.filesTouched} file${appliedFilePlural}.`;
|
|
462
|
+
const text = freshTagLines.length > 0 ? `${appliedText}\n${freshTagLines.join("\n")}` : appliedText;
|
|
443
463
|
return toolResult(appliedDetails).text(text).done();
|
|
444
464
|
},
|
|
445
465
|
});
|