@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.2
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 +142 -1
- package/dist/types/cli/dry-balance-cli.d.ts +15 -1
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/commit/analysis/conventional.d.ts +2 -2
- package/dist/types/commit/analysis/summary.d.ts +2 -2
- package/dist/types/commit/changelog/generate.d.ts +2 -2
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/index.d.ts +3 -3
- package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
- package/dist/types/commit/model-selection.d.ts +10 -4
- package/dist/types/config/api-key-resolver.d.ts +34 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +17 -1
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/dap/config.d.ts +14 -1
- package/dist/types/dap/types.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/utils.d.ts +3 -2
- package/dist/types/main.d.ts +3 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/chat-block.d.ts +64 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -1
- package/dist/types/modes/components/overlay-box.d.ts +17 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
- package/dist/types/modes/components/plan-toc.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +11 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +17 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
- package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -5
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +21 -5
- package/dist/types/modes/utils/copy-targets.d.ts +21 -1
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/plan-mode/approved-plan.d.ts +27 -8
- package/dist/types/plan-mode/plan-protection.d.ts +4 -4
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/messages.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +8 -3
- package/dist/types/slash-commands/types.d.ts +4 -6
- package/dist/types/task/executor.d.ts +17 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/render.d.ts +3 -2
- package/dist/types/tools/archive-reader.d.ts +5 -0
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/find.d.ts +8 -4
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/grouped-file-output.d.ts +95 -12
- package/dist/types/tools/memory-render.d.ts +4 -1
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/plan-mode-guard.d.ts +8 -9
- package/dist/types/tools/render-utils.d.ts +5 -9
- package/dist/types/tools/search.d.ts +6 -2
- package/dist/types/tools/sqlite-reader.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +3 -2
- package/dist/types/tools/write.d.ts +3 -0
- package/dist/types/tools/yield.d.ts +8 -0
- package/dist/types/tui/output-block.d.ts +16 -4
- package/dist/types/tui/status-line.d.ts +3 -0
- package/dist/types/utils/enhanced-paste.d.ts +20 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -1
- package/package.json +9 -9
- package/src/auto-thinking/classifier.ts +5 -1
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +54 -21
- package/src/cli/gallery-cli.ts +4 -1
- package/src/cli/gallery-fixtures/misc.ts +29 -0
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/analysis/conventional.ts +2 -2
- package/src/commit/analysis/summary.ts +2 -2
- package/src/commit/changelog/generate.ts +2 -2
- package/src/commit/changelog/index.ts +2 -2
- package/src/commit/map-reduce/index.ts +3 -3
- package/src/commit/map-reduce/map-phase.ts +2 -2
- package/src/commit/map-reduce/reduce-phase.ts +2 -2
- package/src/commit/model-selection.ts +36 -11
- package/src/commit/pipeline.ts +4 -4
- package/src/config/api-key-resolver.ts +58 -0
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +29 -24
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings-schema.ts +10 -0
- package/src/config/settings.ts +106 -43
- package/src/dap/config.ts +41 -2
- package/src/dap/defaults.json +1 -0
- package/src/dap/session.ts +1 -0
- package/src/dap/types.ts +10 -0
- package/src/debug/index.ts +47 -53
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/renderer.ts +82 -78
- package/src/eval/__tests__/llm-bridge.test.ts +110 -31
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/llm-bridge.ts +22 -6
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/goals/tools/goal-tool.ts +36 -26
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +61 -9
- package/src/lsp/utils.ts +3 -2
- package/src/main.ts +100 -72
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +14 -7
- package/src/mnemopi/backend.ts +5 -1
- package/src/modes/acp/acp-agent.ts +33 -26
- package/src/modes/components/assistant-message.ts +2 -9
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/copy-selector.ts +1 -44
- package/src/modes/components/custom-editor.ts +164 -109
- package/src/modes/components/custom-message.ts +1 -3
- package/src/modes/components/execution-shared.ts +1 -2
- package/src/modes/components/hook-message.ts +1 -3
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/overlay-box.ts +108 -0
- package/src/modes/components/plan-review-overlay.ts +799 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/read-tool-group.ts +20 -4
- package/src/modes/components/skill-message.ts +0 -1
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/todo-reminder.ts +0 -2
- package/src/modes/components/tool-execution.ts +68 -88
- package/src/modes/components/transcript-container.ts +84 -24
- package/src/modes/components/user-message.ts +2 -3
- package/src/modes/controllers/command-controller-shared.ts +7 -6
- package/src/modes/controllers/command-controller.ts +57 -55
- package/src/modes/controllers/event-controller.ts +67 -40
- package/src/modes/controllers/extension-ui-controller.ts +10 -73
- package/src/modes/controllers/input-controller.ts +170 -126
- package/src/modes/controllers/mcp-command-controller.ts +69 -60
- package/src/modes/controllers/selector-controller.ts +23 -25
- package/src/modes/controllers/streaming-reveal.ts +212 -0
- package/src/modes/controllers/tan-command-controller.ts +173 -0
- package/src/modes/interactive-mode.ts +274 -112
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/theme/theme-schema.json +1 -1
- package/src/modes/theme/theme.ts +8 -4
- package/src/modes/types.ts +21 -7
- package/src/modes/utils/copy-targets.ts +133 -27
- package/src/modes/utils/ui-helpers.ts +44 -46
- package/src/modes/workflow.ts +10 -10
- package/src/plan-mode/approved-plan.ts +66 -43
- package/src/plan-mode/plan-protection.ts +4 -4
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/plan-mode-active.md +67 -58
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +2 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +37 -46
- package/src/session/agent-session.ts +119 -18
- package/src/session/auth-storage.ts +2 -0
- package/src/session/messages.ts +26 -0
- package/src/session/session-manager.ts +109 -28
- package/src/slash-commands/builtin-registry.ts +36 -9
- package/src/slash-commands/types.ts +4 -6
- package/src/task/executor.ts +76 -38
- package/src/task/index.ts +4 -0
- package/src/task/render.ts +211 -147
- package/src/tools/archive-reader.ts +64 -0
- package/src/tools/ask.ts +119 -164
- package/src/tools/ast-edit.ts +98 -71
- package/src/tools/ast-grep.ts +37 -43
- package/src/tools/bash.ts +57 -6
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/debug.ts +20 -8
- package/src/tools/eval.ts +13 -2
- package/src/tools/fetch.ts +297 -7
- package/src/tools/find.ts +51 -30
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/gh-renderer.ts +81 -42
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/grouped-file-output.ts +272 -48
- package/src/tools/image-gen.ts +150 -103
- package/src/tools/inspect-image-renderer.ts +63 -41
- package/src/tools/inspect-image.ts +10 -3
- package/src/tools/job.ts +3 -4
- package/src/tools/memory-render.ts +4 -1
- package/src/tools/path-utils.ts +28 -2
- package/src/tools/plan-mode-guard.ts +66 -39
- package/src/tools/read.ts +48 -28
- package/src/tools/render-utils.ts +21 -37
- package/src/tools/resolve.ts +14 -0
- package/src/tools/search-tool-bm25.ts +36 -23
- package/src/tools/search.ts +118 -81
- package/src/tools/sqlite-reader.ts +9 -12
- package/src/tools/todo.ts +118 -52
- package/src/tools/write.ts +83 -64
- package/src/tools/yield.ts +10 -1
- package/src/tui/output-block.ts +60 -13
- package/src/tui/status-line.ts +5 -1
- package/src/utils/commit-message-generator.ts +11 -3
- package/src/utils/enhanced-paste.ts +230 -0
- package/src/utils/title-generator.ts +2 -1
- package/src/web/search/providers/anthropic.ts +25 -19
- package/src/web/search/providers/codex.ts +37 -8
- package/src/web/search/providers/exa.ts +11 -3
- package/src/web/search/providers/kimi.ts +28 -17
- package/src/web/search/providers/parallel.ts +35 -24
- package/src/web/search/providers/synthetic.ts +8 -6
- package/src/web/search/providers/tavily.ts +9 -8
- package/src/web/search/providers/zai.ts +8 -6
package/src/tools/todo.ts
CHANGED
|
@@ -9,8 +9,8 @@ import type { Theme } from "../modes/theme/theme";
|
|
|
9
9
|
import todoDescription from "../prompts/tools/todo.md" with { type: "text" };
|
|
10
10
|
import type { ToolSession } from "../sdk";
|
|
11
11
|
import type { SessionEntry } from "../session/session-manager";
|
|
12
|
-
import { renderStatusLine, renderTreeList } from "../tui";
|
|
13
|
-
import { PREVIEW_LIMITS } from "./render-utils";
|
|
12
|
+
import { framedBlock, renderStatusLine, renderTreeList } from "../tui";
|
|
13
|
+
import { formatErrorDetail, PREVIEW_LIMITS } from "./render-utils";
|
|
14
14
|
|
|
15
15
|
// =============================================================================
|
|
16
16
|
// Types
|
|
@@ -755,19 +755,17 @@ function formatTodoLine(
|
|
|
755
755
|
}
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
-
function renderNoteAttachments(phases: TodoPhase[], uiTheme: Theme): string[] {
|
|
758
|
+
function renderNoteAttachments(phases: TodoPhase[], uiTheme: Theme, indent: string): string[] {
|
|
759
759
|
const lines: string[] = [];
|
|
760
760
|
for (const phase of phases) {
|
|
761
761
|
for (const task of phase.tasks) {
|
|
762
762
|
if (task.status !== "in_progress" || !task.notes || task.notes.length === 0) continue;
|
|
763
|
-
const bar = uiTheme.fg("dim", uiTheme.tree.vertical);
|
|
764
|
-
const title = uiTheme.fg("dim", chalk.italic(`§ notes — ${task.content}`));
|
|
765
763
|
lines.push("");
|
|
766
|
-
lines.push(
|
|
764
|
+
lines.push(`${indent}${uiTheme.fg("dim", chalk.italic(`§ notes — ${task.content}`))}`);
|
|
767
765
|
for (let j = 0; j < task.notes.length; j++) {
|
|
768
|
-
if (j > 0) lines.push(
|
|
766
|
+
if (j > 0) lines.push("");
|
|
769
767
|
for (const noteLine of task.notes[j].split("\n")) {
|
|
770
|
-
lines.push(
|
|
768
|
+
lines.push(`${indent} ${uiTheme.fg("dim", noteLine)}`);
|
|
771
769
|
}
|
|
772
770
|
}
|
|
773
771
|
}
|
|
@@ -775,8 +773,55 @@ function renderNoteAttachments(phases: TodoPhase[], uiTheme: Theme): string[] {
|
|
|
775
773
|
return lines;
|
|
776
774
|
}
|
|
777
775
|
|
|
776
|
+
/**
|
|
777
|
+
* Phases the latest update touched, plus the active (in_progress) phase.
|
|
778
|
+
* Returns `null` when there is no usable signal, meaning "render every phase
|
|
779
|
+
* fully" — this preserves the legacy view and the manual-expand path.
|
|
780
|
+
*/
|
|
781
|
+
function computeTouchedPhases(
|
|
782
|
+
args: TodoRenderArgs | undefined,
|
|
783
|
+
phases: TodoPhase[],
|
|
784
|
+
completedTasks: TodoCompletionTransition[],
|
|
785
|
+
): Set<string> | null {
|
|
786
|
+
const touched = new Set<string>();
|
|
787
|
+
// The phase holding the in_progress task is where attention sits after the
|
|
788
|
+
// auto-promotion that follows every completion.
|
|
789
|
+
for (const phase of phases) {
|
|
790
|
+
if (phase.tasks.some(task => task.status === "in_progress")) touched.add(phase.name);
|
|
791
|
+
}
|
|
792
|
+
// Phases with a task that just transitioned to completed in this update.
|
|
793
|
+
for (const transition of completedTasks) touched.add(transition.phase);
|
|
794
|
+
// Phases explicitly named by the ops that ran. `init` replaces the whole
|
|
795
|
+
// list, so the entire plan is fresh and every phase counts as touched.
|
|
796
|
+
const ops = Array.isArray(args?.ops) ? args.ops : [];
|
|
797
|
+
for (const op of ops) {
|
|
798
|
+
if (!op || typeof op !== "object") continue;
|
|
799
|
+
if (op.op === "init") {
|
|
800
|
+
for (const phase of phases) touched.add(phase.name);
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
if (typeof op.phase === "string" && op.phase) {
|
|
804
|
+
const named = phases.find(phase => phase.name === op.phase);
|
|
805
|
+
if (named) touched.add(named.name);
|
|
806
|
+
}
|
|
807
|
+
if (typeof op.task === "string" && op.task) {
|
|
808
|
+
const located = findTaskByContent(phases, op.task);
|
|
809
|
+
if (located) touched.add(located.phase.name);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return touched.size > 0 ? touched : null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** One-line summary for a collapsed (untouched) phase: dim header + progress. */
|
|
816
|
+
function formatPhaseSummary(phase: TodoPhase, oneBasedIndex: number, uiTheme: Theme): string {
|
|
817
|
+
const total = phase.tasks.length;
|
|
818
|
+
const done = phase.tasks.filter(task => task.status === "completed").length;
|
|
819
|
+
const name = uiTheme.fg("dim", chalk.bold(formatPhaseDisplayName(phase.name, oneBasedIndex)));
|
|
820
|
+
return `${name}${uiTheme.fg("dim", ` ${done}/${total}`)}`;
|
|
821
|
+
}
|
|
822
|
+
|
|
778
823
|
export const todoToolRenderer = {
|
|
779
|
-
renderCall(args: TodoRenderArgs,
|
|
824
|
+
renderCall(args: TodoRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
780
825
|
// `args` here is the raw partially-parsed JSON from the streaming
|
|
781
826
|
// tool-call delta and may not satisfy `TodoRenderArgs` at runtime:
|
|
782
827
|
// `parseStreamingJson` can hand back `{ ops: "[" }` mid-delta, or
|
|
@@ -797,16 +842,33 @@ export const todoToolRenderer = {
|
|
|
797
842
|
}
|
|
798
843
|
return parts.join(" ");
|
|
799
844
|
});
|
|
800
|
-
|
|
801
|
-
|
|
845
|
+
// No body worth boxing while the call streams — a lone status line reads
|
|
846
|
+
// cleaner than an empty frame. The container renders it without chrome.
|
|
847
|
+
const header = renderStatusLine(
|
|
848
|
+
{ icon: "pending", spinnerFrame: options?.spinnerFrame, title: "Todo", meta: ops },
|
|
849
|
+
uiTheme,
|
|
850
|
+
);
|
|
851
|
+
return new Text(header, 0, 0);
|
|
802
852
|
},
|
|
803
853
|
|
|
804
854
|
renderResult(
|
|
805
|
-
result: { content: Array<{ type: string; text?: string }>; details?: TodoToolDetails },
|
|
855
|
+
result: { content: Array<{ type: string; text?: string }>; details?: TodoToolDetails; isError?: boolean },
|
|
806
856
|
options: RenderResultOptions,
|
|
807
857
|
uiTheme: Theme,
|
|
808
|
-
|
|
858
|
+
args?: TodoRenderArgs,
|
|
809
859
|
): Component {
|
|
860
|
+
if (result.isError) {
|
|
861
|
+
const errorText = result.content?.find(content => content.type === "text")?.text ?? "Todo operation failed";
|
|
862
|
+
const header = renderStatusLine({ icon: "error", title: "Todo" }, uiTheme);
|
|
863
|
+
return framedBlock(uiTheme, width => ({
|
|
864
|
+
header,
|
|
865
|
+
sections: [{ lines: formatErrorDetail(errorText, uiTheme).split("\n") }],
|
|
866
|
+
state: "error",
|
|
867
|
+
borderColor: "error",
|
|
868
|
+
width,
|
|
869
|
+
}));
|
|
870
|
+
}
|
|
871
|
+
|
|
810
872
|
const phases = (result.details?.phases ?? []).filter(phase => phase.tasks.length > 0);
|
|
811
873
|
const completedTasks = result.details?.completedTasks ?? [];
|
|
812
874
|
const completionKeysByPhase = new Map<string, Set<string>>();
|
|
@@ -822,48 +884,52 @@ export const todoToolRenderer = {
|
|
|
822
884
|
const header = renderStatusLine({ icon: "success", title: "Todo", meta: [`${allTasks.length} tasks`] }, uiTheme);
|
|
823
885
|
if (allTasks.length === 0) {
|
|
824
886
|
const fallback = result.content?.find(content => content.type === "text")?.text ?? "No todos";
|
|
825
|
-
return new Text(`${header}\n${uiTheme.fg("dim", fallback)}`, 0, 0);
|
|
887
|
+
return new Text(`${header}\n ${uiTheme.fg("dim", fallback)}`, 0, 0);
|
|
826
888
|
}
|
|
827
889
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const phase = phases[p];
|
|
843
|
-
if (phases.length > 1) {
|
|
844
|
-
lines.push(uiTheme.fg("accent", chalk.bold(` ${formatPhaseDisplayName(phase.name, p + 1)}`)));
|
|
845
|
-
}
|
|
846
|
-
const completionKeys = completionKeysByPhase.get(phase.name) ?? EMPTY_COMPLETION_KEYS;
|
|
847
|
-
const treeLines = renderTreeList(
|
|
848
|
-
{
|
|
849
|
-
items: phase.tasks,
|
|
850
|
-
expanded,
|
|
851
|
-
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
852
|
-
itemType: "todo",
|
|
853
|
-
renderItem: todo => formatTodoLine(todo, uiTheme, "", completionKeys, spinnerFrame),
|
|
854
|
-
},
|
|
855
|
-
uiTheme,
|
|
856
|
-
);
|
|
857
|
-
for (const line of treeLines) {
|
|
858
|
-
lines.push(` ${line}`);
|
|
859
|
-
}
|
|
890
|
+
return framedBlock(uiTheme, width => {
|
|
891
|
+
const { expanded, spinnerFrame } = options;
|
|
892
|
+
const multiPhase = phases.length > 1;
|
|
893
|
+
const indent = multiPhase ? " " : "";
|
|
894
|
+
// Collapse phases this update didn't touch down to a one-line summary so
|
|
895
|
+
// a single task flip doesn't redraw every phase's full task list. The
|
|
896
|
+
// manual expand toggle (and the no-signal fallback) still shows all.
|
|
897
|
+
const touched = expanded || !multiPhase ? null : computeTouchedPhases(args, phases, completedTasks);
|
|
898
|
+
const bodyLines: string[] = [];
|
|
899
|
+
for (let p = 0; p < phases.length; p++) {
|
|
900
|
+
const phase = phases[p];
|
|
901
|
+
if (touched && !touched.has(phase.name)) {
|
|
902
|
+
bodyLines.push(formatPhaseSummary(phase, p + 1, uiTheme));
|
|
903
|
+
continue;
|
|
860
904
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
905
|
+
if (multiPhase) {
|
|
906
|
+
bodyLines.push(uiTheme.fg("accent", chalk.bold(formatPhaseDisplayName(phase.name, p + 1))));
|
|
907
|
+
}
|
|
908
|
+
const completionKeys = completionKeysByPhase.get(phase.name) ?? EMPTY_COMPLETION_KEYS;
|
|
909
|
+
const treeLines = renderTreeList(
|
|
910
|
+
{
|
|
911
|
+
items: phase.tasks,
|
|
912
|
+
expanded,
|
|
913
|
+
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
914
|
+
itemType: "todo",
|
|
915
|
+
renderItem: todo => formatTodoLine(todo, uiTheme, "", completionKeys, spinnerFrame),
|
|
916
|
+
},
|
|
917
|
+
uiTheme,
|
|
918
|
+
);
|
|
919
|
+
for (const line of treeLines) {
|
|
920
|
+
bodyLines.push(`${indent}${line}`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
bodyLines.push(...renderNoteAttachments(phases, uiTheme, indent));
|
|
924
|
+
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
925
|
+
return {
|
|
926
|
+
header,
|
|
927
|
+
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
|
928
|
+
state: options.isPartial ? "pending" : "success",
|
|
929
|
+
borderColor: "borderMuted",
|
|
930
|
+
width,
|
|
931
|
+
};
|
|
932
|
+
});
|
|
867
933
|
},
|
|
868
934
|
mergeCallAndResult: true,
|
|
869
935
|
};
|
package/src/tools/write.ts
CHANGED
|
@@ -5,11 +5,10 @@ import * as path from "node:path";
|
|
|
5
5
|
import { formatHashlineHeader, stripHashlinePrefixes } from "@oh-my-pi/hashline";
|
|
6
6
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
7
7
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
8
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
8
|
import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
9
|
import * as z from "zod/v4";
|
|
11
10
|
|
|
12
|
-
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
11
|
+
import { canonicalSnapshotKey, getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
13
12
|
import { normalizeToLF } from "../edit/normalize";
|
|
14
13
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
15
14
|
import { InternalUrlRouter } from "../internal-urls";
|
|
@@ -19,7 +18,7 @@ import { getDiagnosticsLedger } from "../lsp/diagnostics-ledger";
|
|
|
19
18
|
import { getLanguageFromPath, highlightCode, type Theme } from "../modes/theme/theme";
|
|
20
19
|
import writeDescription from "../prompts/tools/write.md" with { type: "text" };
|
|
21
20
|
import type { ToolSession } from "../sdk";
|
|
22
|
-
import {
|
|
21
|
+
import { fileHyperlink, framedBlock, renderStatusLine } from "../tui";
|
|
23
22
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
24
23
|
import { truncateForPrompt } from "./approval";
|
|
25
24
|
import { parseArchivePathCandidates } from "./archive-reader";
|
|
@@ -40,8 +39,6 @@ import {
|
|
|
40
39
|
formatErrorDetail,
|
|
41
40
|
formatExpandHint,
|
|
42
41
|
formatMoreItems,
|
|
43
|
-
formatStatusIcon,
|
|
44
|
-
formatTitle,
|
|
45
42
|
getLspBatchRequest,
|
|
46
43
|
replaceTabs,
|
|
47
44
|
shortenPath,
|
|
@@ -80,6 +77,9 @@ export interface WriteToolDetails {
|
|
|
80
77
|
meta?: OutputMeta;
|
|
81
78
|
/** Set when the file was auto-chmod'd because content begins with a `#!` shebang. */
|
|
82
79
|
madeExecutable?: boolean;
|
|
80
|
+
/** Absolute filesystem path the write resolved to. Used by the renderer to wrap
|
|
81
|
+
* the (possibly cwd-relative) header path in an OSC 8 `file://` hyperlink. */
|
|
82
|
+
resolvedPath?: string;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
@@ -132,7 +132,7 @@ function stripWriteContent(session: ToolSession, content: string): { text: strin
|
|
|
132
132
|
function maybeWriteSnapshotHeader(session: ToolSession, absolutePath: string, content: string): string | undefined {
|
|
133
133
|
if (!resolveFileDisplayMode(session).hashLines) return undefined;
|
|
134
134
|
const normalized = normalizeToLF(content);
|
|
135
|
-
const tag = getFileSnapshotStore(session).record(absolutePath, normalized);
|
|
135
|
+
const tag = getFileSnapshotStore(session).record(canonicalSnapshotKey(absolutePath), normalized);
|
|
136
136
|
return formatHashlineHeader(formatPathRelativeToCwd(absolutePath, session.cwd), tag);
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -419,7 +419,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
419
419
|
}`;
|
|
420
420
|
return {
|
|
421
421
|
content: [{ type: "text", text: `Successfully wrote ${content.length} bytes to ${outputPath}` }],
|
|
422
|
-
details: {},
|
|
422
|
+
details: { resolvedPath: resolvedArchivePath.absolutePath },
|
|
423
423
|
};
|
|
424
424
|
}
|
|
425
425
|
|
|
@@ -535,7 +535,10 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
535
535
|
}
|
|
536
536
|
|
|
537
537
|
invalidateFsScanAfterWrite(resolvedSqlitePath.absolutePath);
|
|
538
|
-
return toolResult<WriteToolDetails>({
|
|
538
|
+
return toolResult<WriteToolDetails>({ resolvedPath: resolvedSqlitePath.absolutePath })
|
|
539
|
+
.text(resultText)
|
|
540
|
+
.sourcePath(resolvedSqlitePath.absolutePath)
|
|
541
|
+
.done();
|
|
539
542
|
} catch (error) {
|
|
540
543
|
if (isEnoent(error)) {
|
|
541
544
|
throw new ToolError(`SQLite database '${displayPath}' not found`);
|
|
@@ -596,12 +599,13 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
596
599
|
if (!diagnostics) {
|
|
597
600
|
return {
|
|
598
601
|
content: [{ type: "text", text: resultText }],
|
|
599
|
-
details: {},
|
|
602
|
+
details: { resolvedPath: absolutePath },
|
|
600
603
|
};
|
|
601
604
|
}
|
|
602
605
|
return {
|
|
603
606
|
content: [{ type: "text", text: resultText }],
|
|
604
607
|
details: {
|
|
608
|
+
resolvedPath: absolutePath,
|
|
605
609
|
diagnostics,
|
|
606
610
|
meta: outputMeta()
|
|
607
611
|
.diagnostics(diagnostics.summary, diagnostics.messages ?? [])
|
|
@@ -874,7 +878,10 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
874
878
|
if (stripped) {
|
|
875
879
|
resultText += `\nNote: auto-stripped hashline display prefixes from content before writing.`;
|
|
876
880
|
}
|
|
877
|
-
return {
|
|
881
|
+
return {
|
|
882
|
+
content: [{ type: "text", text: resultText }],
|
|
883
|
+
details: { resolvedPath: absolutePath },
|
|
884
|
+
};
|
|
878
885
|
}
|
|
879
886
|
|
|
880
887
|
const diagnostics = await this.#writethrough(absolutePath, cleanContent, signal, undefined, batchRequest);
|
|
@@ -891,13 +898,14 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
891
898
|
if (!diagnostics) {
|
|
892
899
|
return {
|
|
893
900
|
content: [{ type: "text", text: resultText }],
|
|
894
|
-
details: { madeExecutable: madeExecutable || undefined },
|
|
901
|
+
details: { resolvedPath: absolutePath, madeExecutable: madeExecutable || undefined },
|
|
895
902
|
};
|
|
896
903
|
}
|
|
897
904
|
|
|
898
905
|
return {
|
|
899
906
|
content: [{ type: "text", text: resultText }],
|
|
900
907
|
details: {
|
|
908
|
+
resolvedPath: absolutePath,
|
|
901
909
|
diagnostics,
|
|
902
910
|
madeExecutable: madeExecutable || undefined,
|
|
903
911
|
meta: outputMeta()
|
|
@@ -961,9 +969,9 @@ function formatStreamingContent(
|
|
|
961
969
|
}
|
|
962
970
|
for (let i = 0; i < highlighted.length; i++) {
|
|
963
971
|
const lineNum = startIndex + i + 1;
|
|
964
|
-
const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")}
|
|
972
|
+
const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")} `);
|
|
965
973
|
const body = replaceTabs(highlighted[i] ?? "");
|
|
966
|
-
text +=
|
|
974
|
+
text += `${gutter}${body}\n`;
|
|
967
975
|
}
|
|
968
976
|
text += uiTheme.fg("dim", `… (streaming)`);
|
|
969
977
|
return text;
|
|
@@ -987,9 +995,9 @@ function renderContentPreview(
|
|
|
987
995
|
let text = "\n\n";
|
|
988
996
|
for (let i = 0; i < highlighted.length; i++) {
|
|
989
997
|
const lineNum = i + 1;
|
|
990
|
-
const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")}
|
|
998
|
+
const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")} `);
|
|
991
999
|
const body = replaceTabs(highlighted[i] ?? "");
|
|
992
|
-
text +=
|
|
1000
|
+
text += `${gutter}${body}\n`;
|
|
993
1001
|
}
|
|
994
1002
|
if (!expanded && hidden > 0) {
|
|
995
1003
|
const hint = formatExpandHint(uiTheme, expanded, hidden > 0);
|
|
@@ -1006,19 +1014,29 @@ export const writeToolRenderer = {
|
|
|
1006
1014
|
const lang = getLanguageFromPath(rawPath) ?? "text";
|
|
1007
1015
|
const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
1008
1016
|
const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1017
|
+
const header = renderStatusLine(
|
|
1018
|
+
{
|
|
1019
|
+
icon: "pending",
|
|
1020
|
+
spinnerFrame: options?.spinnerFrame,
|
|
1021
|
+
title: "Write",
|
|
1022
|
+
description: `${langIcon} ${pathDisplay}`,
|
|
1023
|
+
},
|
|
1024
|
+
uiTheme,
|
|
1025
|
+
);
|
|
1026
|
+
return framedBlock(uiTheme, width => {
|
|
1027
|
+
const body = args.content
|
|
1028
|
+
? formatStreamingContent(args.content, Boolean(options?.expanded), lang, uiTheme)
|
|
1029
|
+
: "";
|
|
1030
|
+
const bodyLines = body ? body.split("\n") : [];
|
|
1031
|
+
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
1032
|
+
return {
|
|
1033
|
+
header,
|
|
1034
|
+
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
|
1035
|
+
state: "pending",
|
|
1036
|
+
borderColor: "borderMuted",
|
|
1037
|
+
width,
|
|
1038
|
+
};
|
|
1039
|
+
});
|
|
1022
1040
|
},
|
|
1023
1041
|
|
|
1024
1042
|
// Only the expanded (Ctrl+O) preview is append-only: it renders the whole
|
|
@@ -1043,23 +1061,33 @@ export const writeToolRenderer = {
|
|
|
1043
1061
|
const fileContent = args?.content || "";
|
|
1044
1062
|
const lang = getLanguageFromPath(rawPath);
|
|
1045
1063
|
const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
1046
|
-
|
|
1064
|
+
// The header shows the cwd-relative path but links to the absolute path the
|
|
1065
|
+
// write resolved to (args.path may be relative, which would yield a broken
|
|
1066
|
+
// `file://` URI). Falls back to plain text when the result lacks a path.
|
|
1067
|
+
const linkTarget = result.details?.resolvedPath;
|
|
1068
|
+
const styledPath = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
|
|
1069
|
+
const pathDisplay = filePath && linkTarget ? fileHyperlink(linkTarget, styledPath) : styledPath;
|
|
1047
1070
|
|
|
1048
1071
|
if (result.isError) {
|
|
1049
1072
|
const errorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1050
|
-
const
|
|
1073
|
+
const header = renderStatusLine(
|
|
1051
1074
|
{ icon: "error", title: "Write", description: `${langIcon} ${pathDisplay}` },
|
|
1052
1075
|
uiTheme,
|
|
1053
1076
|
);
|
|
1054
|
-
return
|
|
1077
|
+
return framedBlock(uiTheme, width => ({
|
|
1078
|
+
header,
|
|
1079
|
+
sections: [{ lines: formatErrorDetail(errorText, uiTheme).split("\n") }],
|
|
1080
|
+
state: "error",
|
|
1081
|
+
borderColor: "error",
|
|
1082
|
+
width,
|
|
1083
|
+
}));
|
|
1055
1084
|
}
|
|
1085
|
+
|
|
1056
1086
|
const lineCount = countLines(fileContent);
|
|
1057
1087
|
const lineSuffix = formatLineCountSuffix(lineCount, uiTheme);
|
|
1058
1088
|
const execSuffix = result.details?.madeExecutable
|
|
1059
1089
|
? `${uiTheme.fg("dim", " · ")}${uiTheme.fg("success", "made executable!")}`
|
|
1060
1090
|
: "";
|
|
1061
|
-
|
|
1062
|
-
// Build header with status icon
|
|
1063
1091
|
const header = renderStatusLine(
|
|
1064
1092
|
{
|
|
1065
1093
|
icon: "success",
|
|
@@ -1070,38 +1098,29 @@ export const writeToolRenderer = {
|
|
|
1070
1098
|
);
|
|
1071
1099
|
const diagnostics = result.details?.diagnostics;
|
|
1072
1100
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
if (diagnostics) {
|
|
1085
|
-
const diagText = formatDiagnostics(diagnostics, expanded, uiTheme, fp =>
|
|
1086
|
-
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
1087
|
-
);
|
|
1088
|
-
if (diagText.trim()) {
|
|
1089
|
-
const diagLines = diagText.split("\n");
|
|
1090
|
-
const firstNonEmpty = diagLines.findIndex(line => line.trim());
|
|
1091
|
-
if (firstNonEmpty >= 0) {
|
|
1092
|
-
text += `\n${diagLines.slice(firstNonEmpty).join("\n")}`;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1101
|
+
return framedBlock(uiTheme, width => {
|
|
1102
|
+
const { expanded } = options;
|
|
1103
|
+
let body = renderContentPreview(fileContent, expanded, lang, uiTheme);
|
|
1104
|
+
if (diagnostics) {
|
|
1105
|
+
const diagText = formatDiagnostics(diagnostics, expanded, uiTheme, fp =>
|
|
1106
|
+
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
1107
|
+
);
|
|
1108
|
+
if (diagText.trim()) {
|
|
1109
|
+
const diagLines = diagText.split("\n");
|
|
1110
|
+
const firstNonEmpty = diagLines.findIndex(line => line.trim());
|
|
1111
|
+
if (firstNonEmpty >= 0) body += `\n${diagLines.slice(firstNonEmpty).join("\n")}`;
|
|
1095
1112
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1113
|
+
}
|
|
1114
|
+
const bodyLines = body.split("\n");
|
|
1115
|
+
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
1116
|
+
return {
|
|
1117
|
+
header,
|
|
1118
|
+
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
|
1119
|
+
state: "success",
|
|
1120
|
+
borderColor: "borderMuted",
|
|
1121
|
+
width,
|
|
1122
|
+
};
|
|
1123
|
+
});
|
|
1105
1124
|
},
|
|
1106
1125
|
mergeCallAndResult: true,
|
|
1107
1126
|
};
|
package/src/tools/yield.ts
CHANGED
|
@@ -20,6 +20,14 @@ export interface YieldDetails {
|
|
|
20
20
|
data: unknown;
|
|
21
21
|
status: "success" | "aborted";
|
|
22
22
|
error?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Set when the yield tool exhausted its in-tool schema-retry budget
|
|
25
|
+
* (MAX_SCHEMA_RETRIES) and accepted the data anyway. Surfaced so the
|
|
26
|
+
* executor's post-mortem finalizer can honor the override instead of
|
|
27
|
+
* re-rejecting the same payload with `schema_violation` — keeping the
|
|
28
|
+
* subagent's acceptance and the parent's view of the result in lockstep.
|
|
29
|
+
*/
|
|
30
|
+
schemaOverridden?: boolean;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
function formatSchema(schema: unknown): string {
|
|
@@ -237,7 +245,7 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
237
245
|
: "Result submitted.";
|
|
238
246
|
return {
|
|
239
247
|
content: [{ type: "text", text: responseText }],
|
|
240
|
-
details: { data, status, error: errorMessage },
|
|
248
|
+
details: { data, status, error: errorMessage, schemaOverridden: schemaValidationOverridden || undefined },
|
|
241
249
|
};
|
|
242
250
|
}
|
|
243
251
|
}
|
|
@@ -254,6 +262,7 @@ subprocessToolRegistry.register<YieldDetails>("yield", {
|
|
|
254
262
|
data: record.data,
|
|
255
263
|
status,
|
|
256
264
|
error: typeof record.error === "string" ? record.error : undefined,
|
|
265
|
+
schemaOverridden: record.schemaOverridden === true ? true : undefined,
|
|
257
266
|
};
|
|
258
267
|
},
|
|
259
268
|
shouldTerminate: event => !event.isError,
|