@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.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 +79 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +10 -10
- package/scripts/build-binary.ts +5 -0
- 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/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 +10 -29
- package/src/commands/commit.ts +10 -0
- 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 +44 -3
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +82 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/discovery/claude-plugins.ts +19 -7
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -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/get-commands-handler.ts +77 -0
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- 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/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- 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 +11 -12
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +327 -95
- 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/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +93 -8
- 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 +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +13 -0
- 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 +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-mode.ts +41 -88
- package/src/modes/rpc/rpc-types.ts +57 -0
- package/src/modes/runtime-init.ts +116 -0
- package/src/modes/theme/defaults/dark-poimandres.json +3 -0
- package/src/modes/theme/defaults/light-poimandres.json +3 -0
- package/src/modes/theme/theme.ts +24 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- 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/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/github.md +4 -4
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +101 -117
- package/src/prompts/tools/read.md +55 -36
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- 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/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/discovery.ts +5 -2
- package/src/task/executor.ts +19 -8
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- 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-command-fixup.ts +47 -0
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +66 -19
- 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/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +12 -2
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +44 -10
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/job.ts +16 -7
- package/src/tools/output-meta.ts +202 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +548 -237
- 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/ssh.ts +3 -2
- package/src/tools/write.ts +64 -9
- 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/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +40 -95
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
- 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
|
}
|
|
@@ -76,6 +76,24 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
76
76
|
runtime.ctx.editor.setText("");
|
|
77
77
|
},
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
name: "goal",
|
|
81
|
+
description: "Toggle goal mode (persistent autonomous objective for this session)",
|
|
82
|
+
subcommands: [
|
|
83
|
+
{ name: "set", description: "Set or replace the goal", usage: "<objective>" },
|
|
84
|
+
{ name: "show", description: "Show current goal details" },
|
|
85
|
+
{ name: "pause", description: "Pause the current goal" },
|
|
86
|
+
{ name: "resume", description: "Resume a paused goal" },
|
|
87
|
+
{ name: "drop", description: "Drop the current goal" },
|
|
88
|
+
{ name: "budget", description: "Adjust the token budget", usage: "<N|off>" },
|
|
89
|
+
],
|
|
90
|
+
inlineHint: "[objective]",
|
|
91
|
+
allowArgs: true,
|
|
92
|
+
handleTui: async (command, runtime) => {
|
|
93
|
+
await runtime.ctx.handleGoalModeCommand(command.args || undefined);
|
|
94
|
+
runtime.ctx.editor.setText("");
|
|
95
|
+
},
|
|
96
|
+
},
|
|
79
97
|
{
|
|
80
98
|
name: "loop",
|
|
81
99
|
description:
|
package/src/ssh/ssh-executor.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { logger, ptree } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { Settings } from "../config/settings";
|
|
2
3
|
import { OutputSink } from "../session/streaming-output";
|
|
4
|
+
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
|
3
5
|
import { buildRemoteCommand, ensureConnection, ensureHostInfo, type SSHConnectionTarget } from "./connection-manager";
|
|
4
6
|
import { hasSshfs, mountRemote } from "./sshfs-mount";
|
|
5
7
|
|
|
@@ -83,10 +85,13 @@ export async function executeSSH(
|
|
|
83
85
|
stderr: "full",
|
|
84
86
|
});
|
|
85
87
|
|
|
88
|
+
const settings = await Settings.init();
|
|
86
89
|
const sink = new OutputSink({
|
|
87
90
|
onChunk: options?.onChunk,
|
|
88
91
|
artifactPath: options?.artifactPath,
|
|
89
92
|
artifactId: options?.artifactId,
|
|
93
|
+
headBytes: resolveOutputSinkHeadBytes(settings),
|
|
94
|
+
maxColumns: resolveOutputMaxColumns(settings),
|
|
90
95
|
});
|
|
91
96
|
|
|
92
97
|
const streams = [child.stdout.pipeTo(sink.createInput())];
|
package/src/system-prompt.ts
CHANGED
|
@@ -530,9 +530,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
530
530
|
description: tools?.get(name)?.description ?? "",
|
|
531
531
|
}));
|
|
532
532
|
|
|
533
|
-
// Filter skills
|
|
533
|
+
// Filter skills for the rendered system prompt:
|
|
534
|
+
// - require the `read` tool so the model can actually fetch skill content;
|
|
535
|
+
// - drop skills with frontmatter `hide: true` (still loadable via skill:// and /skill:<name>).
|
|
534
536
|
const hasRead = tools?.has("read");
|
|
535
|
-
const filteredSkills = hasRead ? skills : [];
|
|
537
|
+
const filteredSkills = hasRead ? skills.filter(skill => skill.hide !== true) : [];
|
|
536
538
|
|
|
537
539
|
const effectiveSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [
|
|
538
540
|
resolvedCustomPrompt,
|
package/src/task/discovery.ts
CHANGED
|
@@ -15,6 +15,7 @@ import * as fs from "node:fs/promises";
|
|
|
15
15
|
import * as os from "node:os";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
18
|
+
import { isProviderEnabled } from "../capability";
|
|
18
19
|
import { findAllNearestProjectConfigDirs, getConfigDirs } from "../config";
|
|
19
20
|
import { listClaudePluginRoots } from "../discovery/helpers";
|
|
20
21
|
import { loadBundledAgents, parseAgent } from "./agents";
|
|
@@ -87,8 +88,10 @@ export async function discoverAgents(cwd: string, home: string = os.homedir()):
|
|
|
87
88
|
if (user) orderedDirs.push({ dir: user.path, source: "user" });
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
// Load agents from Claude Code marketplace plugins
|
|
91
|
-
const { roots: pluginRoots } =
|
|
91
|
+
// Load agents from Claude Code marketplace plugins (respects disabledProviders)
|
|
92
|
+
const { roots: pluginRoots } = isProviderEnabled("claude-plugins")
|
|
93
|
+
? await listClaudePluginRoots(home, resolvedCwd)
|
|
94
|
+
: { roots: [] };
|
|
92
95
|
const sortedPluginRoots = [...pluginRoots].sort((a, b) => {
|
|
93
96
|
if (a.scope === b.scope) return 0;
|
|
94
97
|
return a.scope === "project" ? -1 : 1;
|
package/src/task/executor.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { Settings } from "../config/settings";
|
|
|
16
16
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
17
17
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
18
18
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
19
|
+
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
|
19
20
|
import type { Skill } from "../extensibility/skills";
|
|
20
21
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
21
22
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
@@ -379,21 +380,29 @@ function firstNumberField(record: Record<string, unknown>, keys: string[]): numb
|
|
|
379
380
|
}
|
|
380
381
|
|
|
381
382
|
/**
|
|
382
|
-
*
|
|
383
|
+
* Tokens for progress display: input + output + cacheWrite per turn.
|
|
384
|
+
*
|
|
385
|
+
* Deliberately excludes cacheRead. With prompt caching, cacheRead in each turn
|
|
386
|
+
* equals the full cached context (potentially hundreds of KB), so summing it
|
|
387
|
+
* across all turns produces a cumulative total that is N×context_size — far
|
|
388
|
+
* larger than the context window and misleading as a "work done" metric.
|
|
389
|
+
* cacheWrite is kept because each byte is written once, not repeated per turn.
|
|
390
|
+
* The cost segment handles billing; dedicated cache_read/cache_write segments
|
|
391
|
+
* handle cache-specific monitoring.
|
|
383
392
|
*/
|
|
384
393
|
function getUsageTokens(usage: unknown): number {
|
|
385
394
|
if (!usage || typeof usage !== "object") return 0;
|
|
386
395
|
const record = usage as Record<string, unknown>;
|
|
387
396
|
|
|
388
|
-
const totalTokens = firstNumberField(record, ["totalTokens", "total_tokens"]);
|
|
389
|
-
if (totalTokens !== undefined && totalTokens > 0) return totalTokens;
|
|
390
|
-
|
|
391
397
|
const input = firstNumberField(record, ["input", "input_tokens", "inputTokens"]) ?? 0;
|
|
392
398
|
const output = firstNumberField(record, ["output", "output_tokens", "outputTokens"]) ?? 0;
|
|
393
|
-
const cacheRead = firstNumberField(record, ["cacheRead", "cache_read", "cacheReadTokens"]) ?? 0;
|
|
394
399
|
const cacheWrite = firstNumberField(record, ["cacheWrite", "cache_write", "cacheWriteTokens"]) ?? 0;
|
|
395
|
-
|
|
396
|
-
|
|
400
|
+
const computed = input + output + cacheWrite;
|
|
401
|
+
if (computed > 0) return computed;
|
|
402
|
+
// Fallback for providers that only surface a pre-summed total without individual
|
|
403
|
+
// field breakdown. This total includes cacheRead, but returning it is still better
|
|
404
|
+
// than silently showing 0 for those providers.
|
|
405
|
+
return firstNumberField(record, ["totalTokens", "total_tokens"]) ?? 0;
|
|
397
406
|
}
|
|
398
407
|
|
|
399
408
|
/**
|
|
@@ -497,6 +506,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
497
506
|
recentOutput: [],
|
|
498
507
|
toolCount: 0,
|
|
499
508
|
tokens: 0,
|
|
509
|
+
cost: 0,
|
|
500
510
|
durationMs: 0,
|
|
501
511
|
modelOverride,
|
|
502
512
|
};
|
|
@@ -892,6 +902,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
892
902
|
accumulatedUsage.cost.cacheRead += getNumberField(costRecord, "cacheRead") ?? 0;
|
|
893
903
|
accumulatedUsage.cost.cacheWrite += getNumberField(costRecord, "cacheWrite") ?? 0;
|
|
894
904
|
accumulatedUsage.cost.total += getNumberField(costRecord, "total") ?? 0;
|
|
905
|
+
progress.cost = accumulatedUsage.cost.total;
|
|
895
906
|
}
|
|
896
907
|
}
|
|
897
908
|
// Accumulate tokens for progress display
|
|
@@ -1109,7 +1120,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1109
1120
|
getAllTools: () => session.getAllToolNames(),
|
|
1110
1121
|
setActiveTools: (toolNames: string[]) =>
|
|
1111
1122
|
session.setActiveToolsByName(toolNames.filter(name => !parentOwnedToolNames.has(name))),
|
|
1112
|
-
getCommands: () =>
|
|
1123
|
+
getCommands: () => getSessionSlashCommands(session),
|
|
1113
1124
|
setModel: model => runExtensionSetModel(session, model),
|
|
1114
1125
|
getThinkingLevel: () => session.thinkingLevel,
|
|
1115
1126
|
setThinkingLevel: level => session.setThinkingLevel(level),
|
package/src/task/index.ts
CHANGED
|
@@ -306,6 +306,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
306
306
|
recentOutput: [],
|
|
307
307
|
toolCount: 0,
|
|
308
308
|
tokens: 0,
|
|
309
|
+
cost: 0,
|
|
309
310
|
durationMs: 0,
|
|
310
311
|
});
|
|
311
312
|
}
|
|
@@ -390,6 +391,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
390
391
|
: "failed";
|
|
391
392
|
progress.durationMs = singleResult?.durationMs ?? Math.max(0, Date.now() - startedAt);
|
|
392
393
|
progress.tokens = singleResult?.tokens ?? 0;
|
|
394
|
+
progress.cost = singleResult?.usage?.cost.total ?? 0;
|
|
393
395
|
progress.extractedToolData = singleResult?.extractedToolData;
|
|
394
396
|
}
|
|
395
397
|
completedJobs += 1;
|
|
@@ -831,6 +833,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
831
833
|
recentOutput: [],
|
|
832
834
|
toolCount: 0,
|
|
833
835
|
tokens: 0,
|
|
836
|
+
cost: 0,
|
|
834
837
|
durationMs: 0,
|
|
835
838
|
modelOverride,
|
|
836
839
|
description: taskItem.description,
|