@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -2
- package/dist/cli.js +5790 -5731
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +85 -34
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/modes/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +5 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +49 -32
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +46 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +12 -2
- package/dist/types/task/index.d.ts +13 -6
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +63 -51
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +7 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli-commands.ts +29 -0
- package/src/cli.ts +28 -15
- package/src/commands/launch.ts +4 -0
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/keybindings.ts +6 -1
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +99 -55
- package/src/config/settings.ts +68 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -1
- package/src/eval/py/kernel.ts +31 -11
- package/src/eval/py/prelude.py +5 -6
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +38 -13
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/shared-events.ts +2 -2
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +26 -66
- package/src/memories/index.ts +2 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +151 -4
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +65 -0
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +19 -2
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/controllers/selector-controller.ts +21 -17
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +47 -22
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/theme/theme.ts +18 -5
- package/src/modes/types.ts +5 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +38 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +37 -10
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +422 -291
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +226 -10
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/system-prompt.ts +14 -0
- package/src/task/executor.ts +851 -461
- package/src/task/index.ts +721 -796
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +148 -63
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +82 -66
- package/src/thinking.ts +7 -0
- package/src/tiny/title-client.ts +34 -5
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +61 -10
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/image-gen.ts +11 -4
- package/src/tools/index.ts +17 -13
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
|
@@ -11,6 +11,16 @@ export const DEFAULT_MAX_LINES = 3000;
|
|
|
11
11
|
export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
|
12
12
|
export const DEFAULT_MAX_COLUMN = 512; // Max chars per grep match line
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Default artifact-on-disk cap for {@link OutputSink}.
|
|
16
|
+
*
|
|
17
|
+
* `0` means unbounded: by default, `artifact://<id>` references preserve the
|
|
18
|
+
* complete raw stream instead of a capped head/tail sample.
|
|
19
|
+
*/
|
|
20
|
+
export const ARTIFACT_DEFAULT_MAX_BYTES = 0;
|
|
21
|
+
/** Default head budget; the remainder becomes the rolling tail window. */
|
|
22
|
+
export const ARTIFACT_DEFAULT_HEAD_BYTES = 3 * 1024 * 1024; // 3 MiB
|
|
23
|
+
|
|
14
24
|
const NL = "\n";
|
|
15
25
|
const ELLIPSIS = "…";
|
|
16
26
|
|
|
@@ -58,6 +68,20 @@ export interface OutputSinkOptions {
|
|
|
58
68
|
onChunk?: (chunk: string) => void;
|
|
59
69
|
/** Minimum ms between onChunk calls. 0 = every chunk (default). */
|
|
60
70
|
chunkThrottleMs?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Optional cap on bytes written to the artifact-on-disk file. When the cap
|
|
73
|
+
* is hit, the head window is preserved verbatim and subsequent output feeds
|
|
74
|
+
* a rolling tail window; on close, the sink writes a single
|
|
75
|
+
* `[ARTIFACT TRUNCATED: …]` notice between them. Default
|
|
76
|
+
* {@link ARTIFACT_DEFAULT_MAX_BYTES} (unbounded).
|
|
77
|
+
*/
|
|
78
|
+
artifactMaxBytes?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Bytes reserved for the head window of the capped artifact file. The
|
|
81
|
+
* tail window receives `artifactMaxBytes - artifactHeadBytes`. Default
|
|
82
|
+
* {@link ARTIFACT_DEFAULT_HEAD_BYTES}; clamped to `[0, artifactMaxBytes]`.
|
|
83
|
+
*/
|
|
84
|
+
artifactHeadBytes?: number;
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
export interface TruncationResult {
|
|
@@ -546,6 +570,66 @@ export function truncateMiddle(content: string, options: TruncationOptions = {})
|
|
|
546
570
|
};
|
|
547
571
|
}
|
|
548
572
|
|
|
573
|
+
// =============================================================================
|
|
574
|
+
// Inline byte cap — final defense at the tool-result boundary
|
|
575
|
+
// =============================================================================
|
|
576
|
+
|
|
577
|
+
/** Options for {@link enforceInlineByteCap}. */
|
|
578
|
+
export interface InlineByteCapOptions {
|
|
579
|
+
/** Inline byte budget. Defaults to {@link DEFAULT_MAX_BYTES}. */
|
|
580
|
+
maxBytes?: number;
|
|
581
|
+
/** What the text is, for the elision marker (e.g. "bash output"). */
|
|
582
|
+
label: string;
|
|
583
|
+
/**
|
|
584
|
+
* Persist the full text as a session artifact. When an artifact id is
|
|
585
|
+
* returned, a `[raw output: artifact://<id>]` footer is appended so the
|
|
586
|
+
* elided bytes stay recoverable.
|
|
587
|
+
*/
|
|
588
|
+
saveArtifact?: (full: string) => string | undefined | Promise<string | undefined>;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Drop the partial last line of a head window (keep it if there is no newline at all). */
|
|
592
|
+
function trimHeadToLineBoundary(text: string): string {
|
|
593
|
+
const idx = text.lastIndexOf(NL);
|
|
594
|
+
return idx > 0 ? text.substring(0, idx) : text;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Drop the partial first line of a tail window (keep it if there is no newline at all). */
|
|
598
|
+
function trimTailToLineBoundary(text: string): string {
|
|
599
|
+
const idx = text.indexOf(NL);
|
|
600
|
+
if (idx < 0 || idx === text.length - 1) return text;
|
|
601
|
+
return text.substring(idx + 1);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Final-defense inline size guard for tool results.
|
|
606
|
+
*
|
|
607
|
+
* No-op when `text` fits within `maxBytes` (the common path). Otherwise keeps
|
|
608
|
+
* ~60% of the budget from the head and ~25% from the tail — cut on line
|
|
609
|
+
* boundaries, never splitting a multi-byte UTF-8 sequence — with an elision
|
|
610
|
+
* marker between. The remaining ~15% is slack for the marker and the optional
|
|
611
|
+
* `[raw output: artifact://<id>]` footer, so the result stays under `maxBytes`.
|
|
612
|
+
*/
|
|
613
|
+
export async function enforceInlineByteCap(text: string, options: InlineByteCapOptions): Promise<string> {
|
|
614
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
615
|
+
if (maxBytes <= 0) return text;
|
|
616
|
+
const totalBytes = Buffer.byteLength(text, "utf-8");
|
|
617
|
+
if (totalBytes <= maxBytes) return text;
|
|
618
|
+
|
|
619
|
+
const head = trimHeadToLineBoundary(truncateHeadBytes(text, Math.floor(maxBytes * 0.6)).text);
|
|
620
|
+
const tail = trimTailToLineBoundary(truncateTailBytes(text, Math.floor(maxBytes * 0.25)).text);
|
|
621
|
+
const elidedBytes = Math.max(0, totalBytes - Buffer.byteLength(head, "utf-8") - Buffer.byteLength(tail, "utf-8"));
|
|
622
|
+
const marker = `[… elided ${elidedBytes} bytes of ${options.label} …]`;
|
|
623
|
+
let composed = `${head}\n${marker}\n${tail}`;
|
|
624
|
+
|
|
625
|
+
const artifactId = await options.saveArtifact?.(text);
|
|
626
|
+
if (artifactId) {
|
|
627
|
+
const sep = composed.endsWith(NL) ? "" : NL;
|
|
628
|
+
composed += `${sep}[raw output: artifact://${artifactId}]`;
|
|
629
|
+
}
|
|
630
|
+
return composed;
|
|
631
|
+
}
|
|
632
|
+
|
|
549
633
|
// =============================================================================
|
|
550
634
|
// TailBuffer — ring-style tail buffer with lazy joining
|
|
551
635
|
// =============================================================================
|
|
@@ -676,6 +760,21 @@ export class OutputSink {
|
|
|
676
760
|
readonly #chunkThrottleMs: number;
|
|
677
761
|
readonly #maxColumns: number;
|
|
678
762
|
|
|
763
|
+
// Optional artifact-on-disk cap. When `#artifactMaxBytes > 0` the file sink
|
|
764
|
+
// owns a head budget + a rolling tail buffer; once the head is closed,
|
|
765
|
+
// subsequent chunks are diverted into `#artifactTailRing` (bounded by
|
|
766
|
+
// `#artifactTailBudget`). On `dump()` the tail is flushed back to the sink
|
|
767
|
+
// behind a `[ARTIFACT TRUNCATED: …]` notice. The default cap is disabled so
|
|
768
|
+
// advertised `artifact://<id>` captures are lossless.
|
|
769
|
+
readonly #artifactMaxBytes: number;
|
|
770
|
+
readonly #artifactHeadBudget: number;
|
|
771
|
+
readonly #artifactTailBudget: number;
|
|
772
|
+
#artifactHeadBytesWritten = 0;
|
|
773
|
+
#artifactHeadClosed = false;
|
|
774
|
+
#artifactTailRing = "";
|
|
775
|
+
#artifactTailRingBytes = 0;
|
|
776
|
+
#artifactTailIncomingBytes = 0;
|
|
777
|
+
|
|
679
778
|
constructor(options?: OutputSinkOptions) {
|
|
680
779
|
const {
|
|
681
780
|
artifactPath,
|
|
@@ -685,6 +784,8 @@ export class OutputSink {
|
|
|
685
784
|
maxColumns = 0,
|
|
686
785
|
onChunk,
|
|
687
786
|
chunkThrottleMs = 0,
|
|
787
|
+
artifactMaxBytes = ARTIFACT_DEFAULT_MAX_BYTES,
|
|
788
|
+
artifactHeadBytes = ARTIFACT_DEFAULT_HEAD_BYTES,
|
|
688
789
|
} = options ?? {};
|
|
689
790
|
this.#artifactPath = artifactPath;
|
|
690
791
|
this.#artifactId = artifactId;
|
|
@@ -693,6 +794,9 @@ export class OutputSink {
|
|
|
693
794
|
this.#maxColumns = Math.max(0, maxColumns);
|
|
694
795
|
this.#onChunk = onChunk;
|
|
695
796
|
this.#chunkThrottleMs = chunkThrottleMs;
|
|
797
|
+
this.#artifactMaxBytes = Math.max(0, artifactMaxBytes);
|
|
798
|
+
this.#artifactHeadBudget = Math.max(0, Math.min(artifactHeadBytes, this.#artifactMaxBytes));
|
|
799
|
+
this.#artifactTailBudget = Math.max(0, this.#artifactMaxBytes - this.#artifactHeadBudget);
|
|
696
800
|
}
|
|
697
801
|
|
|
698
802
|
/**
|
|
@@ -865,14 +969,18 @@ export class OutputSink {
|
|
|
865
969
|
/**
|
|
866
970
|
* Write a chunk to the artifact file. Handles the async file sink creation
|
|
867
971
|
* by queuing writes until the sink is ready, then draining synchronously.
|
|
972
|
+
* Once the sink is up, every byte flows through {@link #emitToSink} which
|
|
973
|
+
* owns the head + tail cap so artifacts cannot grow beyond
|
|
974
|
+
* `#artifactMaxBytes` on disk.
|
|
868
975
|
*/
|
|
869
976
|
#writeToFile(chunk: string): void {
|
|
870
977
|
if (this.#fileReady && this.#file) {
|
|
871
|
-
|
|
872
|
-
this.#file.sink.write(chunk);
|
|
978
|
+
this.#emitToSink(chunk);
|
|
873
979
|
return;
|
|
874
980
|
}
|
|
875
|
-
// File sink not yet created — queue this chunk and kick off creation
|
|
981
|
+
// File sink not yet created — queue this chunk and kick off creation.
|
|
982
|
+
// The queue is bounded only by how many chunks arrive before the open
|
|
983
|
+
// resolves (typically <2). The cap is enforced on drain.
|
|
876
984
|
if (!this.#pendingFileWrites) {
|
|
877
985
|
this.#pendingFileWrites = [chunk];
|
|
878
986
|
void this.#createFileSink();
|
|
@@ -881,31 +989,99 @@ export class OutputSink {
|
|
|
881
989
|
}
|
|
882
990
|
}
|
|
883
991
|
|
|
992
|
+
/**
|
|
993
|
+
* Cap-aware sink writer. Bytes flow into the head window verbatim until the
|
|
994
|
+
* budget is exhausted; subsequent bytes are diverted into a rolling tail
|
|
995
|
+
* ring, evicted from the front so total RAM stays bounded by
|
|
996
|
+
* `#artifactTailBudget`. `dump()` replays the ring behind a single notice
|
|
997
|
+
* line before closing the sink.
|
|
998
|
+
*
|
|
999
|
+
* When the cap is disabled (`#artifactMaxBytes === 0`) this collapses to a
|
|
1000
|
+
* straight pass-through, preserving the historical "stream everything"
|
|
1001
|
+
* contract.
|
|
1002
|
+
*/
|
|
1003
|
+
#emitToSink(chunk: string): void {
|
|
1004
|
+
if (!this.#file || chunk.length === 0) return;
|
|
1005
|
+
if (this.#artifactMaxBytes === 0) {
|
|
1006
|
+
this.#file.sink.write(chunk);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const chunkBytes = Buffer.byteLength(chunk, "utf-8");
|
|
1010
|
+
const room = this.#artifactHeadClosed ? 0 : this.#artifactHeadBudget - this.#artifactHeadBytesWritten;
|
|
1011
|
+
if (room >= chunkBytes) {
|
|
1012
|
+
this.#file.sink.write(chunk);
|
|
1013
|
+
this.#artifactHeadBytesWritten += chunkBytes;
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
let overflow = chunk;
|
|
1017
|
+
if (room > 0) {
|
|
1018
|
+
const headSlice = truncateHeadBytes(chunk, room);
|
|
1019
|
+
if (headSlice.bytes > 0) {
|
|
1020
|
+
this.#file.sink.write(headSlice.text);
|
|
1021
|
+
this.#artifactHeadBytesWritten += headSlice.bytes;
|
|
1022
|
+
}
|
|
1023
|
+
// Even when UTF-8 boundary safety leaves a few bytes of nominal room,
|
|
1024
|
+
// this chunk has already overflowed the head window. Close it now so a
|
|
1025
|
+
// later small ASCII chunk cannot be written before this overflow tail.
|
|
1026
|
+
this.#artifactHeadClosed = true;
|
|
1027
|
+
overflow = chunk.substring(headSlice.text.length);
|
|
1028
|
+
}
|
|
1029
|
+
if (overflow.length === 0 || this.#artifactTailBudget === 0) {
|
|
1030
|
+
// No tail budget: count the dropped bytes so the notice reflects them.
|
|
1031
|
+
if (overflow.length > 0) {
|
|
1032
|
+
this.#artifactTailIncomingBytes += Buffer.byteLength(overflow, "utf-8");
|
|
1033
|
+
}
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
this.#pushArtifactTail(overflow);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
#pushArtifactTail(chunk: string): void {
|
|
1040
|
+
const chunkBytes = Buffer.byteLength(chunk, "utf-8");
|
|
1041
|
+
this.#artifactTailIncomingBytes += chunkBytes;
|
|
1042
|
+
const budget = this.#artifactTailBudget;
|
|
1043
|
+
if (chunkBytes >= budget) {
|
|
1044
|
+
// Chunk alone dominates — keep only its tail slice.
|
|
1045
|
+
const { text, bytes } = truncateTailBytes(chunk, budget);
|
|
1046
|
+
this.#artifactTailRing = text;
|
|
1047
|
+
this.#artifactTailRingBytes = bytes;
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
this.#artifactTailRing += chunk;
|
|
1051
|
+
this.#artifactTailRingBytes += chunkBytes;
|
|
1052
|
+
if (this.#artifactTailRingBytes > budget) {
|
|
1053
|
+
const { text, bytes } = truncateTailBytes(this.#artifactTailRing, budget);
|
|
1054
|
+
this.#artifactTailRing = text;
|
|
1055
|
+
this.#artifactTailRingBytes = bytes;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
884
1059
|
async #createFileSink(): Promise<void> {
|
|
885
1060
|
if (!this.#artifactPath || this.#fileReady) return;
|
|
886
1061
|
try {
|
|
887
1062
|
const sink = Bun.file(this.#artifactPath).writer();
|
|
888
1063
|
this.#file = { path: this.#artifactPath, artifactId: this.#artifactId, sink };
|
|
1064
|
+
this.#fileReady = true;
|
|
889
1065
|
|
|
890
1066
|
// Head-retained bytes precede the rolling tail buffer in the capture.
|
|
1067
|
+
// Route through #emitToSink so they count against the artifact head
|
|
1068
|
+
// budget — a direct sink.write would let them escape the cap.
|
|
891
1069
|
if (this.#head.length > 0) {
|
|
892
|
-
|
|
1070
|
+
this.#emitToSink(this.#head);
|
|
893
1071
|
}
|
|
894
1072
|
|
|
895
1073
|
// Flush existing buffer to file BEFORE it gets trimmed further.
|
|
896
1074
|
if (this.#buffer.length > 0) {
|
|
897
|
-
|
|
1075
|
+
this.#emitToSink(this.#buffer);
|
|
898
1076
|
}
|
|
899
1077
|
|
|
900
|
-
// Drain any chunks that arrived while the sink was being created
|
|
1078
|
+
// Drain any chunks that arrived while the sink was being created.
|
|
901
1079
|
if (this.#pendingFileWrites) {
|
|
902
1080
|
for (const pending of this.#pendingFileWrites) {
|
|
903
|
-
|
|
1081
|
+
this.#emitToSink(pending);
|
|
904
1082
|
}
|
|
905
1083
|
this.#pendingFileWrites = undefined;
|
|
906
1084
|
}
|
|
907
|
-
|
|
908
|
-
this.#fileReady = true;
|
|
909
1085
|
} catch {
|
|
910
1086
|
try {
|
|
911
1087
|
await this.#file?.sink?.end();
|
|
@@ -914,6 +1090,7 @@ export class OutputSink {
|
|
|
914
1090
|
}
|
|
915
1091
|
this.#file = undefined;
|
|
916
1092
|
this.#pendingFileWrites = undefined;
|
|
1093
|
+
this.#fileReady = false;
|
|
917
1094
|
}
|
|
918
1095
|
}
|
|
919
1096
|
|
|
@@ -961,6 +1138,42 @@ export class OutputSink {
|
|
|
961
1138
|
this.#pendingChunk = "";
|
|
962
1139
|
}
|
|
963
1140
|
|
|
1141
|
+
/**
|
|
1142
|
+
* Replay the rolling tail ring back into the artifact sink. When bytes
|
|
1143
|
+
* were actually dropped from the middle (the head budget was exhausted
|
|
1144
|
+
* *and* the tail ring evicted), a single `[ARTIFACT TRUNCATED: …]`
|
|
1145
|
+
* notice is injected between head and tail so a reader of
|
|
1146
|
+
* `artifact://<id>` understands the gap. When the total stream simply
|
|
1147
|
+
* spilled past the head budget but still fits below `artifactMaxBytes`,
|
|
1148
|
+
* `droppedBytes` is zero — head + tail together are the verbatim stream
|
|
1149
|
+
* and the notice is suppressed so we don't corrupt the artifact with a
|
|
1150
|
+
* misleading "0 B elided" marker (PR #2083 review by codex).
|
|
1151
|
+
*
|
|
1152
|
+
* No-op when the cap was never hit at all (head budget never exhausted,
|
|
1153
|
+
* tail ring empty).
|
|
1154
|
+
*/
|
|
1155
|
+
#flushArtifactTailIfCapped(): void {
|
|
1156
|
+
if (!this.#file) return;
|
|
1157
|
+
if (this.#artifactMaxBytes === 0) return;
|
|
1158
|
+
const tailBytes = this.#artifactTailRingBytes;
|
|
1159
|
+
const droppedBytes = Math.max(0, this.#artifactTailIncomingBytes - tailBytes);
|
|
1160
|
+
if (tailBytes === 0 && droppedBytes === 0) return;
|
|
1161
|
+
|
|
1162
|
+
if (droppedBytes > 0) {
|
|
1163
|
+
const headWritten = this.#artifactHeadBytesWritten;
|
|
1164
|
+
const totalCapped = headWritten + this.#artifactTailIncomingBytes;
|
|
1165
|
+
const headSep = headWritten > 0 ? "\n" : "";
|
|
1166
|
+
const tailSep = tailBytes > 0 && !this.#artifactTailRing.startsWith("\n") ? "\n" : "";
|
|
1167
|
+
const notice =
|
|
1168
|
+
`${headSep}[ARTIFACT TRUNCATED: kept first ${formatBytes(headWritten)} + last ${formatBytes(tailBytes)} ` +
|
|
1169
|
+
`of ${formatBytes(totalCapped)}; ${formatBytes(droppedBytes)} elided from the middle]${tailSep}`;
|
|
1170
|
+
this.#file.sink.write(notice);
|
|
1171
|
+
}
|
|
1172
|
+
if (tailBytes > 0) {
|
|
1173
|
+
this.#file.sink.write(this.#artifactTailRing);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
964
1177
|
async dump(notice?: string): Promise<OutputSummary> {
|
|
965
1178
|
const noticeLine = notice ? `[${notice}]\n` : "";
|
|
966
1179
|
|
|
@@ -973,7 +1186,10 @@ export class OutputSink {
|
|
|
973
1186
|
}
|
|
974
1187
|
const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
|
|
975
1188
|
|
|
976
|
-
if (this.#file)
|
|
1189
|
+
if (this.#file) {
|
|
1190
|
+
this.#flushArtifactTailIfCapped();
|
|
1191
|
+
await this.#file.sink.end();
|
|
1192
|
+
}
|
|
977
1193
|
|
|
978
1194
|
// Compose the visible output. With head retention, splice head + marker
|
|
979
1195
|
// + tail when content was elided. Otherwise return the rolling buffer.
|
|
@@ -5,6 +5,30 @@ import type { AcpBuiltinSlashCommandResult, SlashCommandRuntime } from "./types"
|
|
|
5
5
|
|
|
6
6
|
export type { AcpBuiltinSlashCommandResult } from "./types";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* All names (primary + aliases) that are reserved by ACP builtins. Used to
|
|
10
|
+
* filter out extension commands that would shadow a builtin or its alias at
|
|
11
|
+
* dispatch time (e.g. `models` is an alias for `/model`, so an extension
|
|
12
|
+
* registering `models` would appear in the palette but execute the builtin).
|
|
13
|
+
*/
|
|
14
|
+
export const ACP_BUILTIN_RESERVED_NAMES: ReadonlySet<string> = new Set(
|
|
15
|
+
BUILTIN_SLASH_COMMANDS_INTERNAL.filter(c => c.handle !== undefined).flatMap(c => [c.name, ...(c.aliases ?? [])]),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether an extension command named `name` would be captured by ACP builtin
|
|
20
|
+
* dispatch before reaching the extension handler. Beyond exact name/alias
|
|
21
|
+
* collisions, `parseSlashCommand` treats `:` as a name/args separator, so a
|
|
22
|
+
* colon-namespaced name whose prefix is a handled builtin (e.g. `model:foo`)
|
|
23
|
+
* executes the `/model` builtin with `foo` as args. Such names must not be
|
|
24
|
+
* advertised to ACP clients.
|
|
25
|
+
*/
|
|
26
|
+
export function isAcpBuiltinShadowedName(name: string): boolean {
|
|
27
|
+
if (ACP_BUILTIN_RESERVED_NAMES.has(name)) return true;
|
|
28
|
+
const colon = name.indexOf(":");
|
|
29
|
+
return colon !== -1 && ACP_BUILTIN_RESERVED_NAMES.has(name.slice(0, colon));
|
|
30
|
+
}
|
|
31
|
+
|
|
8
32
|
/**
|
|
9
33
|
* Commands advertised to ACP clients. Entries without a text-mode `handle`
|
|
10
34
|
* (e.g. `/quit`, `/login`, dashboards) are filtered out so the client doesn't
|
|
@@ -82,6 +82,23 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
82
82
|
runtime.ctx.editor.setText("");
|
|
83
83
|
},
|
|
84
84
|
},
|
|
85
|
+
{
|
|
86
|
+
name: "setup",
|
|
87
|
+
aliases: ["providers"],
|
|
88
|
+
description: "Open provider setup",
|
|
89
|
+
allowArgs: true,
|
|
90
|
+
subcommands: [{ name: "providers", description: "Configure sign-in and web search providers" }],
|
|
91
|
+
handleTui: async (command, runtime) => {
|
|
92
|
+
const args = command.args.trim().toLowerCase();
|
|
93
|
+
const opensProviders = args === "" || args === "providers";
|
|
94
|
+
if (opensProviders) {
|
|
95
|
+
await runtime.ctx.showProviderSetup();
|
|
96
|
+
} else {
|
|
97
|
+
runtime.ctx.showWarning(`Usage: /${command.name} [providers]`);
|
|
98
|
+
}
|
|
99
|
+
runtime.ctx.editor.setText("");
|
|
100
|
+
},
|
|
101
|
+
},
|
|
85
102
|
{
|
|
86
103
|
name: "plan",
|
|
87
104
|
description: "Toggle plan mode (agent plans before executing)",
|
|
@@ -1697,10 +1714,13 @@ for (const command of BUILTIN_SLASH_COMMAND_REGISTRY) {
|
|
|
1697
1714
|
}
|
|
1698
1715
|
}
|
|
1699
1716
|
|
|
1717
|
+
export const BUILTIN_SLASH_COMMAND_RESERVED_NAMES: ReadonlySet<string> = new Set(BUILTIN_SLASH_COMMAND_LOOKUP.keys());
|
|
1718
|
+
|
|
1700
1719
|
/** Builtin command metadata used for slash-command autocomplete and help text. */
|
|
1701
1720
|
export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BUILTIN_SLASH_COMMAND_REGISTRY.map(
|
|
1702
1721
|
command => ({
|
|
1703
1722
|
name: command.name,
|
|
1723
|
+
aliases: command.aliases,
|
|
1704
1724
|
description: command.description,
|
|
1705
1725
|
subcommands: command.subcommands,
|
|
1706
1726
|
inlineHint: command.inlineHint,
|
|
@@ -14,6 +14,7 @@ export interface SubcommandDef {
|
|
|
14
14
|
/** Declarative builtin slash command metadata used by autocomplete and help UI. */
|
|
15
15
|
export interface BuiltinSlashCommand {
|
|
16
16
|
name: string;
|
|
17
|
+
aliases?: string[];
|
|
17
18
|
description: string;
|
|
18
19
|
/** Subcommands for dropdown completion (e.g. /mcp add, /mcp list). */
|
|
19
20
|
subcommands?: SubcommandDef[];
|
|
@@ -82,7 +83,6 @@ export interface TuiSlashCommandRuntime {
|
|
|
82
83
|
|
|
83
84
|
/** Unified slash-command spec consumed by both TUI and ACP dispatchers. */
|
|
84
85
|
export interface SlashCommandSpec extends BuiltinSlashCommand {
|
|
85
|
-
aliases?: string[];
|
|
86
86
|
/** When false, the dispatcher refuses to handle invocations that include arguments. */
|
|
87
87
|
allowArgs?: boolean;
|
|
88
88
|
/**
|
package/src/system-prompt.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger, prom
|
|
|
8
8
|
import { $ } from "bun";
|
|
9
9
|
import { contextFileCapability } from "./capability/context-file";
|
|
10
10
|
import { systemPromptCapability } from "./capability/system-prompt";
|
|
11
|
+
import { findConfigFile } from "./config";
|
|
11
12
|
import type { SkillsSettings } from "./config/settings";
|
|
12
13
|
import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
|
|
13
14
|
import { expandAtImports } from "./discovery/at-imports";
|
|
@@ -208,6 +209,19 @@ async function getEnvironmentInfo(): Promise<Array<{ label: string; value: strin
|
|
|
208
209
|
return entries.filter((e): e is { label: string; value: string } => !!e.value);
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
/** Discover TITLE_SYSTEM.md file for automatic session-title prompt overrides */
|
|
213
|
+
export function discoverTitleSystemPromptFile(cwd?: string): string | undefined {
|
|
214
|
+
const projectPath = findConfigFile("TITLE_SYSTEM.md", { user: false, cwd });
|
|
215
|
+
if (projectPath) {
|
|
216
|
+
return projectPath;
|
|
217
|
+
}
|
|
218
|
+
const globalPath = findConfigFile("TITLE_SYSTEM.md", { user: true, cwd });
|
|
219
|
+
if (globalPath) {
|
|
220
|
+
return globalPath;
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
211
225
|
/** Resolve input as file path or literal string */
|
|
212
226
|
export async function resolvePromptInput(input: string | undefined, description: string): Promise<string | undefined> {
|
|
213
227
|
if (!input) {
|