@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +123 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/scripts/format-prompts.ts +1 -1
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +11 -29
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +31 -4
- package/src/config/settings-schema.ts +102 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +17 -1
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +122 -50
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +55 -2
- package/src/extensibility/extensions/types.ts +98 -221
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +42 -1
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +9 -10
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +24 -11
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +412 -71
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +27 -10
- package/src/modes/controllers/event-controller.ts +60 -18
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +85 -39
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +675 -39
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +30 -88
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +20 -5
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +25 -6
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +25 -24
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +104 -116
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +8 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/resolve.md +6 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +81 -17
- package/src/session/agent-session.ts +656 -125
- package/src/session/blob-store.ts +36 -3
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +717 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +27 -10
- package/src/task/index.ts +20 -1
- package/src/task/render.ts +27 -18
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +203 -6
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +21 -10
- package/src/tools/eval.ts +3 -1
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +39 -39
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +689 -182
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +25 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +605 -239
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +67 -10
- package/src/tui/code-cell.ts +70 -2
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
|
@@ -12,6 +12,7 @@ export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
|
|
12
12
|
export const DEFAULT_MAX_COLUMN = 1024; // Max chars per grep match line
|
|
13
13
|
|
|
14
14
|
const NL = "\n";
|
|
15
|
+
const ELLIPSIS = "…";
|
|
15
16
|
|
|
16
17
|
// =============================================================================
|
|
17
18
|
// Interfaces
|
|
@@ -24,6 +25,14 @@ export interface OutputSummary {
|
|
|
24
25
|
totalBytes: number;
|
|
25
26
|
outputLines: number;
|
|
26
27
|
outputBytes: number;
|
|
28
|
+
/** Bytes elided from the middle when head-retain mode is active. */
|
|
29
|
+
elidedBytes?: number;
|
|
30
|
+
/** Lines elided from the middle when head-retain mode is active. */
|
|
31
|
+
elidedLines?: number;
|
|
32
|
+
/** Bytes dropped by the per-line column cap (sum across all lines). */
|
|
33
|
+
columnDroppedBytes?: number;
|
|
34
|
+
/** Number of distinct lines that hit the per-line column cap. */
|
|
35
|
+
columnTruncatedLines?: number;
|
|
27
36
|
/** Artifact ID for internal URL access (artifact://<id>) when truncated */
|
|
28
37
|
artifactId?: string;
|
|
29
38
|
}
|
|
@@ -31,7 +40,21 @@ export interface OutputSummary {
|
|
|
31
40
|
export interface OutputSinkOptions {
|
|
32
41
|
artifactPath?: string;
|
|
33
42
|
artifactId?: string;
|
|
43
|
+
/** Tail buffer budget (bytes). Default DEFAULT_MAX_BYTES. */
|
|
34
44
|
spillThreshold?: number;
|
|
45
|
+
/**
|
|
46
|
+
* When > 0, the sink keeps the first `headBytes` of output in addition to
|
|
47
|
+
* the rolling tail window. Output between the two windows is elided
|
|
48
|
+
* (middle elision). Default 0 = tail-only behavior.
|
|
49
|
+
*/
|
|
50
|
+
headBytes?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Per-line byte cap. When > 0, lines wider than `maxColumns` bytes are
|
|
53
|
+
* truncated with an ellipsis at write time; remaining bytes up to the next
|
|
54
|
+
* `\n` are dropped. Cap state persists across chunks so split-mid-line
|
|
55
|
+
* writes still respect the budget. Default 0 = no per-line cap.
|
|
56
|
+
*/
|
|
57
|
+
maxColumns?: number;
|
|
35
58
|
onChunk?: (chunk: string) => void;
|
|
36
59
|
/** Minimum ms between onChunk calls. 0 = every chunk (default). */
|
|
37
60
|
chunkThrottleMs?: number;
|
|
@@ -40,11 +63,15 @@ export interface OutputSinkOptions {
|
|
|
40
63
|
export interface TruncationResult {
|
|
41
64
|
content: string;
|
|
42
65
|
truncated?: boolean;
|
|
43
|
-
truncatedBy?: "lines" | "bytes";
|
|
66
|
+
truncatedBy?: "lines" | "bytes" | "middle";
|
|
44
67
|
totalLines: number;
|
|
45
68
|
totalBytes: number;
|
|
46
69
|
outputLines?: number;
|
|
47
70
|
outputBytes?: number;
|
|
71
|
+
/** Bytes elided from the middle (truncateMiddle only). */
|
|
72
|
+
elidedBytes?: number;
|
|
73
|
+
/** Lines elided from the middle (truncateMiddle only). */
|
|
74
|
+
elidedLines?: number;
|
|
48
75
|
lastLinePartial?: boolean;
|
|
49
76
|
firstLineExceedsLimit?: boolean;
|
|
50
77
|
}
|
|
@@ -54,6 +81,16 @@ export interface TruncationOptions {
|
|
|
54
81
|
maxLines?: number;
|
|
55
82
|
/** Maximum number of bytes (default: 50KB) */
|
|
56
83
|
maxBytes?: number;
|
|
84
|
+
/**
|
|
85
|
+
* For `truncateMiddle`: bytes reserved for the head window. The tail
|
|
86
|
+
* window receives `maxBytes - maxHeadBytes`. Default `floor(maxBytes/2)`.
|
|
87
|
+
*/
|
|
88
|
+
maxHeadBytes?: number;
|
|
89
|
+
/**
|
|
90
|
+
* For `truncateMiddle`: lines reserved for the head window. The tail
|
|
91
|
+
* window receives `maxLines - maxHeadLines`. Default `floor(maxLines/2)`.
|
|
92
|
+
*/
|
|
93
|
+
maxHeadLines?: number;
|
|
57
94
|
}
|
|
58
95
|
|
|
59
96
|
/** Result from byte-level truncation helpers. */
|
|
@@ -425,6 +462,90 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
425
462
|
};
|
|
426
463
|
}
|
|
427
464
|
|
|
465
|
+
// =============================================================================
|
|
466
|
+
// Middle elision (keep head + tail, drop middle)
|
|
467
|
+
// =============================================================================
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Format the inline marker substituted for the elided middle region.
|
|
471
|
+
* Returned without surrounding newlines so callers can position it freely.
|
|
472
|
+
*/
|
|
473
|
+
export function formatMiddleElisionMarker(elidedLines: number, elidedBytes: number): string {
|
|
474
|
+
const linesPart = `${elidedLines.toLocaleString()} line${elidedLines === 1 ? "" : "s"}`;
|
|
475
|
+
return `[… ${linesPart} elided (${formatBytes(elidedBytes)}) …]`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Truncate content keeping a head window and a tail window, eliding the middle.
|
|
480
|
+
*
|
|
481
|
+
* The combined output is `<head>\n<marker>\n<tail>` when truncation is needed.
|
|
482
|
+
* `maxHeadBytes` defaults to `floor(maxBytes / 2)`; the tail receives the
|
|
483
|
+
* remainder. Falls back to `truncateTail` / `truncateHead` if either side's
|
|
484
|
+
* budget is empty or the content already fits.
|
|
485
|
+
*/
|
|
486
|
+
export function truncateMiddle(content: string, options: TruncationOptions = {}): TruncationResult {
|
|
487
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
488
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
489
|
+
const headBytes = options.maxHeadBytes ?? Math.floor(maxBytes / 2);
|
|
490
|
+
const tailBytes = Math.max(0, maxBytes - headBytes);
|
|
491
|
+
const headLines = options.maxHeadLines ?? Math.max(1, Math.floor(maxLines / 2));
|
|
492
|
+
const tailLines = Math.max(0, maxLines - headLines);
|
|
493
|
+
|
|
494
|
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
495
|
+
const totalLines = countNewlines(content) + 1;
|
|
496
|
+
|
|
497
|
+
if (totalBytes <= maxBytes && totalLines <= maxLines) {
|
|
498
|
+
return noTruncResult(content, totalLines, totalBytes);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Degenerate budgets → fall back to one-sided truncation.
|
|
502
|
+
if (headBytes <= 0 || headLines <= 0) {
|
|
503
|
+
return truncateTail(content, { maxBytes: tailBytes || maxBytes, maxLines: tailLines || maxLines });
|
|
504
|
+
}
|
|
505
|
+
if (tailBytes <= 0 || tailLines <= 0) {
|
|
506
|
+
return truncateHead(content, { maxBytes: headBytes, maxLines: headLines });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const head = truncateHead(content, { maxBytes: headBytes, maxLines: headLines });
|
|
510
|
+
const tail = truncateTail(content, { maxBytes: tailBytes, maxLines: tailLines });
|
|
511
|
+
|
|
512
|
+
const headLinesKept = head.outputLines ?? 0;
|
|
513
|
+
const tailLinesKept = tail.outputLines ?? 0;
|
|
514
|
+
const headBytesKept = head.outputBytes ?? Buffer.byteLength(head.content, "utf-8");
|
|
515
|
+
const tailBytesKept = tail.outputBytes ?? Buffer.byteLength(tail.content, "utf-8");
|
|
516
|
+
|
|
517
|
+
// Head unusable (first line exceeds budget) → tail-only.
|
|
518
|
+
if (headLinesKept === 0 || head.firstLineExceedsLimit) return tail;
|
|
519
|
+
// Tail unusable → head-only.
|
|
520
|
+
if (tailLinesKept === 0) return head;
|
|
521
|
+
// Windows overlap → no meaningful elision; return content untruncated.
|
|
522
|
+
if (headLinesKept + tailLinesKept >= totalLines) {
|
|
523
|
+
return noTruncResult(content, totalLines, totalBytes);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const elidedLines = totalLines - headLinesKept - tailLinesKept;
|
|
527
|
+
// `totalBytes - headBytesKept - tailBytesKept` includes newline separators
|
|
528
|
+
// between the kept windows and the elided region; close enough for a notice.
|
|
529
|
+
const elidedBytes = Math.max(0, totalBytes - headBytesKept - tailBytesKept);
|
|
530
|
+
const marker = formatMiddleElisionMarker(elidedLines, elidedBytes);
|
|
531
|
+
const composed = `${head.content}\n${marker}\n${tail.content}`;
|
|
532
|
+
const markerBytes = Buffer.byteLength(marker, "utf-8");
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
content: composed,
|
|
536
|
+
truncated: true,
|
|
537
|
+
truncatedBy: "middle",
|
|
538
|
+
totalLines,
|
|
539
|
+
totalBytes,
|
|
540
|
+
outputLines: headLinesKept + tailLinesKept + 1,
|
|
541
|
+
outputBytes: headBytesKept + tailBytesKept + markerBytes + 2,
|
|
542
|
+
elidedLines,
|
|
543
|
+
elidedBytes,
|
|
544
|
+
lastLinePartial: tail.lastLinePartial,
|
|
545
|
+
firstLineExceedsLimit: false,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
428
549
|
// =============================================================================
|
|
429
550
|
// TailBuffer — ring-style tail buffer with lazy joining
|
|
430
551
|
// =============================================================================
|
|
@@ -520,12 +641,21 @@ export class TailBuffer {
|
|
|
520
641
|
export class OutputSink {
|
|
521
642
|
#buffer = "";
|
|
522
643
|
#bufferBytes = 0;
|
|
644
|
+
#head = "";
|
|
645
|
+
#headBytes = 0;
|
|
646
|
+
#headLines = 0; // newline count inside #head
|
|
523
647
|
#totalLines = 0; // newline count
|
|
524
648
|
#totalBytes = 0;
|
|
525
649
|
#sawData = false;
|
|
526
650
|
#truncated = false;
|
|
527
651
|
#lastChunkTime = 0;
|
|
528
652
|
|
|
653
|
+
// Per-line column cap streaming state (persists across `push` calls so a
|
|
654
|
+
// long line split across chunks still trips the same trigger).
|
|
655
|
+
#currentLineBytes = 0;
|
|
656
|
+
#columnEllipsisAdded = false;
|
|
657
|
+
#columnDroppedBytes = 0;
|
|
658
|
+
#columnTruncatedLines = 0;
|
|
529
659
|
#file?: {
|
|
530
660
|
path: string;
|
|
531
661
|
artifactId?: string;
|
|
@@ -539,20 +669,26 @@ export class OutputSink {
|
|
|
539
669
|
readonly #artifactPath?: string;
|
|
540
670
|
readonly #artifactId?: string;
|
|
541
671
|
readonly #spillThreshold: number;
|
|
672
|
+
readonly #headLimit: number;
|
|
542
673
|
readonly #onChunk?: (chunk: string) => void;
|
|
543
674
|
readonly #chunkThrottleMs: number;
|
|
675
|
+
readonly #maxColumns: number;
|
|
544
676
|
|
|
545
677
|
constructor(options?: OutputSinkOptions) {
|
|
546
678
|
const {
|
|
547
679
|
artifactPath,
|
|
548
680
|
artifactId,
|
|
549
681
|
spillThreshold = DEFAULT_MAX_BYTES,
|
|
682
|
+
headBytes = 0,
|
|
683
|
+
maxColumns = 0,
|
|
550
684
|
onChunk,
|
|
551
685
|
chunkThrottleMs = 0,
|
|
552
686
|
} = options ?? {};
|
|
553
687
|
this.#artifactPath = artifactPath;
|
|
554
688
|
this.#artifactId = artifactId;
|
|
555
689
|
this.#spillThreshold = spillThreshold;
|
|
690
|
+
this.#headLimit = Math.max(0, headBytes);
|
|
691
|
+
this.#maxColumns = Math.max(0, maxColumns);
|
|
556
692
|
this.#onChunk = onChunk;
|
|
557
693
|
this.#chunkThrottleMs = chunkThrottleMs;
|
|
558
694
|
}
|
|
@@ -565,6 +701,8 @@ export class OutputSink {
|
|
|
565
701
|
chunk = sanitizeWithOptionalSixelPassthrough(chunk, sanitizeText);
|
|
566
702
|
|
|
567
703
|
// Throttled onChunk: only call the callback when enough time has passed.
|
|
704
|
+
// Live preview gets the raw (pre-cap) chunk so the TUI never lags behind
|
|
705
|
+
// what reached the sink — the column cap is for the persisted LLM view.
|
|
568
706
|
if (this.#onChunk) {
|
|
569
707
|
const now = Date.now();
|
|
570
708
|
if (now - this.#lastChunkTime >= this.#chunkThrottleMs) {
|
|
@@ -573,22 +711,124 @@ export class OutputSink {
|
|
|
573
711
|
}
|
|
574
712
|
}
|
|
575
713
|
|
|
576
|
-
const
|
|
577
|
-
this.#totalBytes +=
|
|
714
|
+
const rawBytes = Buffer.byteLength(chunk, "utf-8");
|
|
715
|
+
this.#totalBytes += rawBytes;
|
|
578
716
|
|
|
579
717
|
if (chunk.length > 0) {
|
|
580
718
|
this.#sawData = true;
|
|
581
719
|
this.#totalLines += countNewlines(chunk);
|
|
582
720
|
}
|
|
583
721
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
722
|
+
// Per-line column cap. State persists across chunks so a mid-line split
|
|
723
|
+
// still respects the budget. Operates on the sanitized chunk; the cap is
|
|
724
|
+
// applied before head/tail accounting but after artifact mirroring decides.
|
|
725
|
+
const capped = this.#maxColumns > 0 ? this.#applyColumnCap(chunk) : chunk;
|
|
726
|
+
const cappedBytes = capped === chunk ? rawBytes : Buffer.byteLength(capped, "utf-8");
|
|
727
|
+
const cappedThisChunk = cappedBytes < rawBytes;
|
|
728
|
+
if (cappedThisChunk) this.#truncated = true;
|
|
729
|
+
|
|
730
|
+
// Mirror RAW chunk to the artifact file so the on-disk record is the full
|
|
731
|
+
// uncapped stream. Mirror triggers on: in-memory overflow OR this chunk's
|
|
732
|
+
// column cap dropped bytes (otherwise we'd lose data) OR file already open.
|
|
733
|
+
if (this.#artifactPath && (this.#file != null || cappedThisChunk || this.#willOverflow(cappedBytes))) {
|
|
589
734
|
this.#writeToFile(chunk);
|
|
590
735
|
}
|
|
591
736
|
|
|
737
|
+
if (cappedBytes === 0) return;
|
|
738
|
+
|
|
739
|
+
// Head retention: drain the (capped) chunk into #head until the budget is
|
|
740
|
+
// exhausted, then forward any leftover to the tail buffer.
|
|
741
|
+
let tailChunk = capped;
|
|
742
|
+
let tailBytes = cappedBytes;
|
|
743
|
+
if (this.#headLimit > 0 && this.#headBytes < this.#headLimit) {
|
|
744
|
+
const room = this.#headLimit - this.#headBytes;
|
|
745
|
+
if (cappedBytes <= room) {
|
|
746
|
+
this.#head += capped;
|
|
747
|
+
this.#headBytes += cappedBytes;
|
|
748
|
+
this.#headLines += countNewlines(capped);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
// Split: head takes a UTF-8-safe prefix; remainder flows to tail.
|
|
752
|
+
const headSlice = truncateHeadBytes(capped, room);
|
|
753
|
+
if (headSlice.bytes > 0) {
|
|
754
|
+
this.#head += headSlice.text;
|
|
755
|
+
this.#headBytes += headSlice.bytes;
|
|
756
|
+
this.#headLines += countNewlines(headSlice.text);
|
|
757
|
+
tailChunk = capped.substring(headSlice.text.length);
|
|
758
|
+
tailBytes = cappedBytes - headSlice.bytes;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
this.#pushTail(tailChunk, tailBytes);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Apply the per-line byte cap to `chunk`, dropping bytes that would push the
|
|
767
|
+
* current line beyond `#maxColumns`. Emits a single `…` once a line trips the
|
|
768
|
+
* cap; subsequent bytes are skipped until the next `\n`. State persists
|
|
769
|
+
* across calls so a long line split across chunks still produces one marker.
|
|
770
|
+
*/
|
|
771
|
+
#applyColumnCap(chunk: string): string {
|
|
772
|
+
if (chunk.length === 0) return chunk;
|
|
773
|
+
const max = this.#maxColumns;
|
|
774
|
+
const parts: string[] = [];
|
|
775
|
+
let cursor = 0;
|
|
776
|
+
while (cursor < chunk.length) {
|
|
777
|
+
const nlIdx = chunk.indexOf(NL, cursor);
|
|
778
|
+
const segEnd = nlIdx === -1 ? chunk.length : nlIdx;
|
|
779
|
+
if (segEnd > cursor) {
|
|
780
|
+
const segment = chunk.substring(cursor, segEnd);
|
|
781
|
+
if (this.#columnEllipsisAdded) {
|
|
782
|
+
// Past the cap; drop until newline.
|
|
783
|
+
this.#columnDroppedBytes += Buffer.byteLength(segment, "utf-8");
|
|
784
|
+
} else {
|
|
785
|
+
const segBytes = Buffer.byteLength(segment, "utf-8");
|
|
786
|
+
const remaining = max - this.#currentLineBytes;
|
|
787
|
+
if (segBytes <= remaining) {
|
|
788
|
+
parts.push(segment);
|
|
789
|
+
this.#currentLineBytes += segBytes;
|
|
790
|
+
} else {
|
|
791
|
+
// First overflow on this line: keep what fits, append ellipsis,
|
|
792
|
+
// arm the skip-until-newline flag.
|
|
793
|
+
const ellipsisBytes = 3; // "…" in UTF-8
|
|
794
|
+
const headRoom = Math.max(0, remaining - ellipsisBytes);
|
|
795
|
+
let kept = "";
|
|
796
|
+
let keptBytes = 0;
|
|
797
|
+
if (headRoom > 0) {
|
|
798
|
+
const sliced = truncateHeadBytes(segment, headRoom);
|
|
799
|
+
kept = sliced.text;
|
|
800
|
+
keptBytes = sliced.bytes;
|
|
801
|
+
parts.push(kept);
|
|
802
|
+
}
|
|
803
|
+
parts.push(ELLIPSIS);
|
|
804
|
+
this.#columnDroppedBytes += segBytes - keptBytes;
|
|
805
|
+
this.#columnTruncatedLines++;
|
|
806
|
+
this.#currentLineBytes += keptBytes + ellipsisBytes;
|
|
807
|
+
this.#columnEllipsisAdded = true;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (nlIdx === -1) break;
|
|
812
|
+
parts.push(NL);
|
|
813
|
+
this.#currentLineBytes = 0;
|
|
814
|
+
this.#columnEllipsisAdded = false;
|
|
815
|
+
cursor = nlIdx + 1;
|
|
816
|
+
}
|
|
817
|
+
return parts.join("");
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
#willOverflow(dataBytes: number): boolean {
|
|
821
|
+
// Triggers file mirroring as soon as the next chunk would push us over
|
|
822
|
+
// the tail budget (head retention does not change spill-to-artifact).
|
|
823
|
+
return this.#bufferBytes + dataBytes > this.#spillThreshold;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
#pushTail(chunk: string, dataBytes: number): void {
|
|
827
|
+
if (dataBytes === 0) return;
|
|
828
|
+
|
|
829
|
+
const threshold = this.#spillThreshold;
|
|
830
|
+
const willOverflow = this.#bufferBytes + dataBytes > threshold;
|
|
831
|
+
|
|
592
832
|
if (!willOverflow) {
|
|
593
833
|
this.#buffer += chunk;
|
|
594
834
|
this.#bufferBytes += dataBytes;
|
|
@@ -612,8 +852,6 @@ export class OutputSink {
|
|
|
612
852
|
this.#buffer = text;
|
|
613
853
|
this.#bufferBytes = bytes;
|
|
614
854
|
}
|
|
615
|
-
|
|
616
|
-
if (this.#file) this.#truncated = true;
|
|
617
855
|
}
|
|
618
856
|
|
|
619
857
|
/**
|
|
@@ -685,26 +923,84 @@ export class OutputSink {
|
|
|
685
923
|
* streaming counters (totalLines/totalBytes reflect the raw chunks that
|
|
686
924
|
* already reached the sink). Used when an upstream minimizer rewrites the
|
|
687
925
|
* captured output after the raw bytes have already been streamed.
|
|
926
|
+
*
|
|
927
|
+
* Clears any retained head window — the minimized text is authoritative.
|
|
688
928
|
*/
|
|
689
929
|
replace(text: string): void {
|
|
690
930
|
this.#buffer = text;
|
|
691
931
|
this.#bufferBytes = Buffer.byteLength(text, "utf-8");
|
|
932
|
+
this.#head = "";
|
|
933
|
+
this.#headBytes = 0;
|
|
934
|
+
this.#headLines = 0;
|
|
935
|
+
this.#currentLineBytes = 0;
|
|
936
|
+
this.#columnEllipsisAdded = false;
|
|
937
|
+
this.#columnDroppedBytes = 0;
|
|
938
|
+
this.#columnTruncatedLines = 0;
|
|
692
939
|
}
|
|
693
940
|
|
|
694
941
|
async dump(notice?: string): Promise<OutputSummary> {
|
|
695
942
|
const noticeLine = notice ? `[${notice}]\n` : "";
|
|
696
|
-
const outputLines = this.#buffer.length > 0 ? countNewlines(this.#buffer) + 1 : 0;
|
|
697
943
|
const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
|
|
698
944
|
|
|
699
945
|
if (this.#file) await this.#file.sink.end();
|
|
700
946
|
|
|
947
|
+
// Compose the visible output. With head retention, splice head + marker
|
|
948
|
+
// + tail when content was elided. Otherwise return the rolling buffer.
|
|
949
|
+
const headBytes = this.#headBytes;
|
|
950
|
+
const tailBuf = this.#buffer;
|
|
951
|
+
const tailBytes = this.#bufferBytes;
|
|
952
|
+
const headLines = this.#headLines + (headBytes > 0 && !this.#head.endsWith("\n") ? 1 : 0);
|
|
953
|
+
const tailLines = tailBuf.length > 0 ? countNewlines(tailBuf) + 1 : 0;
|
|
954
|
+
|
|
955
|
+
// Bytes that survived the column cap. Middle elision operates on these,
|
|
956
|
+
// so column-dropped bytes don't inflate the "elided from middle" count.
|
|
957
|
+
const effectiveTotalBytes = Math.max(0, this.#totalBytes - this.#columnDroppedBytes);
|
|
958
|
+
|
|
959
|
+
let body: string;
|
|
960
|
+
let outputBytes: number;
|
|
961
|
+
let outputLines: number;
|
|
962
|
+
let elidedBytes: number | undefined;
|
|
963
|
+
let elidedLines: number | undefined;
|
|
964
|
+
|
|
965
|
+
if (headBytes > 0 && effectiveTotalBytes > headBytes + tailBytes) {
|
|
966
|
+
// Middle was elided. Emit head + marker + tail.
|
|
967
|
+
elidedBytes = Math.max(0, effectiveTotalBytes - headBytes - tailBytes);
|
|
968
|
+
elidedLines = Math.max(0, totalLines - headLines - tailLines);
|
|
969
|
+
const marker = formatMiddleElisionMarker(elidedLines, elidedBytes);
|
|
970
|
+
const markerBytes = Buffer.byteLength(marker, "utf-8");
|
|
971
|
+
const headSep = this.#head.endsWith("\n") ? "" : "\n";
|
|
972
|
+
const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
|
|
973
|
+
body = `${this.#head}${headSep}${marker}${tailSep}${tailBuf}`;
|
|
974
|
+
outputBytes =
|
|
975
|
+
headBytes +
|
|
976
|
+
markerBytes +
|
|
977
|
+
tailBytes +
|
|
978
|
+
Buffer.byteLength(headSep, "utf-8") +
|
|
979
|
+
Buffer.byteLength(tailSep, "utf-8");
|
|
980
|
+
outputLines = headLines + 1 + tailLines;
|
|
981
|
+
this.#truncated = true;
|
|
982
|
+
} else if (headBytes > 0) {
|
|
983
|
+
// Head + tail combine into the full buffered output (no overlap or elision).
|
|
984
|
+
body = `${this.#head}${tailBuf}`;
|
|
985
|
+
outputBytes = headBytes + tailBytes;
|
|
986
|
+
outputLines = body.length > 0 ? countNewlines(body) + 1 : 0;
|
|
987
|
+
} else {
|
|
988
|
+
body = tailBuf;
|
|
989
|
+
outputBytes = tailBytes;
|
|
990
|
+
outputLines = tailLines;
|
|
991
|
+
}
|
|
992
|
+
|
|
701
993
|
return {
|
|
702
|
-
output: `${noticeLine}${
|
|
994
|
+
output: `${noticeLine}${body}`,
|
|
703
995
|
truncated: this.#truncated,
|
|
704
996
|
totalLines,
|
|
705
997
|
totalBytes: this.#totalBytes,
|
|
706
998
|
outputLines,
|
|
707
|
-
outputBytes
|
|
999
|
+
outputBytes,
|
|
1000
|
+
elidedBytes,
|
|
1001
|
+
elidedLines,
|
|
1002
|
+
columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
|
|
1003
|
+
columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
|
|
708
1004
|
artifactId: this.#file?.artifactId,
|
|
709
1005
|
};
|
|
710
1006
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AvailableCommand } from "@agentclientprotocol/sdk";
|
|
2
|
+
import { BUILTIN_SLASH_COMMANDS_INTERNAL, lookupBuiltinSlashCommand } from "./builtin-registry";
|
|
3
|
+
import { parseSlashCommand } from "./helpers/parse";
|
|
4
|
+
import type { AcpBuiltinCommandRuntime, AcpBuiltinSlashCommandResult } from "./types";
|
|
5
|
+
|
|
6
|
+
export type { AcpBuiltinCommandRuntime, AcpBuiltinSlashCommandResult } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Commands advertised to ACP clients. Entries without a text-mode `handle`
|
|
10
|
+
* (e.g. `/quit`, `/login`, dashboards) are filtered out so the client doesn't
|
|
11
|
+
* see commands it cannot drive.
|
|
12
|
+
*/
|
|
13
|
+
export const ACP_BUILTIN_SLASH_COMMANDS: AvailableCommand[] = BUILTIN_SLASH_COMMANDS_INTERNAL.filter(
|
|
14
|
+
command => command.handle !== undefined,
|
|
15
|
+
).map(command => {
|
|
16
|
+
// Honor mode-specific copy: ACP clients receive concise text-mode
|
|
17
|
+
// descriptions/hints when the spec sets `acpDescription` / `acpInputHint`,
|
|
18
|
+
// otherwise fall back to the unified `description` / `inlineHint`.
|
|
19
|
+
const hint = command.acpInputHint ?? command.inlineHint;
|
|
20
|
+
return {
|
|
21
|
+
name: command.name,
|
|
22
|
+
description: command.acpDescription ?? command.description,
|
|
23
|
+
input: hint ? { hint } : undefined,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Dispatch a slash command in ACP/text mode. Returns:
|
|
29
|
+
* - `false` when no builtin matched (or matched a TUI-only entry); the caller
|
|
30
|
+
* should forward the input as a prompt.
|
|
31
|
+
* - `{ consumed: true }` when the command handled the input entirely.
|
|
32
|
+
* - `{ prompt }` when the command was handled but a residual prompt should be
|
|
33
|
+
* sent to the model.
|
|
34
|
+
*/
|
|
35
|
+
export async function executeAcpBuiltinSlashCommand(
|
|
36
|
+
text: string,
|
|
37
|
+
runtime: AcpBuiltinCommandRuntime,
|
|
38
|
+
): Promise<AcpBuiltinSlashCommandResult> {
|
|
39
|
+
const parsed = parseSlashCommand(text);
|
|
40
|
+
if (!parsed) return false;
|
|
41
|
+
const command = lookupBuiltinSlashCommand(parsed.name);
|
|
42
|
+
if (!command?.handle) return false;
|
|
43
|
+
const result = await command.handle(parsed, runtime);
|
|
44
|
+
if (result === undefined) return { consumed: true };
|
|
45
|
+
return result;
|
|
46
|
+
}
|