dsh-code 0.7.0 → 0.8.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/README.en.md +20 -6
- package/README.md +20 -6
- package/lib/index.mjs +2685 -622
- package/lib/types/app.d.ts +77 -1
- package/lib/types/history.d.ts +15 -4
- package/lib/types/index.d.ts +48 -0
- package/lib/types/kernel-panels.d.ts +7 -0
- package/lib/types/permissions.d.ts +37 -0
- package/lib/types/presets.d.ts +2 -0
- package/lib/types/provider-settings.d.ts +144 -0
- package/lib/types/questions.d.ts +2 -0
- package/lib/types/render/animations.d.ts +8 -6
- package/lib/types/render/lines.d.ts +6 -0
- package/lib/types/render/markdown.d.ts +3 -3
- package/lib/types/render/projection.d.ts +95 -3
- package/lib/types/render/status.d.ts +26 -36
- package/lib/types/render/text.d.ts +14 -7
- package/lib/types/render/tool-detail.d.ts +3 -1
- package/lib/types/render/tool-preview.d.ts +4 -1
- package/lib/types/session-directory.d.ts +15 -0
- package/lib/types/store.d.ts +13 -2
- package/lib/types/version.d.ts +5 -0
- package/package.json +1 -1
- package/src/app.ts +847 -150
- package/src/approval.ts +11 -2
- package/src/history.ts +20 -5
- package/src/index.ts +402 -159
- package/src/kernel-panels.ts +45 -8
- package/src/permissions.ts +85 -0
- package/src/presets.ts +12 -0
- package/src/provider-settings.ts +520 -0
- package/src/questions.ts +15 -5
- package/src/render/animations.ts +32 -18
- package/src/render/lines.ts +21 -6
- package/src/render/markdown.ts +302 -4
- package/src/render/projection.ts +665 -10
- package/src/render/status.ts +68 -162
- package/src/render/text.ts +28 -9
- package/src/render/tool-detail.ts +81 -40
- package/src/render/tool-preview.ts +18 -2
- package/src/session-directory.ts +44 -5
- package/src/skills.ts +8 -4
- package/src/store.ts +26 -8
- package/src/version.ts +16 -0
package/lib/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
|
8
8
|
import { basename, dirname, join, resolve } from "node:path";
|
|
9
9
|
import z from "@deepseek-ai/schemastery";
|
|
10
10
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
11
|
-
import { MessageId, ReasoningEffortId, assertNever, boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
11
|
+
import { MessageId, ReasoningEffortId, assertNever, boundContextSummary, createUserMessage, normalizeApiKey } from "@deepseek-ai/dsh-llm";
|
|
12
12
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
13
13
|
import { PassThrough, Stream } from "node:stream";
|
|
14
14
|
import process$1, { cwd, env } from "node:process";
|
|
@@ -24356,22 +24356,35 @@ function followInspectorCursor(cursor, previousLength, nextLength) {
|
|
|
24356
24356
|
* tool payloads, skill descriptions). Control characters — including ANSI
|
|
24357
24357
|
* CSI/OSC escape sequences — would otherwise pass through Ink into the
|
|
24358
24358
|
* terminal, letting output rewrite the screen or inject prompts. Newlines
|
|
24359
|
-
*
|
|
24360
|
-
*
|
|
24359
|
+
* survive; everything else in C0/C1 plus DEL becomes a visible `\xNN`
|
|
24360
|
+
* escape, and bidi overrides / invisible format controls / Unicode line and
|
|
24361
|
+
* paragraph separators become a visible `\uXXXX` escape (terminal emulators
|
|
24362
|
+
* that render bidirectional text would otherwise reorder the displayed
|
|
24363
|
+
* glyphs and let a command read as something it is not).
|
|
24361
24364
|
*
|
|
24362
24365
|
* @module @deepseek-ai/dsh-code/render/text
|
|
24363
24366
|
*/
|
|
24364
24367
|
/** C0 controls except tab (0x09) and newline (0x0a), plus DEL and C1. */
|
|
24365
24368
|
const CONTROL_ESCAPE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu;
|
|
24366
24369
|
/**
|
|
24367
|
-
*
|
|
24368
|
-
*
|
|
24370
|
+
* Bidi overrides and isolates (U+202A-202E, U+2066-2069), the Arabic Letter
|
|
24371
|
+
* Mark (U+061C), directional and zero-width format characters (U+200B,
|
|
24372
|
+
* U+200E/200F, U+2060-2064, U+FEFF), and Unicode line/paragraph separators
|
|
24373
|
+
* (U+2028/2029). Terminal emulators with bidi support (Windows Terminal,
|
|
24374
|
+
* iTerm2, kitty, WezTerm) reorder or hide these, so they must never reach
|
|
24375
|
+
* the terminal raw.
|
|
24376
|
+
*/
|
|
24377
|
+
const INVISIBLE_ESCAPE = /[\u061c\u200b\u200e\u200f\u2028\u2029\u202a-\u202e\u2060-\u2064\u2066-\u206f\ufeff]/gu;
|
|
24378
|
+
/**
|
|
24379
|
+
* Escape control and deceptive characters so externally sourced text cannot
|
|
24380
|
+
* drive the terminal. C0/C1/DEL render as a literal `\xNN` escape; bidi,
|
|
24381
|
+
* invisible-format, and separator controls render as a literal `\uXXXX`
|
|
24382
|
+
* escape. Newlines and tabs survive (budgeted callers normalize tabs).
|
|
24369
24383
|
* @param text - raw text from a session event, tool payload, or catalog.
|
|
24370
|
-
* @returns text with every
|
|
24371
|
-
* as a literal `\xNN` escape.
|
|
24384
|
+
* @returns display-safe text with every injectable character made visible.
|
|
24372
24385
|
*/
|
|
24373
24386
|
function displayText(text) {
|
|
24374
|
-
return text.replace(CONTROL_ESCAPE, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`);
|
|
24387
|
+
return text.replace(CONTROL_ESCAPE, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`).replace(INVISIBLE_ESCAPE, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
24375
24388
|
}
|
|
24376
24389
|
/** Collapse external text to one terminal-safe logical row. */
|
|
24377
24390
|
function singleLineText(text) {
|
|
@@ -24423,7 +24436,10 @@ function previousCharacter(text, end) {
|
|
|
24423
24436
|
* Keep only the newest display-safe text that fits a terminal rectangle.
|
|
24424
24437
|
* The scan walks backward and stops as soon as the suffix is full, so a long
|
|
24425
24438
|
* reasoning stream does not rescan its entire accumulated prefix per chunk.
|
|
24426
|
-
* Explicit newlines and terminal wrapping both consume rows
|
|
24439
|
+
* Explicit newlines and terminal wrapping both consume rows; tabs expand to
|
|
24440
|
+
* two spaces so terminal tab stops (which render at contextual column 8
|
|
24441
|
+
* boundaries, not at the budgeted cell count) cannot inflate the physical
|
|
24442
|
+
* row count of the live region.
|
|
24427
24443
|
* @param text - raw externally sourced text.
|
|
24428
24444
|
* @param columns - available terminal columns.
|
|
24429
24445
|
* @param rows - available terminal rows.
|
|
@@ -24446,7 +24462,7 @@ function displayTail(text, columns, rows) {
|
|
|
24446
24462
|
end = previous.start;
|
|
24447
24463
|
continue;
|
|
24448
24464
|
}
|
|
24449
|
-
const safe = displayText(previous.char);
|
|
24465
|
+
const safe = previous.char === " " ? " " : displayText(previous.char);
|
|
24450
24466
|
const width = cellWidth(safe);
|
|
24451
24467
|
if (used > 0 && used + width > columnLimit) {
|
|
24452
24468
|
if (row >= rowLimit) break;
|
|
@@ -24551,12 +24567,29 @@ const WHALE_GLYPH = [
|
|
|
24551
24567
|
" ▀▀███████▀▀ ▀▀▀ "
|
|
24552
24568
|
];
|
|
24553
24569
|
//#endregion
|
|
24570
|
+
//#region src/version.ts
|
|
24571
|
+
/** Installed dsh-code version exposed by the terminal header. */
|
|
24572
|
+
/** Read one package manifest version without making terminal startup depend on it. */
|
|
24573
|
+
function readPackageVersion(manifest = new URL("../package.json", import.meta.url)) {
|
|
24574
|
+
try {
|
|
24575
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
24576
|
+
return typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : "0.0.0";
|
|
24577
|
+
} catch {
|
|
24578
|
+
return "0.0.0";
|
|
24579
|
+
}
|
|
24580
|
+
}
|
|
24581
|
+
/** Version of the installed dsh-code package. */
|
|
24582
|
+
const DSH_CODE_VERSION = readPackageVersion();
|
|
24583
|
+
//#endregion
|
|
24554
24584
|
//#region src/render/tool-preview.ts
|
|
24555
24585
|
/**
|
|
24556
24586
|
* Bounded preview line for a tool invocation's raw JSON arguments: the first
|
|
24557
24587
|
* human-meaningful string among the well-known keys (command, path, query, …)
|
|
24558
24588
|
* with a fallback to the bounded raw JSON. Shared by the tool card in the
|
|
24559
|
-
* transcript and the approval bar's command preview.
|
|
24589
|
+
* transcript and the approval bar's command preview. Arguments longer than
|
|
24590
|
+
* {@link MAX_PARSE_CHARS} are never parsed: the preview is a display concern,
|
|
24591
|
+
* and a synchronous `JSON.parse` plus string copies of an unbounded model
|
|
24592
|
+
* payload must not run on the approval or projection paths.
|
|
24560
24593
|
*
|
|
24561
24594
|
* @module @deepseek-ai/dsh-code/render/tool-preview
|
|
24562
24595
|
*/
|
|
@@ -24570,6 +24603,16 @@ const PREVIEW_KEYS = [
|
|
|
24570
24603
|
"query"
|
|
24571
24604
|
];
|
|
24572
24605
|
/**
|
|
24606
|
+
* Raw arguments longer than this are skipped without parsing and fall back
|
|
24607
|
+
* to the bounded raw preview. Well above any realistic command/path/query
|
|
24608
|
+
* string while keeping the synchronous parse cost negligible.
|
|
24609
|
+
*/
|
|
24610
|
+
const MAX_PARSE_CHARS = 4096;
|
|
24611
|
+
/** Bounded raw-arguments fallback shared by the skip-parse and parse-failure paths. */
|
|
24612
|
+
function boundedRawPreview(args) {
|
|
24613
|
+
return args.length > 80 ? `${args.slice(0, 77)}...` : args;
|
|
24614
|
+
}
|
|
24615
|
+
/**
|
|
24573
24616
|
* Resolve one bounded preview for raw tool arguments.
|
|
24574
24617
|
* @param args - raw JSON arguments string as the model produced it.
|
|
24575
24618
|
* @param toolName - the tool the arguments belong to (fallback label).
|
|
@@ -24577,6 +24620,7 @@ const PREVIEW_KEYS = [
|
|
|
24577
24620
|
*/
|
|
24578
24621
|
function toolArgumentsPreview(args, toolName) {
|
|
24579
24622
|
if (args === "") return toolName;
|
|
24623
|
+
if (args.length > MAX_PARSE_CHARS) return boundedRawPreview(args);
|
|
24580
24624
|
try {
|
|
24581
24625
|
const parsed = JSON.parse(args);
|
|
24582
24626
|
if (parsed !== null && typeof parsed === "object") {
|
|
@@ -24587,7 +24631,7 @@ function toolArgumentsPreview(args, toolName) {
|
|
|
24587
24631
|
}
|
|
24588
24632
|
}
|
|
24589
24633
|
} catch {}
|
|
24590
|
-
return args
|
|
24634
|
+
return boundedRawPreview(args);
|
|
24591
24635
|
}
|
|
24592
24636
|
//#endregion
|
|
24593
24637
|
//#region src/render/tool-detail.ts
|
|
@@ -24609,6 +24653,9 @@ const MAX_READ_LINES = 120;
|
|
|
24609
24653
|
const MAX_SOURCES = 10;
|
|
24610
24654
|
const MAX_RAW_CHARS = 6e3;
|
|
24611
24655
|
const MAX_LINE_COLUMNS = 240;
|
|
24656
|
+
/** Hard caps on adversarial `tool/result.meta` before any row is built. */
|
|
24657
|
+
const MAX_DIFFS = 8;
|
|
24658
|
+
const MAX_DIFF_TEXT_CHARS = 24e3;
|
|
24612
24659
|
/** Truncate one line to the visible-column budget with an ellipsis marker. */
|
|
24613
24660
|
function clipLine(text) {
|
|
24614
24661
|
return text.length > MAX_LINE_COLUMNS ? `${text.slice(0, 239)}…` : text;
|
|
@@ -24622,35 +24669,51 @@ function toLines(text) {
|
|
|
24622
24669
|
* Render one change as removed-then-added rows, hunked by common prefix and
|
|
24623
24670
|
* suffix. A null before-image (file create) renders as pure additions. The
|
|
24624
24671
|
* budget caps emitted rows and reports the cut, so a whole-file overwrite
|
|
24625
|
-
* never floods the transcript.
|
|
24672
|
+
* never floods the transcript. Inputs are hard-capped before line splitting
|
|
24673
|
+
* and the row list is built incrementally up to the budget — a crafted or
|
|
24674
|
+
* replayed giant diff cannot force a full intermediate rows array.
|
|
24626
24675
|
* @param oldText - prior content, or null for a create.
|
|
24627
24676
|
* @param newText - content after the change.
|
|
24628
24677
|
* @param budget - maximum rows to emit.
|
|
24629
24678
|
* @returns the bounded rows and whether they were cut.
|
|
24630
24679
|
*/
|
|
24631
24680
|
function diffRows(oldText, newText, budget) {
|
|
24632
|
-
const
|
|
24633
|
-
const
|
|
24681
|
+
const oldRaw = oldText ?? "";
|
|
24682
|
+
const newRaw = newText;
|
|
24683
|
+
let inputTruncated = false;
|
|
24684
|
+
let oldSource = oldRaw;
|
|
24685
|
+
let newSource = newRaw;
|
|
24686
|
+
const combined = oldRaw.length + newRaw.length;
|
|
24687
|
+
if (combined > MAX_DIFF_TEXT_CHARS) {
|
|
24688
|
+
inputTruncated = true;
|
|
24689
|
+
const oldShare = Math.min(oldRaw.length, Math.floor(MAX_DIFF_TEXT_CHARS * oldRaw.length / combined));
|
|
24690
|
+
const newShare = Math.min(newRaw.length, MAX_DIFF_TEXT_CHARS - oldShare);
|
|
24691
|
+
oldSource = oldRaw.slice(0, oldShare);
|
|
24692
|
+
newSource = newRaw.slice(0, newShare);
|
|
24693
|
+
}
|
|
24694
|
+
const oldLines = oldText === null ? [] : toLines(oldSource);
|
|
24695
|
+
const newLines = toLines(newSource);
|
|
24634
24696
|
let prefix = 0;
|
|
24635
24697
|
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) prefix += 1;
|
|
24636
24698
|
let suffix = 0;
|
|
24637
24699
|
while (suffix < oldLines.length - prefix && suffix < newLines.length - prefix && oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]) suffix += 1;
|
|
24638
|
-
const
|
|
24639
|
-
const
|
|
24640
|
-
const
|
|
24700
|
+
const removedCount = oldLines.length - prefix - suffix;
|
|
24701
|
+
const addedCount = newLines.length - prefix - suffix;
|
|
24702
|
+
const truncated = inputTruncated || removedCount + addedCount > budget;
|
|
24703
|
+
const rows = [];
|
|
24704
|
+
const removedLimit = Math.min(removedCount, Math.max(0, budget));
|
|
24705
|
+
for (let index = 0; index < removedLimit; index += 1) rows.push({
|
|
24641
24706
|
mark: "-",
|
|
24642
|
-
text: clipLine(
|
|
24643
|
-
})
|
|
24707
|
+
text: clipLine(oldLines[prefix + index] ?? "")
|
|
24708
|
+
});
|
|
24709
|
+
const addedLimit = Math.min(addedCount, Math.max(0, budget - removedLimit));
|
|
24710
|
+
for (let index = 0; index < addedLimit; index += 1) rows.push({
|
|
24644
24711
|
mark: "+",
|
|
24645
|
-
text: clipLine(
|
|
24646
|
-
})
|
|
24647
|
-
if (rows.length <= budget) return {
|
|
24648
|
-
lines: rows,
|
|
24649
|
-
truncated: false
|
|
24650
|
-
};
|
|
24712
|
+
text: clipLine(newLines[prefix + index] ?? "")
|
|
24713
|
+
});
|
|
24651
24714
|
return {
|
|
24652
|
-
lines: rows
|
|
24653
|
-
truncated
|
|
24715
|
+
lines: rows,
|
|
24716
|
+
truncated
|
|
24654
24717
|
};
|
|
24655
24718
|
}
|
|
24656
24719
|
/** Whether `value` is a valid upstream FileDiff (defensive narrowing). */
|
|
@@ -24684,44 +24747,60 @@ function toolResultDetail(meta, rawText) {
|
|
|
24684
24747
|
if (typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
|
|
24685
24748
|
const record = meta;
|
|
24686
24749
|
const diffs = record["diffs"];
|
|
24687
|
-
if (Array.isArray(diffs) && diffs.length > 0
|
|
24688
|
-
const
|
|
24689
|
-
|
|
24690
|
-
|
|
24691
|
-
|
|
24692
|
-
|
|
24693
|
-
|
|
24694
|
-
|
|
24695
|
-
|
|
24750
|
+
if (Array.isArray(diffs) && diffs.length > 0) {
|
|
24751
|
+
const capped = diffs.slice(0, MAX_DIFFS);
|
|
24752
|
+
if (capped.every(isFileDiff)) {
|
|
24753
|
+
const budget = Math.max(8, Math.floor(MAX_DIFF_LINES / capped.length));
|
|
24754
|
+
const dropped = diffs.length > capped.length;
|
|
24755
|
+
return {
|
|
24756
|
+
kind: "diff",
|
|
24757
|
+
diffs: capped.map((diff, index) => {
|
|
24758
|
+
const rows = diffRows(diff.oldText, diff.newText, budget);
|
|
24759
|
+
return dropped && index === capped.length - 1 ? {
|
|
24760
|
+
path: diff.path,
|
|
24761
|
+
...rows,
|
|
24762
|
+
truncated: true
|
|
24763
|
+
} : {
|
|
24764
|
+
path: diff.path,
|
|
24765
|
+
...rows
|
|
24766
|
+
};
|
|
24767
|
+
})
|
|
24768
|
+
};
|
|
24769
|
+
}
|
|
24696
24770
|
}
|
|
24697
24771
|
const { path, offset, lines, totalLines } = record;
|
|
24698
|
-
if (typeof path === "string" && Number.isInteger(offset) && offset >= 1 && Number.isInteger(totalLines) && totalLines >= 0 && Array.isArray(lines)
|
|
24699
|
-
const window = lines;
|
|
24700
|
-
|
|
24701
|
-
|
|
24702
|
-
|
|
24703
|
-
|
|
24704
|
-
|
|
24705
|
-
|
|
24706
|
-
|
|
24707
|
-
|
|
24708
|
-
|
|
24709
|
-
|
|
24710
|
-
|
|
24711
|
-
|
|
24772
|
+
if (typeof path === "string" && Number.isInteger(offset) && offset >= 1 && Number.isInteger(totalLines) && totalLines >= 0 && Array.isArray(lines)) {
|
|
24773
|
+
const window = lines.slice(0, MAX_READ_LINES);
|
|
24774
|
+
if (window.every(isReadLine)) {
|
|
24775
|
+
const truncated = lines.length > MAX_READ_LINES;
|
|
24776
|
+
return {
|
|
24777
|
+
kind: "read",
|
|
24778
|
+
path,
|
|
24779
|
+
offset,
|
|
24780
|
+
lines: window.map((line) => ({
|
|
24781
|
+
number: line.number,
|
|
24782
|
+
text: clipLine(line.text)
|
|
24783
|
+
})),
|
|
24784
|
+
totalLines,
|
|
24785
|
+
truncated
|
|
24786
|
+
};
|
|
24787
|
+
}
|
|
24712
24788
|
}
|
|
24713
24789
|
const sources = record["sources"];
|
|
24714
|
-
if (Array.isArray(sources)
|
|
24715
|
-
const
|
|
24716
|
-
|
|
24717
|
-
|
|
24718
|
-
|
|
24719
|
-
|
|
24720
|
-
|
|
24721
|
-
|
|
24722
|
-
|
|
24723
|
-
|
|
24724
|
-
|
|
24790
|
+
if (Array.isArray(sources)) {
|
|
24791
|
+
const capped = sources.slice(0, MAX_SOURCES);
|
|
24792
|
+
if (capped.every(isWebSource)) {
|
|
24793
|
+
const truncated = sources.length > MAX_SOURCES;
|
|
24794
|
+
return {
|
|
24795
|
+
kind: "web-search",
|
|
24796
|
+
sources: capped.map((source) => ({
|
|
24797
|
+
url: source.url,
|
|
24798
|
+
title: typeof source.title === "string" ? source.title : void 0,
|
|
24799
|
+
snippet: typeof source.snippet === "string" ? clipLine(source.snippet) : ""
|
|
24800
|
+
})),
|
|
24801
|
+
truncated
|
|
24802
|
+
};
|
|
24803
|
+
}
|
|
24725
24804
|
}
|
|
24726
24805
|
const { url, statusCode } = record;
|
|
24727
24806
|
if (typeof url === "string" && typeof statusCode === "number") return {
|
|
@@ -24750,6 +24829,15 @@ function toolResultDetail(meta, rawText) {
|
|
|
24750
24829
|
*/
|
|
24751
24830
|
/** In-flight UI buffers are tails; the assembled assistant message is authoritative. */
|
|
24752
24831
|
const MAX_STREAMING_CHARS = 65536;
|
|
24832
|
+
/**
|
|
24833
|
+
* Upper bound on remembered `compaction/summary` shadow prices waiting for a
|
|
24834
|
+
* matching `compaction/end`. Compactions are sequential and rare, so a few
|
|
24835
|
+
* slots suffice; an aborted compaction (summary without end) otherwise leaves
|
|
24836
|
+
* an unbounded residue in `anchors.compactionTokens`. An evicted price
|
|
24837
|
+
* degrades to the documented `lastPruneTokens` fallback, exactly like a
|
|
24838
|
+
* missing summary.
|
|
24839
|
+
*/
|
|
24840
|
+
const MAX_COMPACTION_SUMMARY_RESIDUE = 16;
|
|
24753
24841
|
/** Append one delta without retaining an unbounded duplicate of the live reply. */
|
|
24754
24842
|
function appendStreamingTail(current, delta) {
|
|
24755
24843
|
const next = current + delta;
|
|
@@ -24828,7 +24916,9 @@ function createTranscriptView() {
|
|
|
24828
24916
|
firstChunkAt: /* @__PURE__ */ new Map(),
|
|
24829
24917
|
compactionTokens: /* @__PURE__ */ new Map(),
|
|
24830
24918
|
lastPruneTokens: 0,
|
|
24831
|
-
turnFiles: /* @__PURE__ */ new Map()
|
|
24919
|
+
turnFiles: /* @__PURE__ */ new Map(),
|
|
24920
|
+
turnSteps: /* @__PURE__ */ new Map(),
|
|
24921
|
+
turnTools: /* @__PURE__ */ new Map()
|
|
24832
24922
|
}
|
|
24833
24923
|
};
|
|
24834
24924
|
}
|
|
@@ -24954,6 +25044,7 @@ function projectEvent(view, event) {
|
|
|
24954
25044
|
view.anchors.stepStart.delete(key);
|
|
24955
25045
|
const firstChunk = view.anchors.firstChunkAt.get(key);
|
|
24956
25046
|
view.anchors.firstChunkAt.delete(key);
|
|
25047
|
+
if (view.anchors.turnSteps.get(event.data.turn) === key) view.anchors.turnSteps.delete(event.data.turn);
|
|
24957
25048
|
const usage = event.data.usage;
|
|
24958
25049
|
const totals = view.stats.usage;
|
|
24959
25050
|
const text = textOf(event.data.message.content);
|
|
@@ -24989,6 +25080,9 @@ function projectEvent(view, event) {
|
|
|
24989
25080
|
case "tool/call": {
|
|
24990
25081
|
const data = event.data;
|
|
24991
25082
|
view.anchors.toolStart.set(data.callId, event.time);
|
|
25083
|
+
const turnTools = view.anchors.turnTools.get(data.turn) ?? /* @__PURE__ */ new Set();
|
|
25084
|
+
turnTools.add(data.callId);
|
|
25085
|
+
view.anchors.turnTools.set(data.turn, turnTools);
|
|
24992
25086
|
return {
|
|
24993
25087
|
...view,
|
|
24994
25088
|
entries: [...view.entries, {
|
|
@@ -25014,6 +25108,11 @@ function projectEvent(view, event) {
|
|
|
25014
25108
|
const block = event.data.message.content[0];
|
|
25015
25109
|
const started = view.anchors.toolStart.get(block.toolCallId);
|
|
25016
25110
|
view.anchors.toolStart.delete(block.toolCallId);
|
|
25111
|
+
const turnTools = view.anchors.turnTools.get(event.data.turn);
|
|
25112
|
+
if (turnTools !== void 0) {
|
|
25113
|
+
turnTools.delete(block.toolCallId);
|
|
25114
|
+
if (turnTools.size === 0) view.anchors.turnTools.delete(event.data.turn);
|
|
25115
|
+
}
|
|
25017
25116
|
const rawText = textOf(block.content);
|
|
25018
25117
|
const summary = boundContextSummary(rawText);
|
|
25019
25118
|
const detail = toolResultDetail(event.data.meta, rawText);
|
|
@@ -25058,8 +25157,15 @@ function projectEvent(view, event) {
|
|
|
25058
25157
|
turns: view.stats.turns + 1
|
|
25059
25158
|
}
|
|
25060
25159
|
};
|
|
25061
|
-
case "step/start":
|
|
25062
|
-
|
|
25160
|
+
case "step/start": {
|
|
25161
|
+
const key = `${event.data.turn}:${event.data.step}`;
|
|
25162
|
+
const previous = view.anchors.turnSteps.get(event.data.turn);
|
|
25163
|
+
if (previous !== void 0 && previous !== key) {
|
|
25164
|
+
view.anchors.stepStart.delete(previous);
|
|
25165
|
+
view.anchors.firstChunkAt.delete(previous);
|
|
25166
|
+
}
|
|
25167
|
+
view.anchors.turnSteps.set(event.data.turn, key);
|
|
25168
|
+
view.anchors.stepStart.set(key, event.time);
|
|
25063
25169
|
return {
|
|
25064
25170
|
...view,
|
|
25065
25171
|
stats: {
|
|
@@ -25067,14 +25173,17 @@ function projectEvent(view, event) {
|
|
|
25067
25173
|
steps: view.stats.steps + 1
|
|
25068
25174
|
}
|
|
25069
25175
|
};
|
|
25176
|
+
}
|
|
25070
25177
|
case "turn/end": {
|
|
25071
25178
|
const reason = event.data.reason;
|
|
25072
25179
|
const appended = [];
|
|
25073
|
-
if (reason.kind === "error")
|
|
25074
|
-
|
|
25075
|
-
|
|
25076
|
-
|
|
25077
|
-
|
|
25180
|
+
if (reason.kind === "error") {
|
|
25181
|
+
const recovery = reason.error.code === "MISSING_CREDENTIAL" ? " · open /model to add an API key" : "";
|
|
25182
|
+
appended.push({
|
|
25183
|
+
kind: "error",
|
|
25184
|
+
text: `${reason.error.code}: ${reason.error.message}${recovery}`
|
|
25185
|
+
});
|
|
25186
|
+
} else {
|
|
25078
25187
|
const marker = reason.kind === "aborted" ? reason.reason.kind === "user" ? "turn cancelled by the user" : `turn cancelled (${reason.reason.kind})` : reason.kind === "max-tokens" ? "turn hit the output-token ceiling (max-tokens)" : reason.kind === "blocked" ? "turn ended blocked" : reason.kind === "interrupted" ? "turn was interrupted by a restart" : void 0;
|
|
25079
25188
|
if (marker !== void 0) appended.push({
|
|
25080
25189
|
kind: "turn-marker",
|
|
@@ -25087,6 +25196,17 @@ function projectEvent(view, event) {
|
|
|
25087
25196
|
kind: "files",
|
|
25088
25197
|
paths: [...files].slice(0, 12)
|
|
25089
25198
|
});
|
|
25199
|
+
const stepKey = view.anchors.turnSteps.get(event.data.turn);
|
|
25200
|
+
if (stepKey !== void 0) {
|
|
25201
|
+
view.anchors.stepStart.delete(stepKey);
|
|
25202
|
+
view.anchors.firstChunkAt.delete(stepKey);
|
|
25203
|
+
view.anchors.turnSteps.delete(event.data.turn);
|
|
25204
|
+
}
|
|
25205
|
+
const turnToolSet = view.anchors.turnTools.get(event.data.turn);
|
|
25206
|
+
if (turnToolSet !== void 0) {
|
|
25207
|
+
for (const callId of turnToolSet) view.anchors.toolStart.delete(callId);
|
|
25208
|
+
view.anchors.turnTools.delete(event.data.turn);
|
|
25209
|
+
}
|
|
25090
25210
|
if (appended.length === 0) return {
|
|
25091
25211
|
...view,
|
|
25092
25212
|
busy: false,
|
|
@@ -25165,6 +25285,10 @@ function projectEvent(view, event) {
|
|
|
25165
25285
|
title: event.data.title
|
|
25166
25286
|
};
|
|
25167
25287
|
case "compaction/summary":
|
|
25288
|
+
if (view.anchors.compactionTokens.size >= MAX_COMPACTION_SUMMARY_RESIDUE) {
|
|
25289
|
+
const oldest = view.anchors.compactionTokens.keys().next().value;
|
|
25290
|
+
if (oldest !== void 0) view.anchors.compactionTokens.delete(oldest);
|
|
25291
|
+
}
|
|
25168
25292
|
view.anchors.compactionTokens.set(event.data.compactionId, event.data.shadowedTokenCount);
|
|
25169
25293
|
return view;
|
|
25170
25294
|
case "compaction/prune": return {
|
|
@@ -25250,13 +25374,561 @@ function projectEvent(view, event) {
|
|
|
25250
25374
|
default: return view;
|
|
25251
25375
|
}
|
|
25252
25376
|
}
|
|
25377
|
+
/** @internal A fresh replay accumulator whose state mirrors `createTranscriptView()`. */
|
|
25378
|
+
function createReplayAccumulator() {
|
|
25379
|
+
return {
|
|
25380
|
+
entries: [],
|
|
25381
|
+
toolIndex: /* @__PURE__ */ new Map(),
|
|
25382
|
+
commandIndex: /* @__PURE__ */ new Map(),
|
|
25383
|
+
retryIndex: /* @__PURE__ */ new Map(),
|
|
25384
|
+
pendingIndex: /* @__PURE__ */ new Map(),
|
|
25385
|
+
removedCount: 0,
|
|
25386
|
+
pendingTurn: [],
|
|
25387
|
+
pendingStep: [],
|
|
25388
|
+
streaming: "",
|
|
25389
|
+
streamingReasoning: "",
|
|
25390
|
+
todos: [],
|
|
25391
|
+
busy: false,
|
|
25392
|
+
busySince: 0,
|
|
25393
|
+
model: "",
|
|
25394
|
+
plan: false,
|
|
25395
|
+
permission: "",
|
|
25396
|
+
title: "",
|
|
25397
|
+
sandbox: "",
|
|
25398
|
+
goal: void 0,
|
|
25399
|
+
stats: {
|
|
25400
|
+
turns: 0,
|
|
25401
|
+
steps: 0,
|
|
25402
|
+
llmMs: 0,
|
|
25403
|
+
toolMs: 0,
|
|
25404
|
+
usage: {
|
|
25405
|
+
inputTokens: 0,
|
|
25406
|
+
outputTokens: 0,
|
|
25407
|
+
cacheReadTokens: 0
|
|
25408
|
+
},
|
|
25409
|
+
lastPromptTokens: 0,
|
|
25410
|
+
contextWindow: 0,
|
|
25411
|
+
contextSegments: {
|
|
25412
|
+
system: 0,
|
|
25413
|
+
prompt: 0,
|
|
25414
|
+
assistant: 0,
|
|
25415
|
+
thinking: 0,
|
|
25416
|
+
tools: 0
|
|
25417
|
+
},
|
|
25418
|
+
ttftMs: 0,
|
|
25419
|
+
ttftSteps: 0,
|
|
25420
|
+
decodeMs: 0,
|
|
25421
|
+
decodeTokens: 0,
|
|
25422
|
+
reasoningEffort: ""
|
|
25423
|
+
},
|
|
25424
|
+
stepStart: /* @__PURE__ */ new Map(),
|
|
25425
|
+
toolStart: /* @__PURE__ */ new Map(),
|
|
25426
|
+
firstChunkAt: /* @__PURE__ */ new Map(),
|
|
25427
|
+
compactionTokens: /* @__PURE__ */ new Map(),
|
|
25428
|
+
lastPruneTokens: 0,
|
|
25429
|
+
turnFiles: /* @__PURE__ */ new Map(),
|
|
25430
|
+
turnSteps: /* @__PURE__ */ new Map(),
|
|
25431
|
+
turnTools: /* @__PURE__ */ new Map(),
|
|
25432
|
+
ops: 0
|
|
25433
|
+
};
|
|
25434
|
+
}
|
|
25435
|
+
/** Append one entry (O(1)) and account the push. */
|
|
25436
|
+
function appendReplayEntry(acc, entry) {
|
|
25437
|
+
acc.entries.push(entry);
|
|
25438
|
+
acc.ops += 1;
|
|
25439
|
+
}
|
|
25440
|
+
/**
|
|
25441
|
+
* Get (or create) the index list an id owns. Lists are never removed: every
|
|
25442
|
+
* appended row registers its index, so a lookup miss later proves no matching
|
|
25443
|
+
* row exists and the caller can no-op in O(1).
|
|
25444
|
+
*/
|
|
25445
|
+
function indexList(map, id) {
|
|
25446
|
+
let list = map.get(id);
|
|
25447
|
+
if (list === void 0) {
|
|
25448
|
+
list = [];
|
|
25449
|
+
map.set(id, list);
|
|
25450
|
+
}
|
|
25451
|
+
return list;
|
|
25452
|
+
}
|
|
25453
|
+
/**
|
|
25454
|
+
* Apply an id-keyed update to every row that registered the id, mirroring the
|
|
25455
|
+
* copy-on-write reducer's full-array map semantics (all matching rows update,
|
|
25456
|
+
* in order). Each registered index is O(1), so a duplicate id costs
|
|
25457
|
+
* O(#duplicates) — never a full-array scan. The kind+id re-check is defensive:
|
|
25458
|
+
* registered indices are valid by construction, because tool/command/retry
|
|
25459
|
+
* rows are never removed and tombstones never shift indices.
|
|
25460
|
+
*/
|
|
25461
|
+
function updateReplayById(acc, map, id, isMatch, update) {
|
|
25462
|
+
const list = map.get(id);
|
|
25463
|
+
if (list === void 0) return;
|
|
25464
|
+
for (const index of list) {
|
|
25465
|
+
const entry = acc.entries[index];
|
|
25466
|
+
if (entry === void 0 || !isMatch(entry)) continue;
|
|
25467
|
+
acc.entries[index] = update(entry);
|
|
25468
|
+
acc.ops += 1;
|
|
25469
|
+
}
|
|
25470
|
+
}
|
|
25471
|
+
/** Tombstone a retired pending row, keeping every other index stable. */
|
|
25472
|
+
function retireReplayEntry(acc, index) {
|
|
25473
|
+
if (acc.entries[index] !== void 0) {
|
|
25474
|
+
acc.entries[index] = void 0;
|
|
25475
|
+
acc.removedCount += 1;
|
|
25476
|
+
acc.ops += 1;
|
|
25477
|
+
}
|
|
25478
|
+
}
|
|
25479
|
+
/**
|
|
25480
|
+
* Fold one session event into a replay accumulator. This mirrors
|
|
25481
|
+
* {@link projectEvent} case for case — same stats arithmetic, same anchor
|
|
25482
|
+
* set/delete behavior, same entry shapes — so the finished view is identical
|
|
25483
|
+
* to a sequential fold; only the `entries` container operations are mutable.
|
|
25484
|
+
*
|
|
25485
|
+
* @internal Test-instrumentation path; `projectEvents` is the public entry.
|
|
25486
|
+
*/
|
|
25487
|
+
function replayProjectEvent(acc, event) {
|
|
25488
|
+
switch (event.type) {
|
|
25489
|
+
case "user/message": {
|
|
25490
|
+
const message = event.data;
|
|
25491
|
+
for (const target of ["next-turn", "next-step"]) {
|
|
25492
|
+
const ids = target === "next-turn" ? acc.pendingTurn : acc.pendingStep;
|
|
25493
|
+
const index = ids.indexOf(message.id);
|
|
25494
|
+
acc.ops += index < 0 ? ids.length : index + 1;
|
|
25495
|
+
if (index < 0) continue;
|
|
25496
|
+
ids.splice(index, 1);
|
|
25497
|
+
acc.ops += 1;
|
|
25498
|
+
const list = acc.pendingIndex.get(message.id);
|
|
25499
|
+
if (list !== void 0) {
|
|
25500
|
+
for (const entryIndex of list) retireReplayEntry(acc, entryIndex);
|
|
25501
|
+
acc.ops += 1;
|
|
25502
|
+
}
|
|
25503
|
+
}
|
|
25504
|
+
const text = textOf(message.content);
|
|
25505
|
+
if (message.source.kind === "user") {
|
|
25506
|
+
appendReplayEntry(acc, {
|
|
25507
|
+
kind: "user",
|
|
25508
|
+
text,
|
|
25509
|
+
notice: false
|
|
25510
|
+
});
|
|
25511
|
+
acc.stats = {
|
|
25512
|
+
...acc.stats,
|
|
25513
|
+
contextSegments: {
|
|
25514
|
+
...acc.stats.contextSegments,
|
|
25515
|
+
prompt: acc.stats.contextSegments.prompt + estimateTokens(text)
|
|
25516
|
+
}
|
|
25517
|
+
};
|
|
25518
|
+
return;
|
|
25519
|
+
}
|
|
25520
|
+
const notice = message.source.kind === "plugin" && message.source.form === "notice" ? message.source.summary : message.source.kind;
|
|
25521
|
+
const summary = boundContextSummary(notice);
|
|
25522
|
+
appendReplayEntry(acc, {
|
|
25523
|
+
kind: "user",
|
|
25524
|
+
text: summary,
|
|
25525
|
+
notice: true
|
|
25526
|
+
});
|
|
25527
|
+
acc.stats = {
|
|
25528
|
+
...acc.stats,
|
|
25529
|
+
contextSegments: {
|
|
25530
|
+
...acc.stats.contextSegments,
|
|
25531
|
+
system: acc.stats.contextSegments.system + estimateTokens(summary)
|
|
25532
|
+
}
|
|
25533
|
+
};
|
|
25534
|
+
return;
|
|
25535
|
+
}
|
|
25536
|
+
case "agent/inbox/spliced": {
|
|
25537
|
+
const { target, start, removedCount = 0, inserted } = event.data;
|
|
25538
|
+
const ids = target === "next-turn" ? acc.pendingTurn : acc.pendingStep;
|
|
25539
|
+
const removed = ids.slice(start, start + removedCount);
|
|
25540
|
+
acc.ops += removed.length;
|
|
25541
|
+
ids.splice(start, removedCount);
|
|
25542
|
+
acc.ops += removed.length;
|
|
25543
|
+
for (const id of removed) {
|
|
25544
|
+
const list = acc.pendingIndex.get(id);
|
|
25545
|
+
if (list === void 0) continue;
|
|
25546
|
+
for (const entryIndex of list) {
|
|
25547
|
+
const entry = acc.entries[entryIndex];
|
|
25548
|
+
if (entry !== void 0 && entry.kind === "pending" && entry.target === target) retireReplayEntry(acc, entryIndex);
|
|
25549
|
+
}
|
|
25550
|
+
}
|
|
25551
|
+
for (const message of inserted) {
|
|
25552
|
+
appendReplayEntry(acc, {
|
|
25553
|
+
kind: "pending",
|
|
25554
|
+
messageId: message.id,
|
|
25555
|
+
target,
|
|
25556
|
+
text: pendingText(message.content)
|
|
25557
|
+
});
|
|
25558
|
+
indexList(acc.pendingIndex, message.id).push(acc.entries.length - 1);
|
|
25559
|
+
ids.push(message.id);
|
|
25560
|
+
acc.ops += 1;
|
|
25561
|
+
}
|
|
25562
|
+
return;
|
|
25563
|
+
}
|
|
25564
|
+
case "assistant/chunk": {
|
|
25565
|
+
const chunk = event.data.chunk;
|
|
25566
|
+
const key = `${event.data.turn}:${event.data.step}`;
|
|
25567
|
+
if ((chunk.type === "text-delta" || chunk.type === "reasoning-delta" ? chunk.text : "") !== "" && !acc.firstChunkAt.has(key)) {
|
|
25568
|
+
acc.firstChunkAt.set(key, event.time);
|
|
25569
|
+
const started = acc.stepStart.get(key);
|
|
25570
|
+
if (started !== void 0) acc.stats = {
|
|
25571
|
+
...acc.stats,
|
|
25572
|
+
ttftMs: acc.stats.ttftMs + Math.max(0, event.time - started),
|
|
25573
|
+
ttftSteps: acc.stats.ttftSteps + 1
|
|
25574
|
+
};
|
|
25575
|
+
}
|
|
25576
|
+
if (chunk.type === "text-delta") {
|
|
25577
|
+
acc.streaming = appendStreamingTail(acc.streaming, chunk.text);
|
|
25578
|
+
return;
|
|
25579
|
+
}
|
|
25580
|
+
if (chunk.type === "reasoning-delta") {
|
|
25581
|
+
acc.streamingReasoning = appendStreamingTail(acc.streamingReasoning, chunk.text);
|
|
25582
|
+
return;
|
|
25583
|
+
}
|
|
25584
|
+
return;
|
|
25585
|
+
}
|
|
25586
|
+
case "assistant/message": {
|
|
25587
|
+
const key = `${event.data.turn}:${event.data.step}`;
|
|
25588
|
+
const started = acc.stepStart.get(key);
|
|
25589
|
+
acc.stepStart.delete(key);
|
|
25590
|
+
const firstChunk = acc.firstChunkAt.get(key);
|
|
25591
|
+
acc.firstChunkAt.delete(key);
|
|
25592
|
+
if (acc.turnSteps.get(event.data.turn) === key) acc.turnSteps.delete(event.data.turn);
|
|
25593
|
+
const usage = event.data.usage;
|
|
25594
|
+
const totals = acc.stats.usage;
|
|
25595
|
+
const text = textOf(event.data.message.content);
|
|
25596
|
+
const reasoning = reasoningOf(event.data.message.content);
|
|
25597
|
+
acc.streaming = "";
|
|
25598
|
+
acc.streamingReasoning = "";
|
|
25599
|
+
appendReplayEntry(acc, {
|
|
25600
|
+
kind: "assistant",
|
|
25601
|
+
text,
|
|
25602
|
+
reasoning
|
|
25603
|
+
});
|
|
25604
|
+
acc.stats = {
|
|
25605
|
+
...acc.stats,
|
|
25606
|
+
llmMs: acc.stats.llmMs + (started === void 0 ? 0 : Math.max(0, event.time - started)),
|
|
25607
|
+
usage: usage === void 0 ? totals : {
|
|
25608
|
+
inputTokens: totals.inputTokens + usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0),
|
|
25609
|
+
outputTokens: totals.outputTokens + usage.outputTokens,
|
|
25610
|
+
cacheReadTokens: totals.cacheReadTokens + (usage.cacheReadTokens ?? 0)
|
|
25611
|
+
},
|
|
25612
|
+
lastPromptTokens: usage === void 0 ? acc.stats.lastPromptTokens : usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0),
|
|
25613
|
+
decodeMs: acc.stats.decodeMs + (firstChunk === void 0 ? 0 : Math.max(0, event.time - firstChunk)),
|
|
25614
|
+
decodeTokens: acc.stats.decodeTokens + (firstChunk === void 0 || usage === void 0 ? 0 : usage.outputTokens),
|
|
25615
|
+
contextSegments: {
|
|
25616
|
+
...acc.stats.contextSegments,
|
|
25617
|
+
thinking: acc.stats.contextSegments.thinking + estimateTokens(reasoning),
|
|
25618
|
+
assistant: acc.stats.contextSegments.assistant + estimateTokens(text)
|
|
25619
|
+
}
|
|
25620
|
+
};
|
|
25621
|
+
return;
|
|
25622
|
+
}
|
|
25623
|
+
case "tool/call": {
|
|
25624
|
+
const data = event.data;
|
|
25625
|
+
acc.toolStart.set(data.callId, event.time);
|
|
25626
|
+
const turnTools = acc.turnTools.get(data.turn) ?? /* @__PURE__ */ new Set();
|
|
25627
|
+
turnTools.add(data.callId);
|
|
25628
|
+
acc.turnTools.set(data.turn, turnTools);
|
|
25629
|
+
appendReplayEntry(acc, {
|
|
25630
|
+
kind: "tool",
|
|
25631
|
+
callId: data.callId,
|
|
25632
|
+
name: data.name,
|
|
25633
|
+
arguments: data.arguments,
|
|
25634
|
+
preview: toolArgumentsPreview(data.arguments, data.name),
|
|
25635
|
+
state: "running",
|
|
25636
|
+
summary: "",
|
|
25637
|
+
detail: void 0
|
|
25638
|
+
});
|
|
25639
|
+
indexList(acc.toolIndex, data.callId).push(acc.entries.length - 1);
|
|
25640
|
+
acc.stats = {
|
|
25641
|
+
...acc.stats,
|
|
25642
|
+
contextSegments: {
|
|
25643
|
+
...acc.stats.contextSegments,
|
|
25644
|
+
tools: acc.stats.contextSegments.tools + (typeof data.arguments === "string" ? estimateTokens(data.arguments) : 0)
|
|
25645
|
+
}
|
|
25646
|
+
};
|
|
25647
|
+
return;
|
|
25648
|
+
}
|
|
25649
|
+
case "tool/result": {
|
|
25650
|
+
const block = event.data.message.content[0];
|
|
25651
|
+
const started = acc.toolStart.get(block.toolCallId);
|
|
25652
|
+
acc.toolStart.delete(block.toolCallId);
|
|
25653
|
+
const turnTools = acc.turnTools.get(event.data.turn);
|
|
25654
|
+
if (turnTools !== void 0) {
|
|
25655
|
+
turnTools.delete(block.toolCallId);
|
|
25656
|
+
if (turnTools.size === 0) acc.turnTools.delete(event.data.turn);
|
|
25657
|
+
}
|
|
25658
|
+
const rawText = textOf(block.content);
|
|
25659
|
+
const summary = boundContextSummary(rawText);
|
|
25660
|
+
const detail = toolResultDetail(event.data.meta, rawText);
|
|
25661
|
+
if (detail?.kind === "diff") {
|
|
25662
|
+
const set = acc.turnFiles.get(event.data.turn) ?? /* @__PURE__ */ new Set();
|
|
25663
|
+
for (const diff of detail.diffs) set.add(diff.path);
|
|
25664
|
+
acc.turnFiles.set(event.data.turn, set);
|
|
25665
|
+
}
|
|
25666
|
+
const update = (entry) => ({
|
|
25667
|
+
...entry,
|
|
25668
|
+
state: block.isError === true ? "error" : "done",
|
|
25669
|
+
summary,
|
|
25670
|
+
detail
|
|
25671
|
+
});
|
|
25672
|
+
updateReplayById(acc, acc.toolIndex, block.toolCallId, (entry) => entry.callId === block.toolCallId, update);
|
|
25673
|
+
acc.stats = {
|
|
25674
|
+
...acc.stats,
|
|
25675
|
+
toolMs: acc.stats.toolMs + (started === void 0 ? 0 : Math.max(0, event.time - started)),
|
|
25676
|
+
contextSegments: {
|
|
25677
|
+
...acc.stats.contextSegments,
|
|
25678
|
+
tools: acc.stats.contextSegments.tools + estimateTokens(rawText)
|
|
25679
|
+
}
|
|
25680
|
+
};
|
|
25681
|
+
return;
|
|
25682
|
+
}
|
|
25683
|
+
case "todo/write":
|
|
25684
|
+
acc.todos = event.data.todos;
|
|
25685
|
+
return;
|
|
25686
|
+
case "turn/start": {
|
|
25687
|
+
const wasBusy = acc.busy;
|
|
25688
|
+
acc.busy = true;
|
|
25689
|
+
acc.busySince = wasBusy ? acc.busySince : event.time;
|
|
25690
|
+
acc.todos = [];
|
|
25691
|
+
acc.stats = {
|
|
25692
|
+
...acc.stats,
|
|
25693
|
+
turns: acc.stats.turns + 1
|
|
25694
|
+
};
|
|
25695
|
+
return;
|
|
25696
|
+
}
|
|
25697
|
+
case "step/start": {
|
|
25698
|
+
const key = `${event.data.turn}:${event.data.step}`;
|
|
25699
|
+
const previous = acc.turnSteps.get(event.data.turn);
|
|
25700
|
+
if (previous !== void 0 && previous !== key) {
|
|
25701
|
+
acc.stepStart.delete(previous);
|
|
25702
|
+
acc.firstChunkAt.delete(previous);
|
|
25703
|
+
}
|
|
25704
|
+
acc.turnSteps.set(event.data.turn, key);
|
|
25705
|
+
acc.stepStart.set(key, event.time);
|
|
25706
|
+
acc.stats = {
|
|
25707
|
+
...acc.stats,
|
|
25708
|
+
steps: acc.stats.steps + 1
|
|
25709
|
+
};
|
|
25710
|
+
return;
|
|
25711
|
+
}
|
|
25712
|
+
case "turn/end": {
|
|
25713
|
+
const reason = event.data.reason;
|
|
25714
|
+
const appended = [];
|
|
25715
|
+
if (reason.kind === "error") {
|
|
25716
|
+
const recovery = reason.error.code === "MISSING_CREDENTIAL" ? " · open /model to add an API key" : "";
|
|
25717
|
+
appended.push({
|
|
25718
|
+
kind: "error",
|
|
25719
|
+
text: `${reason.error.code}: ${reason.error.message}${recovery}`
|
|
25720
|
+
});
|
|
25721
|
+
} else {
|
|
25722
|
+
const marker = reason.kind === "aborted" ? reason.reason.kind === "user" ? "turn cancelled by the user" : `turn cancelled (${reason.reason.kind})` : reason.kind === "max-tokens" ? "turn hit the output-token ceiling (max-tokens)" : reason.kind === "blocked" ? "turn ended blocked" : reason.kind === "interrupted" ? "turn was interrupted by a restart" : void 0;
|
|
25723
|
+
if (marker !== void 0) appended.push({
|
|
25724
|
+
kind: "turn-marker",
|
|
25725
|
+
text: marker
|
|
25726
|
+
});
|
|
25727
|
+
}
|
|
25728
|
+
const files = acc.turnFiles.get(event.data.turn);
|
|
25729
|
+
acc.turnFiles.delete(event.data.turn);
|
|
25730
|
+
if (files !== void 0 && files.size > 0) appended.push({
|
|
25731
|
+
kind: "files",
|
|
25732
|
+
paths: [...files].slice(0, 12)
|
|
25733
|
+
});
|
|
25734
|
+
const stepKey = acc.turnSteps.get(event.data.turn);
|
|
25735
|
+
if (stepKey !== void 0) {
|
|
25736
|
+
acc.stepStart.delete(stepKey);
|
|
25737
|
+
acc.firstChunkAt.delete(stepKey);
|
|
25738
|
+
acc.turnSteps.delete(event.data.turn);
|
|
25739
|
+
}
|
|
25740
|
+
const turnToolSet = acc.turnTools.get(event.data.turn);
|
|
25741
|
+
if (turnToolSet !== void 0) {
|
|
25742
|
+
for (const callId of turnToolSet) acc.toolStart.delete(callId);
|
|
25743
|
+
acc.turnTools.delete(event.data.turn);
|
|
25744
|
+
}
|
|
25745
|
+
acc.busy = false;
|
|
25746
|
+
acc.busySince = 0;
|
|
25747
|
+
for (const entry of appended) appendReplayEntry(acc, entry);
|
|
25748
|
+
return;
|
|
25749
|
+
}
|
|
25750
|
+
case "llm/retry": {
|
|
25751
|
+
const data = event.data;
|
|
25752
|
+
appendReplayEntry(acc, {
|
|
25753
|
+
kind: "retry",
|
|
25754
|
+
retryId: data.retryId,
|
|
25755
|
+
attempt: data.retry,
|
|
25756
|
+
max: "maxRetries" in data ? data.maxRetries : data.retry,
|
|
25757
|
+
code: data.failure.code,
|
|
25758
|
+
delayMs: data.delayMs,
|
|
25759
|
+
state: "running"
|
|
25760
|
+
});
|
|
25761
|
+
indexList(acc.retryIndex, data.retryId).push(acc.entries.length - 1);
|
|
25762
|
+
return;
|
|
25763
|
+
}
|
|
25764
|
+
case "llm/retry-started": {
|
|
25765
|
+
const data = event.data;
|
|
25766
|
+
updateReplayById(acc, acc.retryIndex, data.retryId, (entry) => entry.retryId === data.retryId, (entry) => ({
|
|
25767
|
+
...entry,
|
|
25768
|
+
state: "done"
|
|
25769
|
+
}));
|
|
25770
|
+
return;
|
|
25771
|
+
}
|
|
25772
|
+
case "sandbox/mode":
|
|
25773
|
+
acc.sandbox = event.data.mode;
|
|
25774
|
+
return;
|
|
25775
|
+
case "goal/change": {
|
|
25776
|
+
const data = event.data;
|
|
25777
|
+
const clip = (text) => text.length > 60 ? `${text.slice(0, 59)}…` : text;
|
|
25778
|
+
if (data.operation === "clear") {
|
|
25779
|
+
acc.goal = void 0;
|
|
25780
|
+
appendReplayEntry(acc, {
|
|
25781
|
+
kind: "turn-marker",
|
|
25782
|
+
text: "◎ goal cleared"
|
|
25783
|
+
});
|
|
25784
|
+
return;
|
|
25785
|
+
}
|
|
25786
|
+
const goal = {
|
|
25787
|
+
objective: data.goal.objective,
|
|
25788
|
+
phase: data.goal.phase,
|
|
25789
|
+
rounds: data.roundsStarted,
|
|
25790
|
+
max: data.goal.maxGoalRounds,
|
|
25791
|
+
blocked: data.goal.blockedReason?.message ?? ""
|
|
25792
|
+
};
|
|
25793
|
+
const line = data.operation === "create" ? `◎ goal: ${clip(data.goal.objective)}` : data.operation === "complete" ? "◎ goal complete" : data.operation === "pause" ? "◎ goal paused" : data.operation === "resume" ? "◎ goal resumed" : data.operation === "block" ? `◎ goal blocked: ${clip(goal.blocked)}` : void 0;
|
|
25794
|
+
acc.goal = goal;
|
|
25795
|
+
if (line !== void 0) appendReplayEntry(acc, {
|
|
25796
|
+
kind: "turn-marker",
|
|
25797
|
+
text: line
|
|
25798
|
+
});
|
|
25799
|
+
return;
|
|
25800
|
+
}
|
|
25801
|
+
case "session/title":
|
|
25802
|
+
acc.title = event.data.title;
|
|
25803
|
+
return;
|
|
25804
|
+
case "compaction/summary":
|
|
25805
|
+
if (acc.compactionTokens.size >= MAX_COMPACTION_SUMMARY_RESIDUE) {
|
|
25806
|
+
const oldest = acc.compactionTokens.keys().next().value;
|
|
25807
|
+
if (oldest !== void 0) acc.compactionTokens.delete(oldest);
|
|
25808
|
+
}
|
|
25809
|
+
acc.compactionTokens.set(event.data.compactionId, event.data.shadowedTokenCount);
|
|
25810
|
+
return;
|
|
25811
|
+
case "compaction/prune":
|
|
25812
|
+
acc.lastPruneTokens = event.data.shadowedTokenCount;
|
|
25813
|
+
return;
|
|
25814
|
+
case "compaction/end": {
|
|
25815
|
+
const ok = event.data.error === void 0;
|
|
25816
|
+
const tokens = acc.compactionTokens.get(event.data.compactionId) ?? acc.lastPruneTokens;
|
|
25817
|
+
acc.compactionTokens.delete(event.data.compactionId);
|
|
25818
|
+
appendReplayEntry(acc, {
|
|
25819
|
+
kind: "compaction",
|
|
25820
|
+
ok,
|
|
25821
|
+
tokens,
|
|
25822
|
+
error: event.data.error ?? ""
|
|
25823
|
+
});
|
|
25824
|
+
return;
|
|
25825
|
+
}
|
|
25826
|
+
case "request/context":
|
|
25827
|
+
acc.stats = {
|
|
25828
|
+
...acc.stats,
|
|
25829
|
+
contextWindow: event.data.contextWindow ?? acc.stats.contextWindow
|
|
25830
|
+
};
|
|
25831
|
+
return;
|
|
25832
|
+
case "request/header": {
|
|
25833
|
+
const config = event.data.header.config;
|
|
25834
|
+
acc.model = `${config.provider}/${config.model}`;
|
|
25835
|
+
acc.stats = {
|
|
25836
|
+
...acc.stats,
|
|
25837
|
+
reasoningEffort: config.reasoningEffort === void 0 ? "" : String(config.reasoningEffort),
|
|
25838
|
+
contextSegments: {
|
|
25839
|
+
...acc.stats.contextSegments,
|
|
25840
|
+
system: estimateTokens(event.data.header.system ?? "")
|
|
25841
|
+
}
|
|
25842
|
+
};
|
|
25843
|
+
return;
|
|
25844
|
+
}
|
|
25845
|
+
case "plan/mode":
|
|
25846
|
+
acc.plan = event.data.active;
|
|
25847
|
+
return;
|
|
25848
|
+
case "permission/preset":
|
|
25849
|
+
acc.permission = event.data.preset;
|
|
25850
|
+
return;
|
|
25851
|
+
case "command/run": {
|
|
25852
|
+
const data = event.data;
|
|
25853
|
+
appendReplayEntry(acc, {
|
|
25854
|
+
kind: "command",
|
|
25855
|
+
commandId: data.commandId,
|
|
25856
|
+
name: data.name,
|
|
25857
|
+
args: data.args ?? "",
|
|
25858
|
+
state: "running",
|
|
25859
|
+
summary: ""
|
|
25860
|
+
});
|
|
25861
|
+
indexList(acc.commandIndex, data.commandId).push(acc.entries.length - 1);
|
|
25862
|
+
return;
|
|
25863
|
+
}
|
|
25864
|
+
case "command/done": {
|
|
25865
|
+
const data = event.data;
|
|
25866
|
+
const update = (candidate) => ({
|
|
25867
|
+
...candidate,
|
|
25868
|
+
state: data.kind === "success" ? "done" : "error",
|
|
25869
|
+
summary: boundContextSummary(data.text ?? "")
|
|
25870
|
+
});
|
|
25871
|
+
updateReplayById(acc, acc.commandIndex, data.commandId, (entry) => entry.commandId === data.commandId, update);
|
|
25872
|
+
return;
|
|
25873
|
+
}
|
|
25874
|
+
default: return;
|
|
25875
|
+
}
|
|
25876
|
+
}
|
|
25877
|
+
/**
|
|
25878
|
+
* Materialize the accumulated fold as a `TranscriptView`, compacting any
|
|
25879
|
+
* retired tombstones. The anchors maps are handed through as-is (their
|
|
25880
|
+
* content is identical to a sequential fold's).
|
|
25881
|
+
*
|
|
25882
|
+
* @internal Test-instrumentation path; `projectEvents` is the public entry.
|
|
25883
|
+
*/
|
|
25884
|
+
function finishReplay(acc) {
|
|
25885
|
+
const entries = acc.removedCount === 0 ? acc.entries : acc.entries.filter((entry) => entry !== void 0);
|
|
25886
|
+
if (acc.removedCount > 0) acc.ops += acc.entries.length;
|
|
25887
|
+
return {
|
|
25888
|
+
entries,
|
|
25889
|
+
streaming: acc.streaming,
|
|
25890
|
+
streamingReasoning: acc.streamingReasoning,
|
|
25891
|
+
todos: acc.todos,
|
|
25892
|
+
busy: acc.busy,
|
|
25893
|
+
busySince: acc.busySince,
|
|
25894
|
+
model: acc.model,
|
|
25895
|
+
plan: acc.plan,
|
|
25896
|
+
permission: acc.permission,
|
|
25897
|
+
title: acc.title,
|
|
25898
|
+
sandbox: acc.sandbox,
|
|
25899
|
+
goal: acc.goal,
|
|
25900
|
+
pending: {
|
|
25901
|
+
"next-turn": [...acc.pendingTurn],
|
|
25902
|
+
"next-step": [...acc.pendingStep]
|
|
25903
|
+
},
|
|
25904
|
+
stats: acc.stats,
|
|
25905
|
+
anchors: {
|
|
25906
|
+
stepStart: acc.stepStart,
|
|
25907
|
+
toolStart: acc.toolStart,
|
|
25908
|
+
firstChunkAt: acc.firstChunkAt,
|
|
25909
|
+
compactionTokens: acc.compactionTokens,
|
|
25910
|
+
lastPruneTokens: acc.lastPruneTokens,
|
|
25911
|
+
turnFiles: acc.turnFiles,
|
|
25912
|
+
turnSteps: acc.turnSteps,
|
|
25913
|
+
turnTools: acc.turnTools
|
|
25914
|
+
}
|
|
25915
|
+
};
|
|
25916
|
+
}
|
|
25253
25917
|
/**
|
|
25254
25918
|
* Fold a replayed event history into one view.
|
|
25919
|
+
*
|
|
25920
|
+
* Folding is near-linear in the log size: the mutable replay accumulator
|
|
25921
|
+
* appends in place and resolves id-keyed updates through index maps, so a
|
|
25922
|
+
* long persisted session replays without the O(N²) copy-on-write rebuilds a
|
|
25923
|
+
* naive sequential fold would incur. The result is identical to folding
|
|
25924
|
+
* {@link projectEvent} per event in order.
|
|
25255
25925
|
* @param events - events in `seq` order.
|
|
25256
25926
|
* @returns the folded view.
|
|
25257
25927
|
*/
|
|
25258
25928
|
function projectEvents(events) {
|
|
25259
|
-
|
|
25929
|
+
const acc = createReplayAccumulator();
|
|
25930
|
+
for (const event of events) replayProjectEvent(acc, event);
|
|
25931
|
+
return finishReplay(acc);
|
|
25260
25932
|
}
|
|
25261
25933
|
/**
|
|
25262
25934
|
* The append-only flush boundary for a transcript view: the count of entries
|
|
@@ -25267,8 +25939,11 @@ function projectEvents(events) {
|
|
|
25267
25939
|
* the inbox claims or cancels them durably (`agent/inbox/spliced` removals,
|
|
25268
25940
|
* `user/message` retirement), and an append-only `<Static>` flush cannot
|
|
25269
25941
|
* erase a row that vanishes from the view — the retired row would ghost on
|
|
25270
|
-
* screen until the next source-backed replay.
|
|
25271
|
-
*
|
|
25942
|
+
* screen until the next source-backed replay. Running commands join the
|
|
25943
|
+
* mutable boundary for the same reason in reverse: `command/done` mutates the
|
|
25944
|
+
* row's state/summary, so a flushed row would keep its stale running mark
|
|
25945
|
+
* until a resize-triggered replay. Everything else (including a completed
|
|
25946
|
+
* tail) is final: later events only APPEND new rows.
|
|
25272
25947
|
* @param entries - the view's transcript entries in order.
|
|
25273
25948
|
* @returns the count of entries safe to flush (0 for an empty transcript).
|
|
25274
25949
|
*/
|
|
@@ -25278,6 +25953,7 @@ function settledEntryCount(entries) {
|
|
|
25278
25953
|
if (entry.kind === "pending") return index;
|
|
25279
25954
|
if (entry.kind === "tool" && entry.state === "running") return index;
|
|
25280
25955
|
if (entry.kind === "retry" && entry.state === "running") return index;
|
|
25956
|
+
if (entry.kind === "command" && entry.state === "running") return index;
|
|
25281
25957
|
}
|
|
25282
25958
|
return entries.length;
|
|
25283
25959
|
}
|
|
@@ -25439,6 +26115,259 @@ const RULE = /^(?:---|\*\*\*|___)\s*$/u;
|
|
|
25439
26115
|
const QUOTE = /^>\s?(.*)$/u;
|
|
25440
26116
|
const UNORDERED = /^\s*[-*+]\s+(.*)$/u;
|
|
25441
26117
|
const ORDERED = /^\s*(\d+)[.)]\s+(.*)$/u;
|
|
26118
|
+
const TABLE_DELIMITER = /^(:)?-+(:)?$/u;
|
|
26119
|
+
const TABLE_COLUMN_GAP = 2;
|
|
26120
|
+
const TABLE_CELL_PADDING = 1;
|
|
26121
|
+
const TABLE_MIN_COLUMN_WIDTH = 3;
|
|
26122
|
+
const TABLE_MIN_ALIGNED_VALUE_WIDTH = 16;
|
|
26123
|
+
const MAX_TABLE_COLUMNS = 12;
|
|
26124
|
+
/** Split one pipe row without treating escaped or inline-code pipes as cells. */
|
|
26125
|
+
function splitTableRow(line) {
|
|
26126
|
+
const source = line.trim();
|
|
26127
|
+
if (!source.includes("|")) return void 0;
|
|
26128
|
+
const cells = [];
|
|
26129
|
+
let current = "";
|
|
26130
|
+
let inCode = false;
|
|
26131
|
+
let sawSeparator = false;
|
|
26132
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
26133
|
+
const char = source[index] ?? "";
|
|
26134
|
+
if (char === "\\" && source[index + 1] === "|") {
|
|
26135
|
+
current += "|";
|
|
26136
|
+
index += 1;
|
|
26137
|
+
continue;
|
|
26138
|
+
}
|
|
26139
|
+
if (char === "`") {
|
|
26140
|
+
inCode = !inCode;
|
|
26141
|
+
current += char;
|
|
26142
|
+
continue;
|
|
26143
|
+
}
|
|
26144
|
+
if (char === "|" && !inCode) {
|
|
26145
|
+
cells.push(current.trim());
|
|
26146
|
+
current = "";
|
|
26147
|
+
sawSeparator = true;
|
|
26148
|
+
continue;
|
|
26149
|
+
}
|
|
26150
|
+
current += char;
|
|
26151
|
+
}
|
|
26152
|
+
if (!sawSeparator) return void 0;
|
|
26153
|
+
cells.push(current.trim());
|
|
26154
|
+
if (source.startsWith("|") && cells[0] === "") cells.shift();
|
|
26155
|
+
if (source.endsWith("|") && cells.at(-1) === "") cells.pop();
|
|
26156
|
+
return cells.length === 0 ? void 0 : cells;
|
|
26157
|
+
}
|
|
26158
|
+
/** Parse the alignment marker carried by one GFM delimiter cell. */
|
|
26159
|
+
function tableAlignment(cell) {
|
|
26160
|
+
const match = TABLE_DELIMITER.exec(cell);
|
|
26161
|
+
if (match === null) return void 0;
|
|
26162
|
+
if (match[1] !== void 0 && match[2] !== void 0) return "center";
|
|
26163
|
+
if (match[2] !== void 0) return "right";
|
|
26164
|
+
return "left";
|
|
26165
|
+
}
|
|
26166
|
+
/** Recognize one complete GFM pipe table at `startIndex`. */
|
|
26167
|
+
function parseTable(source, startIndex) {
|
|
26168
|
+
const headers = splitTableRow(source[startIndex] ?? "");
|
|
26169
|
+
const delimiters = splitTableRow(source[startIndex + 1] ?? "");
|
|
26170
|
+
if (headers === void 0 || delimiters === void 0 || headers.length !== delimiters.length) return void 0;
|
|
26171
|
+
if (headers.length === 0 || headers.length > MAX_TABLE_COLUMNS) return void 0;
|
|
26172
|
+
const alignments = delimiters.map(tableAlignment);
|
|
26173
|
+
if (alignments.some((alignment) => alignment === void 0)) return void 0;
|
|
26174
|
+
const rows = [];
|
|
26175
|
+
let nextIndex = startIndex + 2;
|
|
26176
|
+
while (nextIndex < source.length) {
|
|
26177
|
+
const cells = splitTableRow(source[nextIndex] ?? "");
|
|
26178
|
+
if (cells === void 0) break;
|
|
26179
|
+
rows.push(Array.from({ length: headers.length }, (_, column) => cells[column] ?? ""));
|
|
26180
|
+
nextIndex += 1;
|
|
26181
|
+
}
|
|
26182
|
+
return {
|
|
26183
|
+
headers,
|
|
26184
|
+
alignments,
|
|
26185
|
+
rows,
|
|
26186
|
+
nextIndex
|
|
26187
|
+
};
|
|
26188
|
+
}
|
|
26189
|
+
/** Visible width of one styled cell line. */
|
|
26190
|
+
function segmentsWidth(segments) {
|
|
26191
|
+
return segments.reduce((total, segment) => total + visibleColumns(segment.text), 0);
|
|
26192
|
+
}
|
|
26193
|
+
/** Drop wrapping-only leading spaces while preserving styles. */
|
|
26194
|
+
function trimLeadingSpaces(segments) {
|
|
26195
|
+
const trimmed = segments.map((segment) => ({ ...segment }));
|
|
26196
|
+
while (trimmed[0]?.text.startsWith(" ") === true) {
|
|
26197
|
+
const first = trimmed[0];
|
|
26198
|
+
const text = first.text.replace(/^ +/u, "");
|
|
26199
|
+
if (text === "") trimmed.shift();
|
|
26200
|
+
else trimmed[0] = {
|
|
26201
|
+
...first,
|
|
26202
|
+
text
|
|
26203
|
+
};
|
|
26204
|
+
}
|
|
26205
|
+
return trimmed;
|
|
26206
|
+
}
|
|
26207
|
+
/** Hard-split an oversized soft-wrapped row while retaining inline styles. */
|
|
26208
|
+
function hardWrapSegments(segments, width) {
|
|
26209
|
+
const out = [];
|
|
26210
|
+
let current = [];
|
|
26211
|
+
let used = 0;
|
|
26212
|
+
const flush = () => {
|
|
26213
|
+
out.push(current);
|
|
26214
|
+
current = [];
|
|
26215
|
+
used = 0;
|
|
26216
|
+
};
|
|
26217
|
+
for (const segment of segments) for (const char of segment.text) {
|
|
26218
|
+
const cells = visibleColumns(char);
|
|
26219
|
+
if (used > 0 && used + cells > width) flush();
|
|
26220
|
+
const previous = current.at(-1);
|
|
26221
|
+
if (previous?.style === segment.style) previous.text += char;
|
|
26222
|
+
else current.push({
|
|
26223
|
+
text: char,
|
|
26224
|
+
style: segment.style
|
|
26225
|
+
});
|
|
26226
|
+
used += cells;
|
|
26227
|
+
}
|
|
26228
|
+
if (current.length > 0 || out.length === 0) flush();
|
|
26229
|
+
return out;
|
|
26230
|
+
}
|
|
26231
|
+
/** Wrap one table cell to its allocated content width. */
|
|
26232
|
+
function wrapTableCell(runs, width) {
|
|
26233
|
+
const soft = wrapSegments(runs.map((run) => seg(run.text, run.style)), Math.max(1, width));
|
|
26234
|
+
if (soft.length === 0) return [[]];
|
|
26235
|
+
const wrapped = [];
|
|
26236
|
+
for (const line of soft) {
|
|
26237
|
+
const trimmed = trimLeadingSpaces(line);
|
|
26238
|
+
if (segmentsWidth(trimmed) <= width) wrapped.push(trimmed.map((segment) => ({ ...segment })));
|
|
26239
|
+
else wrapped.push(...hardWrapSegments(trimmed, width));
|
|
26240
|
+
}
|
|
26241
|
+
return wrapped;
|
|
26242
|
+
}
|
|
26243
|
+
/** Visible width after removing inline Markdown delimiters. */
|
|
26244
|
+
function tableCellWidth(cell) {
|
|
26245
|
+
return segmentsWidth(parseInline(cell).map((run) => seg(run.text, run.style)));
|
|
26246
|
+
}
|
|
26247
|
+
/** Allocate readable grid widths or request the vertical record fallback. */
|
|
26248
|
+
function tableColumnWidths(table, width) {
|
|
26249
|
+
const columnCount = table.headers.length;
|
|
26250
|
+
const available = width - (columnCount * TABLE_CELL_PADDING + (columnCount - 1) * TABLE_COLUMN_GAP);
|
|
26251
|
+
if (available < columnCount * TABLE_MIN_COLUMN_WIDTH) return void 0;
|
|
26252
|
+
const widths = table.headers.map((header, column) => Math.max(TABLE_MIN_COLUMN_WIDTH, tableCellWidth(header), ...table.rows.map((row) => tableCellWidth(row[column] ?? ""))));
|
|
26253
|
+
let overflow = widths.reduce((total, value) => total + value, 0) - available;
|
|
26254
|
+
while (overflow > 0) {
|
|
26255
|
+
let widest = -1;
|
|
26256
|
+
for (let column = 0; column < widths.length; column += 1) {
|
|
26257
|
+
if ((widths[column] ?? 0) <= TABLE_MIN_COLUMN_WIDTH) continue;
|
|
26258
|
+
if (widest < 0 || (widths[column] ?? 0) > (widths[widest] ?? 0)) widest = column;
|
|
26259
|
+
}
|
|
26260
|
+
if (widest < 0) return void 0;
|
|
26261
|
+
widths[widest] = (widths[widest] ?? TABLE_MIN_COLUMN_WIDTH) - 1;
|
|
26262
|
+
overflow -= 1;
|
|
26263
|
+
}
|
|
26264
|
+
return widths;
|
|
26265
|
+
}
|
|
26266
|
+
/**
|
|
26267
|
+
* Reject grids whose headers or body values become fragmented vertical strips.
|
|
26268
|
+
* This mirrors Codex's readability fallback without importing its larger table
|
|
26269
|
+
* classification machinery: systemic long-token breaks or a seven-line prose
|
|
26270
|
+
* cell are clearer as vertical field records.
|
|
26271
|
+
*/
|
|
26272
|
+
function tableGridIsReadable(table, widths) {
|
|
26273
|
+
if (table.headers.filter((header, column) => tableCellWidth(header) > (widths[column] ?? 0)).length >= 2) return false;
|
|
26274
|
+
let affectedRows = 0;
|
|
26275
|
+
for (const row of table.rows) {
|
|
26276
|
+
let affected = false;
|
|
26277
|
+
for (let column = 0; column < table.headers.length; column += 1) {
|
|
26278
|
+
const width = widths[column] ?? TABLE_MIN_COLUMN_WIDTH;
|
|
26279
|
+
const runs = parseInline(row[column] ?? "");
|
|
26280
|
+
const plain = runs.map((run) => run.text).join("");
|
|
26281
|
+
const fragmentedToken = plain.split(/\s+/u).some((token) => visibleColumns(token) > width);
|
|
26282
|
+
const wrappedHeight = wrapTableCell(runs, width).length;
|
|
26283
|
+
const catastrophicProse = plain.trim().split(/\s+/u).length >= 4 && width < 12 && wrappedHeight >= 7;
|
|
26284
|
+
if (fragmentedToken || catastrophicProse) {
|
|
26285
|
+
affected = true;
|
|
26286
|
+
break;
|
|
26287
|
+
}
|
|
26288
|
+
}
|
|
26289
|
+
if (affected) affectedRows += 1;
|
|
26290
|
+
}
|
|
26291
|
+
const threshold = table.rows.length <= 1 ? 1 : Math.max(2, Math.ceil(table.rows.length / 3));
|
|
26292
|
+
return affectedRows < threshold;
|
|
26293
|
+
}
|
|
26294
|
+
/** Apply one table-cell alignment to a wrapped content row. */
|
|
26295
|
+
function alignedCell(segments, width, alignment) {
|
|
26296
|
+
const remaining = Math.max(0, width - segmentsWidth(segments));
|
|
26297
|
+
if (alignment === "right") return {
|
|
26298
|
+
left: remaining,
|
|
26299
|
+
right: 0
|
|
26300
|
+
};
|
|
26301
|
+
if (alignment === "center") return {
|
|
26302
|
+
left: Math.floor(remaining / 2),
|
|
26303
|
+
right: Math.ceil(remaining / 2)
|
|
26304
|
+
};
|
|
26305
|
+
return {
|
|
26306
|
+
left: 0,
|
|
26307
|
+
right: remaining
|
|
26308
|
+
};
|
|
26309
|
+
}
|
|
26310
|
+
/** Render one logical grid row, including wrapped cell continuations. */
|
|
26311
|
+
function renderTableGridRow(cells, widths, alignments, header) {
|
|
26312
|
+
const wrapped = cells.map((cell, column) => wrapTableCell(parseInline(cell), widths[column] ?? TABLE_MIN_COLUMN_WIDTH));
|
|
26313
|
+
const height = Math.max(1, ...wrapped.map((lines) => lines.length));
|
|
26314
|
+
return Array.from({ length: height }, (_, rowIndex) => {
|
|
26315
|
+
const segments = [];
|
|
26316
|
+
for (let column = 0; column < cells.length; column += 1) {
|
|
26317
|
+
const line = wrapped[column]?.[rowIndex] ?? [];
|
|
26318
|
+
const styled = header ? line.map((segment) => ({
|
|
26319
|
+
...segment,
|
|
26320
|
+
style: "accentBold"
|
|
26321
|
+
})) : line;
|
|
26322
|
+
const alignment = alignedCell(styled, widths[column] ?? TABLE_MIN_COLUMN_WIDTH, alignments[column] ?? "left");
|
|
26323
|
+
segments.push(seg(" ".repeat(TABLE_CELL_PADDING + alignment.left)));
|
|
26324
|
+
segments.push(...styled);
|
|
26325
|
+
if (column + 1 < cells.length) segments.push(seg(" ".repeat(alignment.right + TABLE_COLUMN_GAP)));
|
|
26326
|
+
}
|
|
26327
|
+
return { segments: merge(segments) };
|
|
26328
|
+
});
|
|
26329
|
+
}
|
|
26330
|
+
/** Render a table as Codex-style borderless rows with measured separators. */
|
|
26331
|
+
function renderTableGrid(table, widths) {
|
|
26332
|
+
const separator = (char) => ({ segments: [seg(widths.map((width) => char.repeat(width + TABLE_CELL_PADDING)).join(" ".repeat(TABLE_COLUMN_GAP)), "dim")] });
|
|
26333
|
+
const lines = [...renderTableGridRow(table.headers, widths, table.alignments, true), separator("━")];
|
|
26334
|
+
for (let row = 0; row < table.rows.length; row += 1) {
|
|
26335
|
+
lines.push(...renderTableGridRow(table.rows[row] ?? [], widths, table.alignments, false));
|
|
26336
|
+
if (row + 1 < table.rows.length) lines.push(separator("─"));
|
|
26337
|
+
}
|
|
26338
|
+
return lines;
|
|
26339
|
+
}
|
|
26340
|
+
/** Render an unreadably narrow grid as vertically scannable field records. */
|
|
26341
|
+
function renderTableRecords(table, width) {
|
|
26342
|
+
const labelWidth = Math.max(...table.headers.map(tableCellWidth));
|
|
26343
|
+
const prefixWidth = TABLE_CELL_PADDING + labelWidth + TABLE_COLUMN_GAP;
|
|
26344
|
+
const aligned = prefixWidth + TABLE_MIN_ALIGNED_VALUE_WIDTH <= width;
|
|
26345
|
+
const lines = [];
|
|
26346
|
+
for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex += 1) {
|
|
26347
|
+
const row = table.rows[rowIndex] ?? [];
|
|
26348
|
+
for (let column = 0; column < table.headers.length; column += 1) {
|
|
26349
|
+
const label = table.headers[column] ?? "";
|
|
26350
|
+
const value = row[column] ?? "";
|
|
26351
|
+
const labelRuns = parseInline(label).map((run) => seg(run.text, "accentBold"));
|
|
26352
|
+
if (aligned) {
|
|
26353
|
+
const valueLines = wrapTableCell(parseInline(value), Math.max(1, width - prefixWidth));
|
|
26354
|
+
for (let lineIndex = 0; lineIndex < valueLines.length; lineIndex += 1) {
|
|
26355
|
+
const prefix = lineIndex === 0 ? [
|
|
26356
|
+
seg(" "),
|
|
26357
|
+
...labelRuns,
|
|
26358
|
+
seg(" ".repeat(labelWidth - tableCellWidth(label) + TABLE_COLUMN_GAP))
|
|
26359
|
+
] : [seg(" ".repeat(prefixWidth))];
|
|
26360
|
+
lines.push({ segments: merge([...prefix, ...valueLines[lineIndex] ?? []]) });
|
|
26361
|
+
}
|
|
26362
|
+
} else {
|
|
26363
|
+
lines.push({ segments: merge([seg(" "), ...labelRuns]) });
|
|
26364
|
+
for (const valueLine of wrapTableCell(parseInline(value), Math.max(1, width - 2))) lines.push({ segments: merge([seg(" "), ...valueLine]) });
|
|
26365
|
+
}
|
|
26366
|
+
}
|
|
26367
|
+
if (rowIndex + 1 < table.rows.length) lines.push({ segments: [seg("─".repeat(width), "dim")] });
|
|
26368
|
+
}
|
|
26369
|
+
return lines;
|
|
26370
|
+
}
|
|
25442
26371
|
/** Render markdown text into styled lines of at most `width` columns. */
|
|
25443
26372
|
function renderMarkdown(text, width) {
|
|
25444
26373
|
const lines = [];
|
|
@@ -25450,7 +26379,7 @@ function renderMarkdown(text, width) {
|
|
|
25450
26379
|
if (separatorPending && lines.length > 0 && lines.at(-1)?.segments.length !== 0) lines.push({ segments: [] });
|
|
25451
26380
|
separatorPending = false;
|
|
25452
26381
|
};
|
|
25453
|
-
const source = text.replaceAll("\r", "").split("\n");
|
|
26382
|
+
const source = text.replaceAll("\r", "").replaceAll(" ", " ").split("\n");
|
|
25454
26383
|
let index = 0;
|
|
25455
26384
|
while (index < source.length) {
|
|
25456
26385
|
const line = source[index] ?? "";
|
|
@@ -25460,6 +26389,14 @@ function renderMarkdown(text, width) {
|
|
|
25460
26389
|
continue;
|
|
25461
26390
|
}
|
|
25462
26391
|
startBlock();
|
|
26392
|
+
const table = parseTable(source, index - 1);
|
|
26393
|
+
if (table !== void 0) {
|
|
26394
|
+
const tableWidth = Math.max(10, Math.floor(width));
|
|
26395
|
+
const columnWidths = tableColumnWidths(table, tableWidth);
|
|
26396
|
+
lines.push(...columnWidths === void 0 || !tableGridIsReadable(table, columnWidths) ? renderTableRecords(table, tableWidth) : renderTableGrid(table, columnWidths));
|
|
26397
|
+
index = table.nextIndex;
|
|
26398
|
+
continue;
|
|
26399
|
+
}
|
|
25463
26400
|
const fence = FENCE.exec(line);
|
|
25464
26401
|
if (fence !== null) {
|
|
25465
26402
|
const language = fence[1] ?? "";
|
|
@@ -25628,19 +26565,28 @@ const DEEPSEEK_WAVE_BANDS = {
|
|
|
25628
26565
|
]]
|
|
25629
26566
|
}
|
|
25630
26567
|
};
|
|
26568
|
+
/** Original Codex duration used as the animation's sampling timeline. */
|
|
26569
|
+
function deepseekWaveBaseDuration(tier, style) {
|
|
26570
|
+
switch (style) {
|
|
26571
|
+
case "aurora": return tier === "flash" ? 1300 : 1600;
|
|
26572
|
+
case "pulse": return tier === "flash" ? 900 : 1250;
|
|
26573
|
+
case "wave": return tier === "flash" ? 1e3 : 1300;
|
|
26574
|
+
}
|
|
26575
|
+
}
|
|
25631
26576
|
/**
|
|
25632
|
-
* Total
|
|
25633
|
-
*
|
|
26577
|
+
* Total visible duration: the Codex ignition duration plus 200ms so its motion
|
|
26578
|
+
* remains readable in a busy terminal.
|
|
25634
26579
|
* @param tier - the active wave tier.
|
|
25635
26580
|
* @param style - the active ignition style.
|
|
25636
26581
|
* @returns the duration in milliseconds.
|
|
25637
26582
|
*/
|
|
25638
26583
|
function deepseekWaveDuration(tier, style = "wave") {
|
|
25639
|
-
|
|
25640
|
-
|
|
25641
|
-
|
|
25642
|
-
|
|
25643
|
-
|
|
26584
|
+
return deepseekWaveBaseDuration(tier, style) + 200;
|
|
26585
|
+
}
|
|
26586
|
+
/** Map the extended display timeline back onto the original Codex samples. */
|
|
26587
|
+
function deepseekWaveSampleElapsedMs(tick, tier, style) {
|
|
26588
|
+
const base = deepseekWaveBaseDuration(tier, style);
|
|
26589
|
+
return tick * 33 * base / deepseekWaveDuration(tier, style);
|
|
25644
26590
|
}
|
|
25645
26591
|
/**
|
|
25646
26592
|
* Pick one ignition style at random, never repeating the previous one —
|
|
@@ -25780,8 +26726,8 @@ function blendRgb(fg, bg, alpha) {
|
|
|
25780
26726
|
* @returns the blended RGB background, or null for transparent.
|
|
25781
26727
|
*/
|
|
25782
26728
|
function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base) {
|
|
25783
|
-
const total =
|
|
25784
|
-
const elapsed = tick
|
|
26729
|
+
const total = deepseekWaveBaseDuration(tier, style) / 1e3;
|
|
26730
|
+
const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3;
|
|
25785
26731
|
const fade = style === "aurora" ? envelope(elapsed, total, .25, .4) : 1;
|
|
25786
26732
|
const weights = [
|
|
25787
26733
|
0,
|
|
@@ -25812,14 +26758,14 @@ function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base) {
|
|
|
25812
26758
|
return blendRgb(mixed, base, alpha);
|
|
25813
26759
|
}
|
|
25814
26760
|
/**
|
|
25815
|
-
* The sparkle glyph for a tick — Codex `spark_frame
|
|
25816
|
-
*
|
|
25817
|
-
*
|
|
26761
|
+
* The sparkle glyph for a tick — Codex `spark_frame`, sampled on the same
|
|
26762
|
+
* proportionally slowed DeepSeek Wave timeline as the composer background.
|
|
26763
|
+
* The Ink layer still must skip occupied cells.
|
|
25818
26764
|
* @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
|
|
25819
|
-
* @returns the sparkle glyph, or null outside the
|
|
26765
|
+
* @returns the sparkle glyph, or null outside the stretched tail window.
|
|
25820
26766
|
*/
|
|
25821
26767
|
function deepseekWaveSpark(tick) {
|
|
25822
|
-
const elapsed = tick
|
|
26768
|
+
const elapsed = deepseekWaveSampleElapsedMs(tick, "deepseek", "wave");
|
|
25823
26769
|
if (elapsed < 900) return null;
|
|
25824
26770
|
const frame = Math.floor((elapsed - 900) / 100);
|
|
25825
26771
|
return SPARK_GLYPHS[frame] ?? null;
|
|
@@ -25836,8 +26782,8 @@ function deepseekWaveSpark(tick) {
|
|
|
25836
26782
|
* @returns the blended border RGB.
|
|
25837
26783
|
*/
|
|
25838
26784
|
function deepseekWaveBorderColor(tick, tier, style, hues, dim) {
|
|
25839
|
-
const total =
|
|
25840
|
-
const glow = envelope(tick
|
|
26785
|
+
const total = deepseekWaveBaseDuration(tier, style) / 1e3;
|
|
26786
|
+
const glow = envelope(deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3, total, total * .25, total * .4);
|
|
25841
26787
|
return blendRgb(hues[0], dim, .25 + glow * .6);
|
|
25842
26788
|
}
|
|
25843
26789
|
/**
|
|
@@ -25850,8 +26796,8 @@ function deepseekWaveBorderColor(tick, tier, style, hues, dim) {
|
|
|
25850
26796
|
* @returns true while the wordmark should be visible.
|
|
25851
26797
|
*/
|
|
25852
26798
|
function deepseekWaveWordVisible(tick, tier, style = "wave") {
|
|
25853
|
-
const total =
|
|
25854
|
-
return envelope(tick
|
|
26799
|
+
const total = deepseekWaveBaseDuration(tier, style) / 1e3;
|
|
26800
|
+
return envelope(deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3, total, total * .2, total * .35) > .25;
|
|
25855
26801
|
}
|
|
25856
26802
|
/**
|
|
25857
26803
|
* The per-character color for the `deepseek` wordmark: the tier's hues
|
|
@@ -25916,136 +26862,43 @@ function formatDuration(ms) {
|
|
|
25916
26862
|
* Compact decode rate: one decimal under a hundred, whole below a thousand,
|
|
25917
26863
|
* then thousands (15.3 / 124 / 1.2K).
|
|
25918
26864
|
* @param n - tokens per second.
|
|
25919
|
-
* @returns display string.
|
|
25920
|
-
*/
|
|
25921
|
-
function formatRate(n) {
|
|
25922
|
-
if (n < 100) return String(Math.round(n * 10) / 10);
|
|
25923
|
-
if (n < 1e3) return String(Math.round(n));
|
|
25924
|
-
return String(Math.round(n / 100) / 10) + "K";
|
|
25925
|
-
}
|
|
25926
|
-
/**
|
|
25927
|
-
* Cache-hit share of billed prompt-side input.
|
|
25928
|
-
* @param usage - cumulative token totals.
|
|
25929
|
-
* @returns rounded integer percent, or null when no input was billed.
|
|
25930
|
-
*/
|
|
25931
|
-
function cacheHitPercent(usage) {
|
|
25932
|
-
return usage.inputTokens === 0 ? null : Math.round(usage.cacheReadTokens / usage.inputTokens * 100);
|
|
25933
|
-
}
|
|
25934
|
-
/** Separator between trailing state spans. */
|
|
25935
|
-
const STATUS_ITEM_SEPARATOR = " · ";
|
|
25936
|
-
/** The Codex-style mode cycle hint appended to the permission badge. */
|
|
25937
|
-
const STATUS_CYCLE_HINT = " (shift+tab to cycle)";
|
|
25938
|
-
/** Free-tail floor in columns: wide enough for the bare percent readout, so
|
|
25939
|
-
* the warning stays visible even at 100%+ occupancy. */
|
|
25940
|
-
const CONTEXT_MIN_FREE = 5;
|
|
25941
|
-
/** The five content types in conversation order, dark → light blue. */
|
|
25942
|
-
const CONTEXT_SEGMENTS = [
|
|
25943
|
-
{
|
|
25944
|
-
key: "system",
|
|
25945
|
-
tone: "ctxSystem",
|
|
25946
|
-
labels: [
|
|
25947
|
-
"system",
|
|
25948
|
-
"sys",
|
|
25949
|
-
"s"
|
|
25950
|
-
]
|
|
25951
|
-
},
|
|
25952
|
-
{
|
|
25953
|
-
key: "prompt",
|
|
25954
|
-
tone: "ctxPrompt",
|
|
25955
|
-
labels: [
|
|
25956
|
-
"prompt",
|
|
25957
|
-
"pr",
|
|
25958
|
-
"p"
|
|
25959
|
-
]
|
|
25960
|
-
},
|
|
25961
|
-
{
|
|
25962
|
-
key: "assistant",
|
|
25963
|
-
tone: "ctxAssistant",
|
|
25964
|
-
labels: [
|
|
25965
|
-
"assistant",
|
|
25966
|
-
"ast",
|
|
25967
|
-
"a"
|
|
25968
|
-
]
|
|
25969
|
-
},
|
|
25970
|
-
{
|
|
25971
|
-
key: "thinking",
|
|
25972
|
-
tone: "ctxThinking",
|
|
25973
|
-
labels: [
|
|
25974
|
-
"think",
|
|
25975
|
-
"th",
|
|
25976
|
-
"t"
|
|
25977
|
-
]
|
|
25978
|
-
},
|
|
25979
|
-
{
|
|
25980
|
-
key: "tools",
|
|
25981
|
-
tone: "ctxTools",
|
|
25982
|
-
labels: [
|
|
25983
|
-
"tools",
|
|
25984
|
-
"tl",
|
|
25985
|
-
"x"
|
|
25986
|
-
]
|
|
25987
|
-
}
|
|
25988
|
-
];
|
|
25989
|
-
/** First label whose visible width fits the segment; empty only when none do. */
|
|
25990
|
-
function chooseContextLabel(labels, width) {
|
|
25991
|
-
for (const label of labels) if (visibleColumns(label) <= width) return label;
|
|
25992
|
-
return "";
|
|
25993
|
-
}
|
|
25994
|
-
/** Center a label inside its segment width; the padding spaces carry the tone. */
|
|
25995
|
-
function centerInSegment(text, width) {
|
|
25996
|
-
const textWidth = visibleColumns(text);
|
|
25997
|
-
const left = Math.floor((width - textWidth) / 2);
|
|
25998
|
-
return " ".repeat(Math.max(0, left)) + text + " ".repeat(Math.max(0, width - textWidth - left));
|
|
26865
|
+
* @returns display string.
|
|
26866
|
+
*/
|
|
26867
|
+
function formatRate(n) {
|
|
26868
|
+
if (n < 100) return String(Math.round(n * 10) / 10);
|
|
26869
|
+
if (n < 1e3) return String(Math.round(n));
|
|
26870
|
+
return String(Math.round(n / 100) / 10) + "K";
|
|
25999
26871
|
}
|
|
26000
26872
|
/**
|
|
26001
|
-
*
|
|
26002
|
-
*
|
|
26873
|
+
* Cache-hit share of billed prompt-side input.
|
|
26874
|
+
* @param usage - cumulative token totals.
|
|
26875
|
+
* @returns rounded integer percent, or null when no input was billed.
|
|
26003
26876
|
*/
|
|
26004
|
-
function
|
|
26005
|
-
|
|
26006
|
-
const total = values.reduce((sum, value) => sum + value, 0);
|
|
26007
|
-
if (total <= 0) return values.map(() => 0);
|
|
26008
|
-
const raw = values.map((value) => value / total * columns);
|
|
26009
|
-
const allocated = raw.map(Math.floor);
|
|
26010
|
-
let remaining = columns - allocated.reduce((sum, value) => sum + value, 0);
|
|
26011
|
-
const remainders = raw.map((value, index) => ({
|
|
26012
|
-
index,
|
|
26013
|
-
remainder: value - Math.floor(value)
|
|
26014
|
-
})).sort((left, right) => right.remainder - left.remainder);
|
|
26015
|
-
for (const slot of remainders) {
|
|
26016
|
-
if (remaining <= 0) break;
|
|
26017
|
-
allocated[slot.index] = (allocated[slot.index] ?? 0) + 1;
|
|
26018
|
-
remaining -= 1;
|
|
26019
|
-
}
|
|
26020
|
-
return allocated;
|
|
26021
|
-
}
|
|
26022
|
-
/** Bar column widths: every non-zero segment keeps at least one column before
|
|
26023
|
-
* the remaining columns share by token proportion. */
|
|
26024
|
-
function allocateBarColumns(values, width) {
|
|
26025
|
-
const visible = values.map((value, index) => value > 0 ? index : -1).filter((index) => index >= 0);
|
|
26026
|
-
if (visible.length === 0 || visible.length >= width) return allocateProportionally(values, width);
|
|
26027
|
-
const minimum = values.map(() => 0);
|
|
26028
|
-
for (const index of visible) minimum[index] = 1;
|
|
26029
|
-
const remaining = allocateProportionally(values, width - visible.length);
|
|
26030
|
-
return minimum.map((min, index) => min + (remaining[index] ?? 0));
|
|
26877
|
+
function cacheHitPercent(usage) {
|
|
26878
|
+
return usage.inputTokens === 0 ? null : Math.round(usage.cacheReadTokens / usage.inputTokens * 100);
|
|
26031
26879
|
}
|
|
26880
|
+
/** Separator between trailing state spans. */
|
|
26881
|
+
const STATUS_ITEM_SEPARATOR = " · ";
|
|
26882
|
+
/** The Codex-style mode cycle hint appended to the permission badge. */
|
|
26883
|
+
const STATUS_CYCLE_HINT = " (shift+tab to cycle)";
|
|
26884
|
+
/** Free-tail floor in columns: wide enough for the bare percent readout, so
|
|
26885
|
+
* the warning stays visible even at 100%+ occupancy. */
|
|
26886
|
+
const CONTEXT_MIN_FREE = 5;
|
|
26032
26887
|
/**
|
|
26033
|
-
* Render context occupancy as
|
|
26034
|
-
*
|
|
26035
|
-
*
|
|
26036
|
-
*
|
|
26037
|
-
*
|
|
26038
|
-
*
|
|
26039
|
-
*
|
|
26040
|
-
*
|
|
26041
|
-
* total comes from the reported `lastPromptTokens`, never from the estimates.
|
|
26042
|
-
* @param segments - estimated used tokens per content type.
|
|
26888
|
+
* Render context occupancy as ONE stepless bar: a solid DeepSeek-blue fill
|
|
26889
|
+
* run, a dim dotted free track, and the usage readout riding the track's
|
|
26890
|
+
* right edge (`12.3K/1.0M 25%`, shrinking to the bare percent as the track
|
|
26891
|
+
* narrows). No per-content-type segmentation. Column split is deterministic:
|
|
26892
|
+
* the free share is `Math.round(free/window*width)` clamped to at least
|
|
26893
|
+
* CONTEXT_MIN_FREE columns and at most the full width; the fill takes every
|
|
26894
|
+
* remaining column, so a given occupancy always renders the identical bar.
|
|
26895
|
+
* The readout flips to amber once occupancy reaches the warning threshold.
|
|
26043
26896
|
* @param usedTokens - reported used tokens (drives the readout and percent).
|
|
26044
26897
|
* @param contextWindow - route capacity.
|
|
26045
26898
|
* @param width - total bar interior columns.
|
|
26046
26899
|
* @returns tone-split spans for the footer to paint.
|
|
26047
26900
|
*/
|
|
26048
|
-
function contextBar(
|
|
26901
|
+
function contextBar(usedTokens, contextWindow, width) {
|
|
26049
26902
|
if (width <= 0 || contextWindow <= 0) return [];
|
|
26050
26903
|
const used = Math.max(0, usedTokens);
|
|
26051
26904
|
const percent = Math.round(used / contextWindow * 100);
|
|
@@ -26056,26 +26909,14 @@ function contextBar(segments, usedTokens, contextWindow, width) {
|
|
|
26056
26909
|
const total = `${formatTokens(used)}/${formatTokens(contextWindow)}`;
|
|
26057
26910
|
const percentText = `${percent}%`;
|
|
26058
26911
|
const readout = freeColumns >= visibleColumns(`${total} ${percentText}`) ? `${total} ${percentText}` : freeColumns >= visibleColumns(percentText) ? percentText : "";
|
|
26059
|
-
const values = CONTEXT_SEGMENTS.map((segment) => segments[segment.key]);
|
|
26060
|
-
const allocation = allocateBarColumns(values.reduce((sum, value) => sum + value, 0) > 0 ? values : [
|
|
26061
|
-
0,
|
|
26062
|
-
used,
|
|
26063
|
-
0,
|
|
26064
|
-
0,
|
|
26065
|
-
0
|
|
26066
|
-
], usedColumns);
|
|
26067
26912
|
const spans = [];
|
|
26068
|
-
|
|
26069
|
-
|
|
26070
|
-
|
|
26071
|
-
|
|
26072
|
-
text: centerInSegment(chooseContextLabel(CONTEXT_SEGMENTS[index].labels, columns), columns),
|
|
26073
|
-
tone: CONTEXT_SEGMENTS[index].tone
|
|
26074
|
-
});
|
|
26075
|
-
}
|
|
26913
|
+
if (usedColumns > 0) spans.push({
|
|
26914
|
+
text: "█".repeat(usedColumns),
|
|
26915
|
+
tone: "ctxFill"
|
|
26916
|
+
});
|
|
26076
26917
|
const pad = freeColumns - visibleColumns(readout);
|
|
26077
26918
|
if (pad > 0) spans.push({
|
|
26078
|
-
text: "
|
|
26919
|
+
text: "░".repeat(pad),
|
|
26079
26920
|
tone: "label"
|
|
26080
26921
|
});
|
|
26081
26922
|
if (readout !== "") spans.push({
|
|
@@ -26098,6 +26939,12 @@ const STATUS_ITEMS = [
|
|
|
26098
26939
|
description: "working-directory basename",
|
|
26099
26940
|
side: "left"
|
|
26100
26941
|
},
|
|
26942
|
+
{
|
|
26943
|
+
id: "mode",
|
|
26944
|
+
label: "mode",
|
|
26945
|
+
description: "agent preset composing the session",
|
|
26946
|
+
side: "left"
|
|
26947
|
+
},
|
|
26101
26948
|
{
|
|
26102
26949
|
id: "branch",
|
|
26103
26950
|
label: "branch",
|
|
@@ -26105,15 +26952,21 @@ const STATUS_ITEMS = [
|
|
|
26105
26952
|
side: "left"
|
|
26106
26953
|
},
|
|
26107
26954
|
{
|
|
26108
|
-
id: "
|
|
26109
|
-
label: "
|
|
26110
|
-
description: "
|
|
26955
|
+
id: "context",
|
|
26956
|
+
label: "context",
|
|
26957
|
+
description: "context-window occupancy meter",
|
|
26111
26958
|
side: "left"
|
|
26112
26959
|
},
|
|
26113
26960
|
{
|
|
26114
|
-
id: "
|
|
26115
|
-
label: "
|
|
26116
|
-
description: "
|
|
26961
|
+
id: "permission",
|
|
26962
|
+
label: "permission",
|
|
26963
|
+
description: "permission preset badge with cycle hint",
|
|
26964
|
+
side: "right"
|
|
26965
|
+
},
|
|
26966
|
+
{
|
|
26967
|
+
id: "plan",
|
|
26968
|
+
label: "plan",
|
|
26969
|
+
description: "plan-mode state mark",
|
|
26117
26970
|
side: "left"
|
|
26118
26971
|
},
|
|
26119
26972
|
{
|
|
@@ -26134,12 +26987,6 @@ const STATUS_ITEMS = [
|
|
|
26134
26987
|
description: "cache-hit share of billed input",
|
|
26135
26988
|
side: "left"
|
|
26136
26989
|
},
|
|
26137
|
-
{
|
|
26138
|
-
id: "context",
|
|
26139
|
-
label: "context",
|
|
26140
|
-
description: "context-window occupancy meter",
|
|
26141
|
-
side: "left"
|
|
26142
|
-
},
|
|
26143
26990
|
{
|
|
26144
26991
|
id: "tokens",
|
|
26145
26992
|
label: "tokens",
|
|
@@ -26156,19 +27003,13 @@ const STATUS_ITEMS = [
|
|
|
26156
27003
|
id: "goal",
|
|
26157
27004
|
label: "goal",
|
|
26158
27005
|
description: "live goal phase and round progress",
|
|
26159
|
-
side: "
|
|
27006
|
+
side: "left"
|
|
26160
27007
|
},
|
|
26161
27008
|
{
|
|
26162
27009
|
id: "sandbox",
|
|
26163
27010
|
label: "sandbox",
|
|
26164
27011
|
description: "divergent sandbox-mode override",
|
|
26165
|
-
side: "
|
|
26166
|
-
},
|
|
26167
|
-
{
|
|
26168
|
-
id: "permission",
|
|
26169
|
-
label: "permission",
|
|
26170
|
-
description: "permission preset badge with cycle hint",
|
|
26171
|
-
side: "right"
|
|
27012
|
+
side: "left"
|
|
26172
27013
|
}
|
|
26173
27014
|
];
|
|
26174
27015
|
/**
|
|
@@ -26199,23 +27040,22 @@ const WIDTH_SAFETY = 1;
|
|
|
26199
27040
|
/** Column budget for the session title before it ellipsizes. */
|
|
26200
27041
|
const TITLE_BUDGET = 48;
|
|
26201
27042
|
/**
|
|
26202
|
-
*
|
|
26203
|
-
*
|
|
26204
|
-
*
|
|
26205
|
-
* ellipsizes instead.
|
|
27043
|
+
* Primary-row drop ranks: context drops before permission; the identity
|
|
27044
|
+
* cluster never drops and ellipsizes only after the right badge is gone.
|
|
27045
|
+
* Secondary-row groups reuse the remaining ranks independently.
|
|
26206
27046
|
*/
|
|
26207
27047
|
const RANK_TITLE = 10;
|
|
26208
27048
|
const RANK_TOKENS = 50;
|
|
26209
27049
|
const RANK_COUNTS = 90;
|
|
27050
|
+
const RANK_CONTEXT = 90;
|
|
26210
27051
|
const RANK_SANDBOX = 92;
|
|
26211
27052
|
const RANK_GOAL = 95;
|
|
26212
27053
|
const RANK_BADGE = 100;
|
|
26213
27054
|
const RANK_IDENTITY = Number.POSITIVE_INFINITY;
|
|
26214
|
-
/** Row 2 drop ranks:
|
|
27055
|
+
/** Row 2 drop ranks: title and durations go first; state and counts survive longest. */
|
|
26215
27056
|
const RANK2_DURATIONS = 40;
|
|
26216
27057
|
const RANK2_CACHE = 50;
|
|
26217
|
-
const
|
|
26218
|
-
const RANK2_MODE = 70;
|
|
27058
|
+
const RANK2_PLAN = 70;
|
|
26219
27059
|
/**
|
|
26220
27060
|
* Traffic-light tone for a permission preset: read-only stays success green,
|
|
26221
27061
|
* full access reads error red, and every workspace-scoped middle ground
|
|
@@ -26276,15 +27116,22 @@ function buildCandidates(facts, stats, busy, enabled) {
|
|
|
26276
27116
|
text: cwd,
|
|
26277
27117
|
tone: "path"
|
|
26278
27118
|
});
|
|
27119
|
+
const mode = safe(facts.mode ?? "");
|
|
27120
|
+
if (mode !== "" && enabled.has("mode")) {
|
|
27121
|
+
push({
|
|
27122
|
+
text: "/mode ",
|
|
27123
|
+
tone: "label"
|
|
27124
|
+
});
|
|
27125
|
+
identity.push({
|
|
27126
|
+
text: mode,
|
|
27127
|
+
tone: "accent"
|
|
27128
|
+
});
|
|
27129
|
+
}
|
|
26279
27130
|
const branch = safe(facts.branch);
|
|
26280
27131
|
if (branch !== "" && enabled.has("branch")) push({
|
|
26281
27132
|
text: "⑂ " + branch,
|
|
26282
27133
|
tone: "branch"
|
|
26283
27134
|
});
|
|
26284
|
-
if (facts.plan && enabled.has("plan")) push({
|
|
26285
|
-
text: "⧉ plan",
|
|
26286
|
-
tone: "accent"
|
|
26287
|
-
});
|
|
26288
27135
|
const left = [{
|
|
26289
27136
|
group: { spans: identity },
|
|
26290
27137
|
rank: RANK_IDENTITY,
|
|
@@ -26292,17 +27139,13 @@ function buildCandidates(facts, stats, busy, enabled) {
|
|
|
26292
27139
|
}];
|
|
26293
27140
|
const right = [];
|
|
26294
27141
|
const row2 = [];
|
|
26295
|
-
|
|
26296
|
-
if (mode !== "" && enabled.has("mode")) row2.push({
|
|
27142
|
+
if (facts.plan && enabled.has("plan")) row2.push({
|
|
26297
27143
|
group: { spans: [{
|
|
26298
|
-
text: "
|
|
26299
|
-
tone: "label"
|
|
26300
|
-
}, {
|
|
26301
|
-
text: mode,
|
|
27144
|
+
text: "⧉ plan",
|
|
26302
27145
|
tone: "accent"
|
|
26303
27146
|
}] },
|
|
26304
|
-
rank:
|
|
26305
|
-
id: "
|
|
27147
|
+
rank: RANK2_PLAN,
|
|
27148
|
+
id: "plan"
|
|
26306
27149
|
});
|
|
26307
27150
|
if (stats.turns > 0 || stats.steps > 0) {
|
|
26308
27151
|
if (enabled.has("turns")) {
|
|
@@ -26319,7 +27162,7 @@ function buildCandidates(facts, stats, busy, enabled) {
|
|
|
26319
27162
|
};
|
|
26320
27163
|
pair("turns", String(stats.turns));
|
|
26321
27164
|
pair("steps", String(stats.steps));
|
|
26322
|
-
|
|
27165
|
+
row2.push({
|
|
26323
27166
|
group: { spans: counts },
|
|
26324
27167
|
rank: RANK_COUNTS,
|
|
26325
27168
|
id: "turns"
|
|
@@ -26369,12 +27212,12 @@ function buildCandidates(facts, stats, busy, enabled) {
|
|
|
26369
27212
|
rank: RANK2_CACHE,
|
|
26370
27213
|
id: "cache"
|
|
26371
27214
|
});
|
|
26372
|
-
if (stats.contextWindow > 0 && stats.lastPromptTokens > 0 && enabled.has("context"))
|
|
27215
|
+
if (stats.contextWindow > 0 && stats.lastPromptTokens > 0 && enabled.has("context")) left.push({
|
|
26373
27216
|
group: { spans: [{
|
|
26374
27217
|
text: "context ",
|
|
26375
27218
|
tone: "label"
|
|
26376
|
-
}, ...contextBar(stats.
|
|
26377
|
-
rank:
|
|
27219
|
+
}, ...contextBar(stats.lastPromptTokens, stats.contextWindow, 24)] },
|
|
27220
|
+
rank: RANK_CONTEXT,
|
|
26378
27221
|
id: "context"
|
|
26379
27222
|
});
|
|
26380
27223
|
if ((stats.usage.inputTokens > 0 || stats.usage.outputTokens > 0) && enabled.has("tokens")) {
|
|
@@ -26391,14 +27234,14 @@ function buildCandidates(facts, stats, busy, enabled) {
|
|
|
26391
27234
|
};
|
|
26392
27235
|
pair("in", formatTokens(stats.usage.inputTokens));
|
|
26393
27236
|
pair("out", formatTokens(stats.usage.outputTokens));
|
|
26394
|
-
|
|
27237
|
+
row2.push({
|
|
26395
27238
|
group: { spans: tokens },
|
|
26396
27239
|
rank: RANK_TOKENS,
|
|
26397
27240
|
id: "tokens"
|
|
26398
27241
|
});
|
|
26399
27242
|
}
|
|
26400
27243
|
const label = truncateColumns(safe(facts.title !== void 0 && facts.title !== "" ? facts.title : facts.sessionId), TITLE_BUDGET);
|
|
26401
|
-
if (label !== "" && enabled.has("title"))
|
|
27244
|
+
if (label !== "" && enabled.has("title")) row2.push({
|
|
26402
27245
|
group: { spans: [{
|
|
26403
27246
|
text: label,
|
|
26404
27247
|
tone: "meta"
|
|
@@ -26406,20 +27249,20 @@ function buildCandidates(facts, stats, busy, enabled) {
|
|
|
26406
27249
|
rank: RANK_TITLE,
|
|
26407
27250
|
id: "title"
|
|
26408
27251
|
});
|
|
26409
|
-
if (facts.goal !== void 0 && enabled.has("goal"))
|
|
26410
|
-
|
|
27252
|
+
if (facts.goal !== void 0 && enabled.has("goal")) row2.push({
|
|
27253
|
+
group: { spans: [{
|
|
26411
27254
|
text: facts.goal.phase === "active" ? "◎ round " + facts.goal.rounds + "/" + facts.goal.max : "◎ " + safe(facts.goal.phase),
|
|
26412
27255
|
tone: "accent"
|
|
26413
|
-
},
|
|
27256
|
+
}] },
|
|
26414
27257
|
rank: RANK_GOAL,
|
|
26415
27258
|
id: "goal"
|
|
26416
27259
|
});
|
|
26417
27260
|
const sandbox = safe(facts.sandbox ?? "");
|
|
26418
|
-
if (sandbox !== "" && sandbox.toLowerCase() !== facts.permission.toLowerCase() && enabled.has("sandbox"))
|
|
26419
|
-
|
|
27261
|
+
if (sandbox !== "" && sandbox.toLowerCase() !== facts.permission.toLowerCase() && enabled.has("sandbox")) row2.push({
|
|
27262
|
+
group: { spans: [{
|
|
26420
27263
|
text: "sandbox " + sandbox,
|
|
26421
27264
|
tone: "warn"
|
|
26422
|
-
},
|
|
27265
|
+
}] },
|
|
26423
27266
|
rank: RANK_SANDBOX,
|
|
26424
27267
|
id: "sandbox"
|
|
26425
27268
|
});
|
|
@@ -26444,12 +27287,10 @@ function buildCandidates(facts, stats, busy, enabled) {
|
|
|
26444
27287
|
};
|
|
26445
27288
|
}
|
|
26446
27289
|
/**
|
|
26447
|
-
* Compose the two-row footer layout under a column budget. Row 1
|
|
26448
|
-
*
|
|
26449
|
-
*
|
|
26450
|
-
*
|
|
26451
|
-
* (mode, context bar, cache, duration figures) fits its own budget and
|
|
26452
|
-
* degrades to empty before any row-1 content is touched.
|
|
27290
|
+
* Compose the two-row footer layout under a column budget. Row 1 keeps model,
|
|
27291
|
+
* cwd, mode, branch, context, then the right-pinned permission badge and cycle
|
|
27292
|
+
* hint. It drops hint, context, and permission before ellipsizing identity.
|
|
27293
|
+
* Row 2 fits all secondary figures and state within its own budget.
|
|
26453
27294
|
* @param facts - identity facts resolved by the runner.
|
|
26454
27295
|
* @param stats - session figures folded from the durable log.
|
|
26455
27296
|
* @param columns - usable columns for each row (before their left padding).
|
|
@@ -26525,8 +27366,9 @@ function layoutStatusBar(facts, stats, columns, options = {}) {
|
|
|
26525
27366
|
};
|
|
26526
27367
|
}
|
|
26527
27368
|
const row2Kept = [...orderedRow2];
|
|
27369
|
+
const row2Budget = Math.max(1, budget - 2);
|
|
26528
27370
|
const row2Width = () => joinWidth(row2Kept.map((entry) => spansWidth(entry.group.spans)), groupSeparator);
|
|
26529
|
-
while (row2Width() >
|
|
27371
|
+
while (row2Width() > row2Budget && row2Kept.length > 0) {
|
|
26530
27372
|
let dropIndex = 0;
|
|
26531
27373
|
let dropRank = Number.POSITIVE_INFINITY;
|
|
26532
27374
|
for (let index = 0; index < row2Kept.length; index += 1) if (row2Kept[index].rank < dropRank) {
|
|
@@ -26612,6 +27454,17 @@ function markdownLines(text, columns) {
|
|
|
26612
27454
|
const width = Math.max(1, Math.floor(columns));
|
|
26613
27455
|
return renderMarkdown(displayText(text), Math.max(10, width)).flatMap((line) => styledLines(line.segments.map((segment) => lineSegment(segment.text, segment.style)), width));
|
|
26614
27456
|
}
|
|
27457
|
+
/**
|
|
27458
|
+
* Codex-style reasoning rows: the marker occupies the reply gutter and every
|
|
27459
|
+
* wrapped or explicit continuation starts with the same two-column indent, so
|
|
27460
|
+
* reasoning content and assistant Markdown share one left edge.
|
|
27461
|
+
*/
|
|
27462
|
+
function reasoningLines(text, columns) {
|
|
27463
|
+
const width = Math.max(1, Math.floor(columns));
|
|
27464
|
+
if (width < 3) return textLines(text, width, "dimItalic");
|
|
27465
|
+
const contentWidth = width - 2;
|
|
27466
|
+
return displayText(text).replaceAll(" ", " ").replaceAll("\r", "").split("\n").flatMap((line) => styledLines([lineSegment(line, "dimItalic")], contentWidth)).map((line, index) => ({ segments: [lineSegment(index === 0 ? "✻ " : " ", "dimItalic"), ...line.segments] }));
|
|
27467
|
+
}
|
|
26615
27468
|
/** Expanded structured tool detail as scrollable, width-safe rows. */
|
|
26616
27469
|
function toolDetailLines(detail, columns) {
|
|
26617
27470
|
switch (detail.kind) {
|
|
@@ -26637,7 +27490,7 @@ function transcriptEntryLines(entry, columns) {
|
|
|
26637
27490
|
case "user": return styledLines([lineSegment(entry.notice ? "⤷ " : "❯ ", entry.notice ? "dim" : "brand"), lineSegment(entry.text, entry.notice ? "dim" : "plain")], width);
|
|
26638
27491
|
case "pending": return styledLines([lineSegment("❯ ", "brand"), lineSegment(entry.text, "plain")], width);
|
|
26639
27492
|
case "assistant": {
|
|
26640
|
-
const reasoning = entry.reasoning === "" ? [] :
|
|
27493
|
+
const reasoning = entry.reasoning === "" ? [] : reasoningLines(entry.reasoning, width);
|
|
26641
27494
|
const body = markdownLines(entry.text, Math.max(10, width - 2)).map((line) => ({ segments: [{
|
|
26642
27495
|
text: " ",
|
|
26643
27496
|
style: "plain"
|
|
@@ -26681,7 +27534,7 @@ function ListFrame(props) {
|
|
|
26681
27534
|
const stdout = useStdout().stdout;
|
|
26682
27535
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
26683
27536
|
if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
|
|
26684
|
-
if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`${props.title} · esc close
|
|
27537
|
+
if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(singleLineText(`${props.title} · esc close`), viewport.contentColumns));
|
|
26685
27538
|
const stateRows = props.loading ? [{
|
|
26686
27539
|
key: "loading",
|
|
26687
27540
|
text: " loading…"
|
|
@@ -26704,10 +27557,10 @@ function ListFrame(props) {
|
|
|
26704
27557
|
}, (0, import_react.createElement)(Text, {
|
|
26705
27558
|
color: inkColor(getPalette().brandBright),
|
|
26706
27559
|
wrap: "truncate-end"
|
|
26707
|
-
}, truncateColumns(props.title, viewport.contentColumns)), (0, import_react.createElement)(Text, {
|
|
27560
|
+
}, truncateColumns(singleLineText(props.title), viewport.contentColumns)), (0, import_react.createElement)(Text, {
|
|
26708
27561
|
dimColor: true,
|
|
26709
27562
|
wrap: "truncate-end"
|
|
26710
|
-
}, truncateColumns(`search: ${props.query === "" ? "type to filter" : props.query}
|
|
27563
|
+
}, truncateColumns(singleLineText(`search: ${props.query === "" ? "type to filter" : props.query}`), viewport.contentColumns)), ...visible.map((row, index) => {
|
|
26711
27564
|
const absolute = offset + index;
|
|
26712
27565
|
const selected = !props.loading && props.error === void 0 && props.rows.length > 0 && absolute === props.cursor;
|
|
26713
27566
|
return (0, import_react.createElement)(Text, {
|
|
@@ -26715,11 +27568,11 @@ function ListFrame(props) {
|
|
|
26715
27568
|
color: selected ? inkColor(getPalette().brandBright) : row.disabled ? inkColor(getPalette().dim) : void 0,
|
|
26716
27569
|
dimColor: row.disabled,
|
|
26717
27570
|
wrap: "truncate-end"
|
|
26718
|
-
}, truncateColumns(`${selected ? "› " : " "}${row.text}`, viewport.contentColumns));
|
|
27571
|
+
}, truncateColumns(`${selected ? "› " : " "}${singleLineText(row.text)}`, viewport.contentColumns));
|
|
26719
27572
|
}), (0, import_react.createElement)(Text, {
|
|
26720
27573
|
dimColor: true,
|
|
26721
27574
|
wrap: "truncate-end"
|
|
26722
|
-
}, truncateColumns(props.footer, viewport.contentColumns)));
|
|
27575
|
+
}, truncateColumns(singleLineText(props.footer), viewport.contentColumns)));
|
|
26723
27576
|
}
|
|
26724
27577
|
function editQuery(query, input, key) {
|
|
26725
27578
|
if (key.backspace || key.delete) return query.slice(0, -1);
|
|
@@ -26771,6 +27624,51 @@ function ModePanel({ current, load, select, close }) {
|
|
|
26771
27624
|
footer: "↑↓ choose · enter switch · r refresh · esc close"
|
|
26772
27625
|
});
|
|
26773
27626
|
}
|
|
27627
|
+
function PermissionPanel({ current, load, select, close }) {
|
|
27628
|
+
const [rows, setRows] = (0, import_react.useState)([]);
|
|
27629
|
+
const [query, setQuery] = (0, import_react.useState)("");
|
|
27630
|
+
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
27631
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
27632
|
+
const [error, setError] = (0, import_react.useState)();
|
|
27633
|
+
const refresh = () => {
|
|
27634
|
+
setLoading(true);
|
|
27635
|
+
setError(void 0);
|
|
27636
|
+
Promise.resolve().then(load).then((value) => {
|
|
27637
|
+
setRows(value);
|
|
27638
|
+
setLoading(false);
|
|
27639
|
+
}, (reason) => {
|
|
27640
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
27641
|
+
setLoading(false);
|
|
27642
|
+
});
|
|
27643
|
+
};
|
|
27644
|
+
(0, import_react.useEffect)(refresh, []);
|
|
27645
|
+
const visible = (0, import_react.useMemo)(() => rows.filter((row) => `${row.id} ${row.description ?? ""}`.toLowerCase().includes(query.toLowerCase())), [rows, query]);
|
|
27646
|
+
(0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, visible.length - 1))), [visible.length]);
|
|
27647
|
+
useInput((input, key) => {
|
|
27648
|
+
if (key.escape || input === "q") return close();
|
|
27649
|
+
if (input === "r" && query === "") return refresh();
|
|
27650
|
+
if (key.upArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length);
|
|
27651
|
+
if (key.downArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + 1) % visible.length);
|
|
27652
|
+
if (key.return && visible[cursor] !== void 0) return select(visible[cursor].id);
|
|
27653
|
+
const next = editQuery(query, input, key);
|
|
27654
|
+
if (next !== void 0) {
|
|
27655
|
+
setQuery(next);
|
|
27656
|
+
setCursor(0);
|
|
27657
|
+
}
|
|
27658
|
+
});
|
|
27659
|
+
return (0, import_react.createElement)(ListFrame, {
|
|
27660
|
+
title: `/permission · current ${current}`,
|
|
27661
|
+
rows: visible.map((row) => ({
|
|
27662
|
+
key: row.id,
|
|
27663
|
+
text: `${row.id === current ? "●" : "○"} ${row.id}${row.description === void 0 ? "" : ` · ${row.description}`}`
|
|
27664
|
+
})),
|
|
27665
|
+
cursor,
|
|
27666
|
+
loading,
|
|
27667
|
+
error,
|
|
27668
|
+
query,
|
|
27669
|
+
footer: "↑↓ choose · enter select · r refresh · esc close"
|
|
27670
|
+
});
|
|
27671
|
+
}
|
|
26774
27672
|
function PluginPanel({ load, close, initialQuery = "" }) {
|
|
26775
27673
|
const [epoch, setEpoch] = (0, import_react.useState)(0);
|
|
26776
27674
|
const [query, setQuery] = (0, import_react.useState)(initialQuery);
|
|
@@ -26950,7 +27848,7 @@ function DocumentPanel({ title, text, error, close }) {
|
|
|
26950
27848
|
}, (0, import_react.createElement)(Text, {
|
|
26951
27849
|
color: inkColor(getPalette().brandBright),
|
|
26952
27850
|
wrap: "truncate-end"
|
|
26953
|
-
}, truncateColumns(title, viewport.contentColumns)), ...body.map((line, index) => (0, import_react.createElement)(Text, {
|
|
27851
|
+
}, truncateColumns(singleLineText(title), viewport.contentColumns)), ...body.map((line, index) => (0, import_react.createElement)(Text, {
|
|
26954
27852
|
key: `${scroll}-${index}`,
|
|
26955
27853
|
wrap: "truncate-end"
|
|
26956
27854
|
}, truncateColumns(line, viewport.contentColumns))), (0, import_react.createElement)(Text, {
|
|
@@ -27020,7 +27918,7 @@ function HistoryPanel({ entries, fill, close }) {
|
|
|
27020
27918
|
key: `history-${absolute}`,
|
|
27021
27919
|
color: selected ? inkColor(getPalette().brandBright) : void 0,
|
|
27022
27920
|
wrap: "truncate-end"
|
|
27023
|
-
}, truncateColumns((selected ? "› " : " ") +
|
|
27921
|
+
}, truncateColumns((selected ? "› " : " ") + singleLineText(entry), viewport.contentColumns));
|
|
27024
27922
|
}), (0, import_react.createElement)(Text, {
|
|
27025
27923
|
dimColor: true,
|
|
27026
27924
|
wrap: "truncate-end"
|
|
@@ -27168,7 +28066,7 @@ function serializeHistoryEntry(text) {
|
|
|
27168
28066
|
* @param max - entry cap.
|
|
27169
28067
|
* @returns persistent entries, oldest first.
|
|
27170
28068
|
*/
|
|
27171
|
-
function parseHistoryFile(raw, max =
|
|
28069
|
+
function parseHistoryFile(raw, max = 100) {
|
|
27172
28070
|
const kept = [];
|
|
27173
28071
|
for (const line of raw.split("\n")) {
|
|
27174
28072
|
if (line === "") continue;
|
|
@@ -27185,27 +28083,30 @@ function parseHistoryFile(raw, max = 500) {
|
|
|
27185
28083
|
return kept.slice(-max);
|
|
27186
28084
|
}
|
|
27187
28085
|
/**
|
|
27188
|
-
* Append one entry to the persistent file content: JSON line, capped to the
|
|
27189
|
-
* newest `max` entries with a trailing newline.
|
|
27190
|
-
* @param current - existing file content.
|
|
27191
|
-
* @param text - submission to persist.
|
|
27192
|
-
* @param max - entry cap.
|
|
27193
|
-
* @returns the new file content.
|
|
27194
|
-
*/
|
|
27195
|
-
function appendHistoryContent(current, text, max = 500) {
|
|
27196
|
-
return [...parseHistoryFile(current, max), text].slice(-max).map(serializeHistoryEntry).join("\n") + "\n";
|
|
27197
|
-
}
|
|
27198
|
-
/**
|
|
27199
28086
|
* Record one in-session submission: empty text is ignored and an adjacent
|
|
27200
|
-
* duplicate collapses (Codex `record_local_submission` semantics).
|
|
28087
|
+
* duplicate collapses (Codex `record_local_submission` semantics). The local
|
|
28088
|
+
* pool shares the persistent pool's cap so the recall space stays bounded.
|
|
27201
28089
|
* @param local - current in-session entries, oldest first.
|
|
27202
28090
|
* @param text - the submitted prompt.
|
|
28091
|
+
* @param max - the local pool cap.
|
|
27203
28092
|
* @returns the updated local list.
|
|
27204
28093
|
*/
|
|
27205
|
-
function recordLocalEntry(local, text) {
|
|
28094
|
+
function recordLocalEntry(local, text, max = 100) {
|
|
27206
28095
|
if (text === "") return local;
|
|
27207
28096
|
if (local.length > 0 && local[local.length - 1] === text) return local;
|
|
27208
|
-
return [...local, text];
|
|
28097
|
+
return [...local, text].slice(-max);
|
|
28098
|
+
}
|
|
28099
|
+
/**
|
|
28100
|
+
* Serialize a capped entry list to the history file format (one JSON line per
|
|
28101
|
+
* entry, trailing newline). The runner writes the in-memory list as the whole
|
|
28102
|
+
* file, so rapid same-process submissions cannot lose entries to a
|
|
28103
|
+
* read-modify-write race (the file is never read back before writing).
|
|
28104
|
+
* @param entries - the entries to persist, oldest first.
|
|
28105
|
+
* @returns the file content, '' for an empty list.
|
|
28106
|
+
*/
|
|
28107
|
+
function serializeHistoryList(entries) {
|
|
28108
|
+
if (entries.length === 0) return "";
|
|
28109
|
+
return entries.map(serializeHistoryEntry).join("\n") + "\n";
|
|
27209
28110
|
}
|
|
27210
28111
|
/**
|
|
27211
28112
|
* Build the recall space, newest first: local entries, then persistent
|
|
@@ -27386,13 +28287,18 @@ function DeepDivingLine({ since }) {
|
|
|
27386
28287
|
* the freshest tokens stay visible while a long reply streams; the complete
|
|
27387
28288
|
* text lands in the flushed scrollback once the turn assembles it.
|
|
27388
28289
|
*/
|
|
27389
|
-
function StreamTail({ text, dim, maxRows, prefix, children }) {
|
|
28290
|
+
function StreamTail({ text, dim, maxRows, prefix = "", continuationPrefix = prefix, children }) {
|
|
27390
28291
|
const columns = useStdout().stdout?.columns ?? 80;
|
|
27391
28292
|
const safeRows = Math.max(1, maxRows);
|
|
27392
|
-
const
|
|
28293
|
+
const prefixColumns = Math.max(visibleColumns(prefix), visibleColumns(continuationPrefix));
|
|
28294
|
+
const contentColumns = Math.max(10, columns - 3 - prefixColumns);
|
|
27393
28295
|
const initial = displayTail(text, contentColumns, safeRows);
|
|
27394
28296
|
const tail = initial.truncated && safeRows > 1 ? displayTail(text, contentColumns, safeRows - 1) : initial;
|
|
27395
|
-
|
|
28297
|
+
const rows = tail.text.split("\n");
|
|
28298
|
+
return (0, import_react.createElement)(Box, { flexDirection: "column" }, tail.truncated && safeRows > 1 ? (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, continuationPrefix, "…") : void 0, ...rows.map((row, index) => (0, import_react.createElement)(Text, {
|
|
28299
|
+
key: index,
|
|
28300
|
+
dimColor: dim || void 0
|
|
28301
|
+
}, index === 0 ? prefix : continuationPrefix, row, index + 1 === rows.length ? children : void 0)));
|
|
27396
28302
|
}
|
|
27397
28303
|
/** Ink props for one markdown style class. */
|
|
27398
28304
|
function segmentProps(style) {
|
|
@@ -27403,6 +28309,12 @@ function segmentProps(style) {
|
|
|
27403
28309
|
italic: void 0,
|
|
27404
28310
|
strikethrough: void 0
|
|
27405
28311
|
};
|
|
28312
|
+
case "accentBold": return {
|
|
28313
|
+
color: inkColor(getPalette().brandBright),
|
|
28314
|
+
bold: true,
|
|
28315
|
+
italic: void 0,
|
|
28316
|
+
strikethrough: void 0
|
|
28317
|
+
};
|
|
27406
28318
|
case "code": return {
|
|
27407
28319
|
color: inkColor(getPalette().code),
|
|
27408
28320
|
bold: void 0,
|
|
@@ -27479,11 +28391,11 @@ function lineStyleProps(style) {
|
|
|
27479
28391
|
dimColor: void 0
|
|
27480
28392
|
};
|
|
27481
28393
|
case "dimItalic": return {
|
|
27482
|
-
color:
|
|
28394
|
+
color: inkColor(getPalette().dim),
|
|
27483
28395
|
bold: void 0,
|
|
27484
28396
|
italic: true,
|
|
27485
28397
|
strikethrough: void 0,
|
|
27486
|
-
dimColor:
|
|
28398
|
+
dimColor: void 0
|
|
27487
28399
|
};
|
|
27488
28400
|
default: return {
|
|
27489
28401
|
...segmentProps(style),
|
|
@@ -27521,6 +28433,12 @@ function MarkdownBody({ text, indent = 0 }) {
|
|
|
27521
28433
|
...segmentProps(segment.style)
|
|
27522
28434
|
}, segment.text)))));
|
|
27523
28435
|
}
|
|
28436
|
+
/** Expanded reasoning with the same two-column content edge as the reply. */
|
|
28437
|
+
function ReasoningBody({ text }) {
|
|
28438
|
+
const columns = useStdout().stdout?.columns ?? 80;
|
|
28439
|
+
const lines = (0, import_react.useMemo)(() => reasoningLines(text, Math.max(10, columns - 2)), [text, columns]);
|
|
28440
|
+
return (0, import_react.createElement)(StyledRows, { lines });
|
|
28441
|
+
}
|
|
27524
28442
|
/**
|
|
27525
28443
|
* One expanded tool-card body for the verbose transcript (Ctrl+O): the
|
|
27526
28444
|
* presentation contract's structured cards — inline diffs, read windows,
|
|
@@ -27568,23 +28486,20 @@ function ToolDetailBody({ detail }) {
|
|
|
27568
28486
|
function EntryLine({ entry, showReasoning, verbose }) {
|
|
27569
28487
|
switch (entry.kind) {
|
|
27570
28488
|
case "user": return entry.notice ? (0, import_react.createElement)(Text, { dimColor: true }, `⤷ ${displayText(entry.text)}`) : (0, import_react.createElement)(Text, null, brand("❯ "), displayText(entry.text));
|
|
27571
|
-
case "assistant": return (0, import_react.createElement)(Box, { flexDirection: "column" }, entry.reasoning === "" ? void 0 : showReasoning ? (0, import_react.createElement)(Text, {
|
|
27572
|
-
dimColor: true,
|
|
27573
|
-
italic: true
|
|
27574
|
-
}, ` ✻ ${displayText(entry.reasoning)}`) : (0, import_react.createElement)(Text, { dimColor: true }, ` ✻ Thinking (${entry.reasoning.length} chars, Ctrl+R to expand)`), (0, import_react.createElement)(MarkdownBody, {
|
|
28489
|
+
case "assistant": return (0, import_react.createElement)(Box, { flexDirection: "column" }, entry.reasoning === "" ? void 0 : showReasoning ? (0, import_react.createElement)(ReasoningBody, { text: entry.reasoning }) : (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, `✻ Thinking (${entry.reasoning.length} chars, Ctrl+R to expand)`), (0, import_react.createElement)(MarkdownBody, {
|
|
27575
28490
|
text: entry.text,
|
|
27576
28491
|
indent: 2
|
|
27577
28492
|
}));
|
|
27578
28493
|
case "tool": {
|
|
27579
28494
|
const mark = entry.state === "running" ? (0, import_react.createElement)(Pulse) : entry.state === "error" ? (0, import_react.createElement)(Text, { color: inkColor(getPalette().error) }, "⨯") : (0, import_react.createElement)(Text, { color: inkColor(getPalette().success) }, "⏺");
|
|
27580
|
-
return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, mark, " ", brand(entry.name), entry.preview === "" ? "" : ` ${dim(displayText(entry.preview))}`), entry.summary === "" ? void 0 : (0, import_react.createElement)(Text, {
|
|
28495
|
+
return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, mark, " ", brand(displayText(entry.name)), entry.preview === "" ? "" : ` ${dim(displayText(entry.preview))}`), entry.summary === "" ? void 0 : (0, import_react.createElement)(Text, {
|
|
27581
28496
|
color: entry.state === "error" ? inkColor(getPalette().error) : inkColor(getPalette().dim),
|
|
27582
28497
|
wrap: verbose ? "truncate-end" : void 0
|
|
27583
28498
|
}, ` ⎿ ${displayText(entry.summary)}`), verbose && entry.detail !== void 0 ? (0, import_react.createElement)(ToolDetailBody, { detail: entry.detail }) : void 0);
|
|
27584
28499
|
}
|
|
27585
28500
|
case "command": {
|
|
27586
28501
|
const mark = entry.state === "running" ? (0, import_react.createElement)(Pulse) : entry.state === "error" ? (0, import_react.createElement)(Text, { color: inkColor(getPalette().error) }, "⨯") : (0, import_react.createElement)(Text, { color: inkColor(getPalette().success) }, "⏺");
|
|
27587
|
-
return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, mark, " ", brand(`/${entry.name}`), entry.args === "" ? "" : ` ${dim(displayText(entry.args))}`), entry.summary === "" ? void 0 : (0, import_react.createElement)(Text, {
|
|
28502
|
+
return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, mark, " ", brand(displayText(`/${entry.name}`)), entry.args === "" ? "" : ` ${dim(displayText(entry.args))}`), entry.summary === "" ? void 0 : (0, import_react.createElement)(Text, {
|
|
27588
28503
|
color: inkColor(getPalette().dim),
|
|
27589
28504
|
wrap: verbose ? "truncate-end" : void 0
|
|
27590
28505
|
}, ` ⎿ ${displayText(entry.summary)}`));
|
|
@@ -27615,26 +28530,30 @@ function EntryLine({ entry, showReasoning, verbose }) {
|
|
|
27615
28530
|
}
|
|
27616
28531
|
}
|
|
27617
28532
|
/**
|
|
27618
|
-
* The whale
|
|
27619
|
-
*
|
|
27620
|
-
*
|
|
27621
|
-
*
|
|
27622
|
-
* wordmark that stays correct at any size.
|
|
28533
|
+
* The whale header with a compact three-line copy lockup. The title, bilingual
|
|
28534
|
+
* slogan, and key hint stay centered inside the existing eight content rows,
|
|
28535
|
+
* preserving the Static header's ten physical rows. Short or narrow terminals
|
|
28536
|
+
* keep a one-line form.
|
|
27623
28537
|
*/
|
|
27624
28538
|
function Header({ resumed }) {
|
|
27625
|
-
const
|
|
27626
|
-
const
|
|
27627
|
-
|
|
27628
|
-
|
|
27629
|
-
|
|
28539
|
+
const stdout = useStdout().stdout;
|
|
28540
|
+
const rows = stdout?.rows ?? 40;
|
|
28541
|
+
const columns = stdout?.columns ?? 80;
|
|
28542
|
+
const title = `DeepSeek Harness · v${DSH_CODE_VERSION}`;
|
|
28543
|
+
const slogan = "Into the Unknown 探索未至之境";
|
|
28544
|
+
const hint = resumed ? "resumed · /help · Esc interrupt" : "/help · Esc interrupt · Ctrl+C quit";
|
|
28545
|
+
const copyColumns = Math.max(visibleColumns(title), visibleColumns(slogan), visibleColumns(hint));
|
|
28546
|
+
const compact = `${title} · ${hint}`;
|
|
28547
|
+
if (rows < 20 || columns < 26 + copyColumns + 8) return (0, import_react.createElement)(Box, {
|
|
28548
|
+
width: Math.max(1, columns - 1),
|
|
27630
28549
|
borderStyle: "round",
|
|
27631
28550
|
borderColor: inkColor(getPalette().brand),
|
|
27632
|
-
paddingX: 1
|
|
27633
|
-
alignSelf: "flex-start"
|
|
28551
|
+
paddingX: 1
|
|
27634
28552
|
}, (0, import_react.createElement)(Text, {
|
|
27635
28553
|
color: inkColor(getPalette().brandBright),
|
|
27636
|
-
bold: true
|
|
27637
|
-
|
|
28554
|
+
bold: true,
|
|
28555
|
+
wrap: "truncate-end"
|
|
28556
|
+
}, truncateColumns(compact, Math.max(1, columns - 5))));
|
|
27638
28557
|
return (0, import_react.createElement)(Box, {
|
|
27639
28558
|
flexDirection: "row",
|
|
27640
28559
|
gap: 2,
|
|
@@ -27651,11 +28570,19 @@ function Header({ resumed }) {
|
|
|
27651
28570
|
color: inkColor(getPalette().brand)
|
|
27652
28571
|
}, row))), (0, import_react.createElement)(Box, {
|
|
27653
28572
|
flexDirection: "column",
|
|
28573
|
+
width: copyColumns,
|
|
27654
28574
|
justifyContent: "center"
|
|
27655
28575
|
}, (0, import_react.createElement)(Text, {
|
|
27656
28576
|
color: inkColor(getPalette().brandBright),
|
|
27657
|
-
bold: true
|
|
27658
|
-
|
|
28577
|
+
bold: true,
|
|
28578
|
+
wrap: "truncate-end"
|
|
28579
|
+
}, title), (0, import_react.createElement)(Text, {
|
|
28580
|
+
color: inkColor(getPalette().code),
|
|
28581
|
+
wrap: "truncate-end"
|
|
28582
|
+
}, (0, import_react.createElement)(Text, { bold: true }, "Into the Unknown"), " 探索未至之境"), (0, import_react.createElement)(Text, {
|
|
28583
|
+
color: inkColor(getPalette().dim),
|
|
28584
|
+
wrap: "truncate-end"
|
|
28585
|
+
}, hint)));
|
|
27659
28586
|
}
|
|
27660
28587
|
/** Todo status glyph: web TodoPanel's three-state marker. */
|
|
27661
28588
|
function todoMark(status) {
|
|
@@ -27718,31 +28645,11 @@ function statusToneProps(tone) {
|
|
|
27718
28645
|
bold: void 0,
|
|
27719
28646
|
dimColor: void 0
|
|
27720
28647
|
};
|
|
27721
|
-
case "
|
|
27722
|
-
color: inkColor(getPalette().brandDeep),
|
|
27723
|
-
bold: void 0,
|
|
27724
|
-
dimColor: void 0
|
|
27725
|
-
};
|
|
27726
|
-
case "ctxPrompt": return {
|
|
28648
|
+
case "ctxFill": return {
|
|
27727
28649
|
color: inkColor(getPalette().brand),
|
|
27728
28650
|
bold: void 0,
|
|
27729
28651
|
dimColor: void 0
|
|
27730
28652
|
};
|
|
27731
|
-
case "ctxAssistant": return {
|
|
27732
|
-
color: inkColor(getPalette().brandMid),
|
|
27733
|
-
bold: void 0,
|
|
27734
|
-
dimColor: void 0
|
|
27735
|
-
};
|
|
27736
|
-
case "ctxThinking": return {
|
|
27737
|
-
color: inkColor(getPalette().brandBright),
|
|
27738
|
-
bold: void 0,
|
|
27739
|
-
dimColor: void 0
|
|
27740
|
-
};
|
|
27741
|
-
case "ctxTools": return {
|
|
27742
|
-
color: inkColor(getPalette().code),
|
|
27743
|
-
bold: void 0,
|
|
27744
|
-
dimColor: void 0
|
|
27745
|
-
};
|
|
27746
28653
|
case "success": return {
|
|
27747
28654
|
color: inkColor(getPalette().code),
|
|
27748
28655
|
bold: true,
|
|
@@ -27805,7 +28712,7 @@ function StatusLine({ facts, stats, busy, columns, items }) {
|
|
|
27805
28712
|
busy,
|
|
27806
28713
|
items
|
|
27807
28714
|
});
|
|
27808
|
-
const renderRow = (row, key) => {
|
|
28715
|
+
const renderRow = (row, key, indent = 0) => {
|
|
27809
28716
|
const leftParts = [];
|
|
27810
28717
|
row.left.forEach((group, groupIndex) => {
|
|
27811
28718
|
if (groupIndex > 0) leftParts.push((0, import_react.createElement)(Text, {
|
|
@@ -27837,12 +28744,12 @@ function StatusLine({ facts, stats, busy, columns, items }) {
|
|
|
27837
28744
|
color: inkColor(getPalette().dim)
|
|
27838
28745
|
}, STATUS_CYCLE_HINT));
|
|
27839
28746
|
return (0, import_react.createElement)(Box, {
|
|
27840
|
-
paddingLeft: 2,
|
|
28747
|
+
paddingLeft: 2 + indent,
|
|
27841
28748
|
justifyContent: rightParts.length > 0 ? "space-between" : void 0
|
|
27842
28749
|
}, (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...leftParts), rightParts.length > 0 ? (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...rightParts) : void 0);
|
|
27843
28750
|
};
|
|
27844
28751
|
const row2Present = layout.row2.left.length > 0;
|
|
27845
|
-
return (0, import_react.createElement)(Box, { flexDirection: "column" }, renderRow(layout.row1, "s1"), row2Present ? renderRow(layout.row2, "s2") : void 0);
|
|
28752
|
+
return (0, import_react.createElement)(Box, { flexDirection: "column" }, renderRow(layout.row1, "s1"), row2Present ? renderRow(layout.row2, "s2", 2) : void 0);
|
|
27846
28753
|
}
|
|
27847
28754
|
/**
|
|
27848
28755
|
* One fixed-height local feedback row. Errors remain visible while a slash
|
|
@@ -28149,22 +29056,132 @@ function QuestionBar({ store, snapshot, locked }) {
|
|
|
28149
29056
|
width: viewport.outerColumns,
|
|
28150
29057
|
paddingX: 1,
|
|
28151
29058
|
borderStyle: "round",
|
|
28152
|
-
borderColor: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep)
|
|
29059
|
+
borderColor: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep)
|
|
29060
|
+
}, (0, import_react.createElement)(Text, {
|
|
29061
|
+
color: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep),
|
|
29062
|
+
bold: true,
|
|
29063
|
+
wrap: "truncate-end"
|
|
29064
|
+
}, truncateColumns(`${isPlan ? "📋 plan review" : "❓ question"} ${index + 1}/${pending.request.questions.length} · lines ${rendered.lines.length === 0 ? 0 : visibleScroll + 1}-${Math.min(rendered.lines.length, visibleScroll + viewport.bodyRows)}/${rendered.lines.length}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(StyledRows, { lines: rendered.lines.slice(visibleScroll, visibleScroll + viewport.bodyRows) }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
|
|
29065
|
+
dimColor: true,
|
|
29066
|
+
wrap: "truncate-end"
|
|
29067
|
+
}, dim(truncateColumns(footer, viewport.contentColumns))));
|
|
29068
|
+
}
|
|
29069
|
+
/** The /model panel: a scrolling list over the advisory model directory. */
|
|
29070
|
+
function ModelPanel({ directory, error, onSelect, onProviders, onRetry, onClose }) {
|
|
29071
|
+
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
29072
|
+
const stdout = useStdout().stdout;
|
|
29073
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
29074
|
+
const rows = directory?.rows ?? [];
|
|
29075
|
+
(0, import_react.useEffect)(() => {
|
|
29076
|
+
if (rows.length === 0) {
|
|
29077
|
+
if (cursor !== 0) setCursor(0);
|
|
29078
|
+
return;
|
|
29079
|
+
}
|
|
29080
|
+
if (cursor >= rows.length) setCursor(rows.length - 1);
|
|
29081
|
+
}, [rows.length, cursor]);
|
|
29082
|
+
useInput((input, key) => {
|
|
29083
|
+
if (key.escape || input === "q") {
|
|
29084
|
+
onClose();
|
|
29085
|
+
return;
|
|
29086
|
+
}
|
|
29087
|
+
if (input === "r") {
|
|
29088
|
+
onRetry();
|
|
29089
|
+
return;
|
|
29090
|
+
}
|
|
29091
|
+
if (input === "a" && onProviders !== void 0) {
|
|
29092
|
+
onProviders();
|
|
29093
|
+
return;
|
|
29094
|
+
}
|
|
29095
|
+
if (rows.length === 0) return;
|
|
29096
|
+
if (key.upArrow) {
|
|
29097
|
+
setCursor(cursor > 0 ? cursor - 1 : rows.length - 1);
|
|
29098
|
+
return;
|
|
29099
|
+
}
|
|
29100
|
+
if (key.downArrow) {
|
|
29101
|
+
setCursor(cursor < rows.length - 1 ? cursor + 1 : 0);
|
|
29102
|
+
return;
|
|
29103
|
+
}
|
|
29104
|
+
if (key.pageUp) {
|
|
29105
|
+
setCursor((current) => Math.max(0, current - Math.max(1, viewport.bodyRows - 1)));
|
|
29106
|
+
return;
|
|
29107
|
+
}
|
|
29108
|
+
if (key.pageDown) {
|
|
29109
|
+
setCursor((current) => Math.min(rows.length - 1, current + Math.max(1, viewport.bodyRows - 1)));
|
|
29110
|
+
return;
|
|
29111
|
+
}
|
|
29112
|
+
if (input === "g") {
|
|
29113
|
+
setCursor(0);
|
|
29114
|
+
return;
|
|
29115
|
+
}
|
|
29116
|
+
if (input === "G") {
|
|
29117
|
+
setCursor(rows.length - 1);
|
|
29118
|
+
return;
|
|
29119
|
+
}
|
|
29120
|
+
if (key.return && rows[cursor] !== void 0) onSelect(rows[cursor]);
|
|
29121
|
+
});
|
|
29122
|
+
if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
|
|
29123
|
+
if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`/model${onProviders === void 0 ? "" : " · a providers"} · r retry · esc/q close`, viewport.contentColumns));
|
|
29124
|
+
const visibleStateRows = (directory === void 0 && error === void 0 ? [(0, import_react.createElement)(Text, {
|
|
29125
|
+
key: "loading",
|
|
29126
|
+
dimColor: true,
|
|
29127
|
+
wrap: "truncate-end"
|
|
29128
|
+
}, " loading models…")] : error !== void 0 ? [(0, import_react.createElement)(Text, {
|
|
29129
|
+
key: "error",
|
|
29130
|
+
color: inkColor(getPalette().error),
|
|
29131
|
+
wrap: "truncate-end"
|
|
29132
|
+
}, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns))] : [...directory?.failures.length === 0 ? [] : [(0, import_react.createElement)(Text, {
|
|
29133
|
+
key: "failures",
|
|
29134
|
+
color: inkColor(getPalette().warn),
|
|
29135
|
+
wrap: "truncate-end"
|
|
29136
|
+
}, truncateColumns(` unavailable providers: ${directory?.failures.join(", ")}`, viewport.contentColumns))], ...rows.length === 0 ? [(0, import_react.createElement)(Text, {
|
|
29137
|
+
key: "empty",
|
|
29138
|
+
dimColor: true,
|
|
29139
|
+
wrap: "truncate-end"
|
|
29140
|
+
}, " no models available")] : []]).slice(0, viewport.bodyRows);
|
|
29141
|
+
const rowBudget = Math.max(0, viewport.bodyRows - visibleStateRows.length);
|
|
29142
|
+
const first = selectionWindow(cursor, rows.length, rowBudget);
|
|
29143
|
+
const visible = rowBudget === 0 ? [] : rows.slice(first, first + rowBudget);
|
|
29144
|
+
return (0, import_react.createElement)(Box, {
|
|
29145
|
+
flexDirection: "column",
|
|
29146
|
+
width: viewport.outerColumns,
|
|
29147
|
+
paddingX: 1,
|
|
29148
|
+
borderStyle: "round",
|
|
29149
|
+
borderColor: inkColor(getPalette().brand)
|
|
28153
29150
|
}, (0, import_react.createElement)(Text, {
|
|
28154
|
-
color: inkColor(
|
|
29151
|
+
color: inkColor(getPalette().brand),
|
|
28155
29152
|
bold: true,
|
|
28156
29153
|
wrap: "truncate-end"
|
|
28157
|
-
}, truncateColumns(
|
|
29154
|
+
}, truncateColumns(`/model — select model${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...visibleStateRows, ...visible.map((row) => {
|
|
29155
|
+
const index = rows.indexOf(row);
|
|
29156
|
+
const label = displayText(`${row.providerName} · ${row.modelName}`);
|
|
29157
|
+
return (0, import_react.createElement)(Text, {
|
|
29158
|
+
key: `${row.provider}/${row.model}`,
|
|
29159
|
+
color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
|
|
29160
|
+
wrap: "truncate-end"
|
|
29161
|
+
}, truncateColumns(`${index === cursor ? "❯ " : " "}${label}`, viewport.contentColumns));
|
|
29162
|
+
}), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
|
|
28158
29163
|
dimColor: true,
|
|
28159
29164
|
wrap: "truncate-end"
|
|
28160
|
-
}, dim(truncateColumns(
|
|
29165
|
+
}, dim(truncateColumns(`↑↓ move · pgup/pgdn page · enter select${onProviders === void 0 ? "" : " · a providers"} · r retry · esc/q close`, viewport.contentColumns))));
|
|
28161
29166
|
}
|
|
28162
|
-
/**
|
|
28163
|
-
function
|
|
28164
|
-
const
|
|
29167
|
+
/** Compact provider-state copy; only value-free credential facts cross this boundary. */
|
|
29168
|
+
function providerStateLabel(row) {
|
|
29169
|
+
const route = row.active ? "active" : "dormant";
|
|
29170
|
+
const credential = row.credential;
|
|
29171
|
+
if (credential?.kind === "error") return `${route} · key status unavailable`;
|
|
29172
|
+
if (credential?.kind === "facts") {
|
|
29173
|
+
if (!credential.configured) return `${route} · key missing`;
|
|
29174
|
+
return `${route} · key ${credential.source === void 0 ? "configured" : singleLineText(credential.source)}${credential.writable ? "" : " · read-only"}`;
|
|
29175
|
+
}
|
|
29176
|
+
return `${route} · ${row.configured ? "provider auth" : "not configured"}`;
|
|
29177
|
+
}
|
|
29178
|
+
/** The provider-management stage reached from /model with `a`. */
|
|
29179
|
+
function ProviderPanel({ directory, error, onCredential, onUnset, onRemove, onRetry, onBack }) {
|
|
28165
29180
|
const stdout = useStdout().stdout;
|
|
28166
29181
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
28167
29182
|
const rows = directory?.rows ?? [];
|
|
29183
|
+
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
29184
|
+
const [actionError, setActionError] = (0, import_react.useState)(void 0);
|
|
28168
29185
|
(0, import_react.useEffect)(() => {
|
|
28169
29186
|
if (rows.length === 0) {
|
|
28170
29187
|
if (cursor !== 0) setCursor(0);
|
|
@@ -28172,62 +29189,87 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }) {
|
|
|
28172
29189
|
}
|
|
28173
29190
|
if (cursor >= rows.length) setCursor(rows.length - 1);
|
|
28174
29191
|
}, [rows.length, cursor]);
|
|
28175
|
-
|
|
29192
|
+
useStableInput((input, key) => {
|
|
28176
29193
|
if (key.escape || input === "q") {
|
|
28177
|
-
|
|
29194
|
+
onBack();
|
|
28178
29195
|
return;
|
|
28179
29196
|
}
|
|
28180
29197
|
if (input === "r") {
|
|
29198
|
+
setActionError(void 0);
|
|
28181
29199
|
onRetry();
|
|
28182
29200
|
return;
|
|
28183
29201
|
}
|
|
28184
29202
|
if (rows.length === 0) return;
|
|
28185
29203
|
if (key.upArrow) {
|
|
29204
|
+
setActionError(void 0);
|
|
28186
29205
|
setCursor(cursor > 0 ? cursor - 1 : rows.length - 1);
|
|
28187
29206
|
return;
|
|
28188
29207
|
}
|
|
28189
29208
|
if (key.downArrow) {
|
|
29209
|
+
setActionError(void 0);
|
|
28190
29210
|
setCursor(cursor < rows.length - 1 ? cursor + 1 : 0);
|
|
28191
29211
|
return;
|
|
28192
29212
|
}
|
|
28193
29213
|
if (key.pageUp) {
|
|
29214
|
+
setActionError(void 0);
|
|
28194
29215
|
setCursor((current) => Math.max(0, current - Math.max(1, viewport.bodyRows - 1)));
|
|
28195
29216
|
return;
|
|
28196
29217
|
}
|
|
28197
29218
|
if (key.pageDown) {
|
|
29219
|
+
setActionError(void 0);
|
|
28198
29220
|
setCursor((current) => Math.min(rows.length - 1, current + Math.max(1, viewport.bodyRows - 1)));
|
|
28199
29221
|
return;
|
|
28200
29222
|
}
|
|
28201
|
-
|
|
28202
|
-
|
|
29223
|
+
const target = rows[cursor];
|
|
29224
|
+
if (target === void 0) return;
|
|
29225
|
+
if (input === "d") {
|
|
29226
|
+
const facts = target.credential;
|
|
29227
|
+
if (facts?.kind !== "facts" || !facts.configured) setActionError("this provider has no configured API key to remove");
|
|
29228
|
+
else if (!facts.writable) setActionError("this API key is supplied read-only by the environment");
|
|
29229
|
+
else onUnset(target);
|
|
28203
29230
|
return;
|
|
28204
29231
|
}
|
|
28205
|
-
if (input === "
|
|
28206
|
-
|
|
29232
|
+
if (input === "x") {
|
|
29233
|
+
if (!target.removable) setActionError("this provider profile is not removable");
|
|
29234
|
+
else onRemove(target);
|
|
28207
29235
|
return;
|
|
28208
29236
|
}
|
|
28209
|
-
if (key.return
|
|
28210
|
-
|
|
29237
|
+
if (key.return) {
|
|
29238
|
+
if (target.settingsNs.length === 0) setActionError("this provider is not managed by Harness settings");
|
|
29239
|
+
else if (target.credential?.kind === "error") setActionError("credential status is unavailable; retry before writing");
|
|
29240
|
+
else if (target.credential?.kind === "facts" && !target.credential.writable) setActionError("this API key is supplied read-only by the environment");
|
|
29241
|
+
else if (target.credentialRef === void 0 && directory?.writable !== true) setActionError("settings are read-only; this provider cannot be activated here");
|
|
29242
|
+
else onCredential(target);
|
|
29243
|
+
}
|
|
29244
|
+
}, true);
|
|
28211
29245
|
if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
|
|
28212
|
-
if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/model ·
|
|
28213
|
-
const
|
|
29246
|
+
if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/model providers · enter key · d remove key · esc back", viewport.contentColumns));
|
|
29247
|
+
const visibleStateRows = (directory === void 0 && error === void 0 ? [(0, import_react.createElement)(Text, {
|
|
28214
29248
|
key: "loading",
|
|
28215
|
-
|
|
29249
|
+
color: inkColor(getPalette().dim),
|
|
28216
29250
|
wrap: "truncate-end"
|
|
28217
|
-
}, " loading
|
|
29251
|
+
}, " loading providers…")] : error !== void 0 ? [(0, import_react.createElement)(Text, {
|
|
28218
29252
|
key: "error",
|
|
28219
29253
|
color: inkColor(getPalette().error),
|
|
28220
29254
|
wrap: "truncate-end"
|
|
28221
|
-
}, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns))] : [
|
|
28222
|
-
|
|
28223
|
-
|
|
28224
|
-
|
|
28225
|
-
|
|
28226
|
-
|
|
28227
|
-
|
|
28228
|
-
|
|
28229
|
-
|
|
28230
|
-
|
|
29255
|
+
}, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns))] : [
|
|
29256
|
+
...actionError === void 0 ? [] : [(0, import_react.createElement)(Text, {
|
|
29257
|
+
key: "action-error",
|
|
29258
|
+
color: inkColor(getPalette().error),
|
|
29259
|
+
wrap: "truncate-end"
|
|
29260
|
+
}, truncateColumns(` ${actionError}`, viewport.contentColumns))],
|
|
29261
|
+
...(directory?.failures ?? []).map((failure, index) => (0, import_react.createElement)(Text, {
|
|
29262
|
+
key: `failure-${index}`,
|
|
29263
|
+
color: inkColor(getPalette().warn),
|
|
29264
|
+
wrap: "truncate-end"
|
|
29265
|
+
}, truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns))),
|
|
29266
|
+
...rows.length === 0 ? [(0, import_react.createElement)(Text, {
|
|
29267
|
+
key: "empty",
|
|
29268
|
+
color: inkColor(getPalette().dim),
|
|
29269
|
+
wrap: "truncate-end"
|
|
29270
|
+
}, " no configurable providers")] : []
|
|
29271
|
+
]).slice(0, viewport.bodyRows);
|
|
29272
|
+
const rowBudget = Math.max(0, viewport.bodyRows - visibleStateRows.length);
|
|
28231
29273
|
const first = selectionWindow(cursor, rows.length, rowBudget);
|
|
28232
29274
|
const visible = rowBudget === 0 ? [] : rows.slice(first, first + rowBudget);
|
|
28233
29275
|
return (0, import_react.createElement)(Box, {
|
|
@@ -28240,18 +29282,168 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }) {
|
|
|
28240
29282
|
color: inkColor(getPalette().brand),
|
|
28241
29283
|
bold: true,
|
|
28242
29284
|
wrap: "truncate-end"
|
|
28243
|
-
}, truncateColumns(`/model —
|
|
29285
|
+
}, truncateColumns(`/model — providers${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...visibleStateRows, ...visible.map((row) => {
|
|
28244
29286
|
const index = rows.indexOf(row);
|
|
28245
|
-
const label =
|
|
29287
|
+
const label = `${row.displayName === row.provider ? row.provider : `${row.displayName} (${row.provider})`} · ${providerStateLabel(row)}${row.removable ? " · custom" : ""}`;
|
|
28246
29288
|
return (0, import_react.createElement)(Text, {
|
|
28247
|
-
key:
|
|
29289
|
+
key: row.provider,
|
|
28248
29290
|
color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
|
|
28249
29291
|
wrap: "truncate-end"
|
|
28250
|
-
}, truncateColumns(`${index === cursor ? "❯ " : " "}${label}`, viewport.contentColumns));
|
|
29292
|
+
}, truncateColumns(`${index === cursor ? "❯ " : " "}${displayText(label)}`, viewport.contentColumns));
|
|
28251
29293
|
}), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
|
|
28252
|
-
|
|
29294
|
+
color: inkColor(getPalette().dim),
|
|
29295
|
+
wrap: "truncate-end"
|
|
29296
|
+
}, truncateColumns("↑↓ move · enter add/update key · d remove key · x remove custom provider · r retry · esc back", viewport.contentColumns)));
|
|
29297
|
+
}
|
|
29298
|
+
/** Write-only masked API-key editor; the secret lives only in this mounted component. */
|
|
29299
|
+
function ProviderCredentialPanel({ target, save, done, back }) {
|
|
29300
|
+
const stdout = useStdout().stdout;
|
|
29301
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
29302
|
+
const [draft, setDraft] = (0, import_react.useState)("");
|
|
29303
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
29304
|
+
const [error, setError] = (0, import_react.useState)(void 0);
|
|
29305
|
+
const submit = () => {
|
|
29306
|
+
if (busy) return;
|
|
29307
|
+
setBusy(true);
|
|
29308
|
+
setError(void 0);
|
|
29309
|
+
Promise.resolve().then(() => save(target, draft)).then(() => {
|
|
29310
|
+
setDraft("");
|
|
29311
|
+
done();
|
|
29312
|
+
}, (reason) => {
|
|
29313
|
+
setError(singleLineText(reason instanceof Error ? reason.message : String(reason)));
|
|
29314
|
+
setBusy(false);
|
|
29315
|
+
});
|
|
29316
|
+
};
|
|
29317
|
+
useStableInput((input, key) => {
|
|
29318
|
+
if (busy) return;
|
|
29319
|
+
if (key.escape) {
|
|
29320
|
+
setDraft("");
|
|
29321
|
+
back();
|
|
29322
|
+
return;
|
|
29323
|
+
}
|
|
29324
|
+
if (key.return) {
|
|
29325
|
+
submit();
|
|
29326
|
+
return;
|
|
29327
|
+
}
|
|
29328
|
+
if (key.backspace || key.delete) {
|
|
29329
|
+
setError(void 0);
|
|
29330
|
+
setDraft((current) => [...current].slice(0, -1).join(""));
|
|
29331
|
+
return;
|
|
29332
|
+
}
|
|
29333
|
+
if (key.ctrl && input === "u") {
|
|
29334
|
+
setError(void 0);
|
|
29335
|
+
setDraft("");
|
|
29336
|
+
return;
|
|
29337
|
+
}
|
|
29338
|
+
if (key.ctrl || key.meta || input.length === 0) return;
|
|
29339
|
+
const next = draft + input;
|
|
29340
|
+
if (next.length > 4096) {
|
|
29341
|
+
setError("API key input is too long");
|
|
29342
|
+
return;
|
|
29343
|
+
}
|
|
29344
|
+
setError(void 0);
|
|
29345
|
+
setDraft(next);
|
|
29346
|
+
}, true);
|
|
29347
|
+
if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
|
|
29348
|
+
const keyBudget = Math.max(1, viewport.contentColumns - 4);
|
|
29349
|
+
const bullets = "•".repeat(Math.min([...draft].length, keyBudget));
|
|
29350
|
+
if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`API key ${bullets}${busy ? " saving…" : " ▏"} · esc back`, viewport.contentColumns));
|
|
29351
|
+
const identity = target.displayName === target.provider ? target.provider : `${target.displayName} (${target.provider})`;
|
|
29352
|
+
const source = target.credential?.kind === "facts" && target.credential.configured ? `replaces ${singleLineText(target.credential.source ?? "stored key")}` : "new key";
|
|
29353
|
+
const providerRow = (0, import_react.createElement)(Text, {
|
|
29354
|
+
key: "provider",
|
|
29355
|
+
wrap: "truncate-end"
|
|
29356
|
+
}, truncateColumns(` provider ${displayText(identity)}`, viewport.contentColumns));
|
|
29357
|
+
const referenceRow = (0, import_react.createElement)(Text, {
|
|
29358
|
+
key: "reference",
|
|
29359
|
+
color: inkColor(getPalette().dim),
|
|
29360
|
+
wrap: "truncate-end"
|
|
29361
|
+
}, truncateColumns(` reference ${displayText(target.credentialRef ?? target.suggestedRef)} · ${source}`, viewport.contentColumns));
|
|
29362
|
+
const keyRow = (0, import_react.createElement)(Text, {
|
|
29363
|
+
key: "key",
|
|
29364
|
+
color: error === void 0 ? inkColor(getPalette().brandBright) : inkColor(getPalette().error),
|
|
29365
|
+
wrap: "truncate-end"
|
|
29366
|
+
}, truncateColumns(` key ${bullets}${busy ? " saving…" : " ▏"}`, viewport.contentColumns));
|
|
29367
|
+
const errorRow = error === void 0 ? void 0 : (0, import_react.createElement)(Text, {
|
|
29368
|
+
key: "error",
|
|
29369
|
+
color: inkColor(getPalette().error),
|
|
29370
|
+
wrap: "truncate-end"
|
|
29371
|
+
}, truncateColumns(` ${error}`, viewport.contentColumns));
|
|
29372
|
+
const detailRows = errorRow === void 0 ? [providerRow, referenceRow] : [providerRow, errorRow];
|
|
29373
|
+
const primaryRow = viewport.bodyRows === 1 && errorRow !== void 0 ? errorRow : keyRow;
|
|
29374
|
+
const detailBudget = Math.max(0, viewport.bodyRows - 1);
|
|
29375
|
+
const bodyRows = [...detailBudget === 0 ? [] : detailRows.slice(-detailBudget), ...viewport.bodyRows === 0 ? [] : [primaryRow]];
|
|
29376
|
+
return (0, import_react.createElement)(Box, {
|
|
29377
|
+
flexDirection: "column",
|
|
29378
|
+
width: viewport.outerColumns,
|
|
29379
|
+
paddingX: 1,
|
|
29380
|
+
borderStyle: "round",
|
|
29381
|
+
borderColor: inkColor(getPalette().brand)
|
|
29382
|
+
}, (0, import_react.createElement)(Text, {
|
|
29383
|
+
color: inkColor(getPalette().brand),
|
|
29384
|
+
bold: true,
|
|
29385
|
+
wrap: "truncate-end"
|
|
29386
|
+
}, truncateColumns("/model — add API key", viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...bodyRows, (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
|
|
29387
|
+
color: inkColor(getPalette().dim),
|
|
29388
|
+
wrap: "truncate-end"
|
|
29389
|
+
}, truncateColumns("type or paste key · enter save · ctrl+u clear · esc back", viewport.contentColumns)));
|
|
29390
|
+
}
|
|
29391
|
+
/** Bounded destructive-action confirmation for credential or provider removal. */
|
|
29392
|
+
function ProviderConfirmPanel({ target, kind, confirm, done, back }) {
|
|
29393
|
+
const stdout = useStdout().stdout;
|
|
29394
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
29395
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
29396
|
+
const [error, setError] = (0, import_react.useState)(void 0);
|
|
29397
|
+
const run = () => {
|
|
29398
|
+
if (busy) return;
|
|
29399
|
+
setBusy(true);
|
|
29400
|
+
setError(void 0);
|
|
29401
|
+
Promise.resolve().then(() => confirm(target)).then(done, (reason) => {
|
|
29402
|
+
setError(singleLineText(reason instanceof Error ? reason.message : String(reason)));
|
|
29403
|
+
setBusy(false);
|
|
29404
|
+
});
|
|
29405
|
+
};
|
|
29406
|
+
useStableInput((input, key) => {
|
|
29407
|
+
if (busy) return;
|
|
29408
|
+
if (key.escape || input === "n") {
|
|
29409
|
+
back();
|
|
29410
|
+
return;
|
|
29411
|
+
}
|
|
29412
|
+
if (input === "y") run();
|
|
29413
|
+
}, true);
|
|
29414
|
+
if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
|
|
29415
|
+
const action = kind === "credential" ? "remove API key" : "remove provider";
|
|
29416
|
+
if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`${action} ${target.displayName}? · y confirm · n/esc back`, viewport.contentColumns));
|
|
29417
|
+
const identity = target.displayName === target.provider ? target.provider : `${target.displayName} (${target.provider})`;
|
|
29418
|
+
const identityRow = (0, import_react.createElement)(Text, {
|
|
29419
|
+
key: "identity",
|
|
29420
|
+
wrap: "truncate-end"
|
|
29421
|
+
}, truncateColumns(` ${displayText(identity)}`, viewport.contentColumns));
|
|
29422
|
+
const descriptionRow = (0, import_react.createElement)(Text, {
|
|
29423
|
+
key: "description",
|
|
29424
|
+
color: inkColor(getPalette().dim),
|
|
28253
29425
|
wrap: "truncate-end"
|
|
28254
|
-
},
|
|
29426
|
+
}, truncateColumns(kind === "credential" ? " the provider profile and selected model stay available" : " the user settings profile and its managed key will be removed", viewport.contentColumns));
|
|
29427
|
+
const errorRow = error === void 0 ? void 0 : (0, import_react.createElement)(Text, {
|
|
29428
|
+
key: "error",
|
|
29429
|
+
color: inkColor(getPalette().error),
|
|
29430
|
+
wrap: "truncate-end"
|
|
29431
|
+
}, truncateColumns(` ${error}`, viewport.contentColumns));
|
|
29432
|
+
const bodyRows = errorRow === void 0 ? [identityRow, descriptionRow].slice(0, viewport.bodyRows) : [identityRow, errorRow].slice(-viewport.bodyRows);
|
|
29433
|
+
return (0, import_react.createElement)(Box, {
|
|
29434
|
+
flexDirection: "column",
|
|
29435
|
+
width: viewport.outerColumns,
|
|
29436
|
+
paddingX: 1,
|
|
29437
|
+
borderStyle: "round",
|
|
29438
|
+
borderColor: inkColor(getPalette().warn)
|
|
29439
|
+
}, (0, import_react.createElement)(Text, {
|
|
29440
|
+
color: inkColor(getPalette().warn),
|
|
29441
|
+
bold: true,
|
|
29442
|
+
wrap: "truncate-end"
|
|
29443
|
+
}, truncateColumns(`/model — ${action}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...bodyRows, (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
|
|
29444
|
+
color: inkColor(getPalette().dim),
|
|
29445
|
+
wrap: "truncate-end"
|
|
29446
|
+
}, truncateColumns(busy ? "working…" : "y confirm · n/esc back", viewport.contentColumns)));
|
|
28255
29447
|
}
|
|
28256
29448
|
/**
|
|
28257
29449
|
* The /help overlay: one scrolling card with the keyboard map, the TUI-local
|
|
@@ -28319,6 +29511,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
|
|
|
28319
29511
|
(0, import_react.createElement)(Box, { key: "local-model" }, row("/model", "switch the model")),
|
|
28320
29512
|
(0, import_react.createElement)(Box, { key: "local-effort" }, row("/effort", "adjust reasoning effort for the current model")),
|
|
28321
29513
|
(0, import_react.createElement)(Box, { key: "local-mode" }, row("/mode", "inspect or select the agent preset (/mode [preset])")),
|
|
29514
|
+
(0, import_react.createElement)(Box, { key: "local-permission" }, row("/permission", "inspect or select the permission preset (/permission [preset])")),
|
|
28322
29515
|
(0, import_react.createElement)(Box, { key: "local-new" }, row("/new", "create and switch to a fresh session (/new [preset])")),
|
|
28323
29516
|
(0, import_react.createElement)(Box, { key: "local-resume" }, row("/resume", "browse or switch root sessions (/resume [id|prefix])")),
|
|
28324
29517
|
(0, import_react.createElement)(Box, { key: "local-plugin" }, row("/plugin", "inspect the live plugin composition")),
|
|
@@ -28591,6 +29784,11 @@ function completionCandidates(value, descriptors, skills) {
|
|
|
28591
29784
|
description: "select the agent preset",
|
|
28592
29785
|
origin: "command"
|
|
28593
29786
|
},
|
|
29787
|
+
{
|
|
29788
|
+
label: "/permission",
|
|
29789
|
+
description: "inspect or select the permission preset",
|
|
29790
|
+
origin: "command"
|
|
29791
|
+
},
|
|
28594
29792
|
{
|
|
28595
29793
|
label: "/new",
|
|
28596
29794
|
description: "start a fresh session",
|
|
@@ -28722,7 +29920,7 @@ function CompletionMenu({ active, mention, index, rows }) {
|
|
|
28722
29920
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
28723
29921
|
* box passes every key through untouched.
|
|
28724
29922
|
*/
|
|
28725
|
-
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openResume, openPlugin, openStatusline, openTheme, openHistory, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed,
|
|
29923
|
+
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openStatusline, openTheme, openHistory, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle }) {
|
|
28726
29924
|
const columns = useStdout().stdout?.columns ?? 80;
|
|
28727
29925
|
const [value, setValue] = (0, import_react.useState)("");
|
|
28728
29926
|
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
@@ -28922,6 +30120,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
28922
30120
|
openEffort();
|
|
28923
30121
|
return;
|
|
28924
30122
|
}
|
|
30123
|
+
if (text === "/permission") {
|
|
30124
|
+
openPermission();
|
|
30125
|
+
return;
|
|
30126
|
+
}
|
|
30127
|
+
if (text.startsWith("/permission ")) {
|
|
30128
|
+
dispatch(text);
|
|
30129
|
+
return;
|
|
30130
|
+
}
|
|
28925
30131
|
if (text === "/mode" || text.startsWith("/mode ")) {
|
|
28926
30132
|
if (text.slice(5).trim() === "") openMode();
|
|
28927
30133
|
else dispatch(text);
|
|
@@ -29066,6 +30272,40 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
29066
30272
|
setDismissedMenuValue(void 0);
|
|
29067
30273
|
}
|
|
29068
30274
|
});
|
|
30275
|
+
const [waveTick, setWaveTick] = (0, import_react.useState)(null);
|
|
30276
|
+
const wavePrevious = (0, import_react.useRef)({
|
|
30277
|
+
tier: null,
|
|
30278
|
+
style: null
|
|
30279
|
+
});
|
|
30280
|
+
(0, import_react.useEffect)(() => {
|
|
30281
|
+
const previous = wavePrevious.current;
|
|
30282
|
+
wavePrevious.current = {
|
|
30283
|
+
tier: waveTier,
|
|
30284
|
+
style: waveStyle
|
|
30285
|
+
};
|
|
30286
|
+
if (waveTier === null) {
|
|
30287
|
+
setWaveTick(null);
|
|
30288
|
+
return;
|
|
30289
|
+
}
|
|
30290
|
+
if (previous.tier !== waveTier || previous.style !== waveStyle) setWaveTick(0);
|
|
30291
|
+
}, [waveTier, waveStyle]);
|
|
30292
|
+
const waveActive = waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
|
|
30293
|
+
(0, import_react.useEffect)(() => {
|
|
30294
|
+
if (!waveActive) return;
|
|
30295
|
+
const id = setInterval(() => {
|
|
30296
|
+
setWaveTick((current) => current === null ? 0 : current + 1);
|
|
30297
|
+
}, 33);
|
|
30298
|
+
return () => {
|
|
30299
|
+
clearInterval(id);
|
|
30300
|
+
};
|
|
30301
|
+
}, [waveActive]);
|
|
30302
|
+
(0, import_react.useEffect)(() => {
|
|
30303
|
+
if (waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 >= deepseekWaveDuration(waveTier, waveStyle)) setWaveTick(null);
|
|
30304
|
+
}, [
|
|
30305
|
+
waveTick,
|
|
30306
|
+
waveTier,
|
|
30307
|
+
waveStyle
|
|
30308
|
+
]);
|
|
29069
30309
|
const tierActive = waveTier !== null;
|
|
29070
30310
|
const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier);
|
|
29071
30311
|
const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
|
|
@@ -29179,13 +30419,156 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
29179
30419
|
};
|
|
29180
30420
|
return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && !busy ? waveRow() : frame(staticRow));
|
|
29181
30421
|
}
|
|
30422
|
+
/** Build one settled row (row Box plus its roomy-prompt spacers). */
|
|
30423
|
+
function buildSettledRow(entry, index, showReasoning) {
|
|
30424
|
+
const row = (0, import_react.createElement)(EntryLine, {
|
|
30425
|
+
entry,
|
|
30426
|
+
showReasoning,
|
|
30427
|
+
verbose: false
|
|
30428
|
+
});
|
|
30429
|
+
const roomyPrompt = entry.kind === "user" && !entry.notice;
|
|
30430
|
+
return {
|
|
30431
|
+
box: (0, import_react.createElement)(Box, {
|
|
30432
|
+
key: index,
|
|
30433
|
+
paddingX: 1
|
|
30434
|
+
}, row),
|
|
30435
|
+
before: roomyPrompt ? (0, import_react.createElement)(Box, {
|
|
30436
|
+
key: `prompt-before-${index}`,
|
|
30437
|
+
paddingX: 1
|
|
30438
|
+
}, (0, import_react.createElement)(Text, null, " ")) : void 0,
|
|
30439
|
+
after: roomyPrompt ? (0, import_react.createElement)(Box, {
|
|
30440
|
+
key: `prompt-after-${index}`,
|
|
30441
|
+
paddingX: 1
|
|
30442
|
+
}, (0, import_react.createElement)(Text, null, " ")) : void 0,
|
|
30443
|
+
reasonSensitive: entry.kind === "assistant" && entry.reasoning !== "",
|
|
30444
|
+
showReasoning
|
|
30445
|
+
};
|
|
30446
|
+
}
|
|
30447
|
+
/**
|
|
30448
|
+
* The settled `<Static>` row set as a PURE incremental state machine (App
|
|
30449
|
+
* drives it from the memo; tests drive it directly and read `built`).
|
|
30450
|
+
*
|
|
30451
|
+
* The settled prefix is permanently final: the projection only APPENDS below
|
|
30452
|
+
* the flush boundary, removes pending rows at or beyond it, and replaces
|
|
30453
|
+
* running tool/retry/command rows there too. So extending the cache never
|
|
30454
|
+
* rescans the old prefix — a grown boundary builds ONLY the newly settled
|
|
30455
|
+
* suffix and reuses every cached element, letting React bail out of unchanged
|
|
30456
|
+
* rows and keeping long histories out of the per-durable-event path (no O(N)
|
|
30457
|
+
* rebuild of rows, Map, or MarkdownBody parses). `records` is mutated in place
|
|
30458
|
+
* on the append/toggle paths to stay O(delta).
|
|
30459
|
+
*
|
|
30460
|
+
* Full rebuilds run only on the rare, deliberate paths: no cache yet, a
|
|
30461
|
+
* source-backed replay (`epoch` bump: resize / Ctrl+L / Ctrl+R remounts
|
|
30462
|
+
* `<Static>` and must re-flush the CURRENT rows), a `resumed` change, or a shrink
|
|
30463
|
+
* (`store.reset`). A reasoning toggle rebuilds only the rows whose text
|
|
30464
|
+
* depends on it, preserving the other rows' element identity.
|
|
30465
|
+
*/
|
|
30466
|
+
function computeSettledRows(previous, entries, settled, showReasoning, resumed, epoch) {
|
|
30467
|
+
if (previous === void 0 || previous.epoch !== epoch || previous.resumed !== resumed || settled < previous.entries.length) {
|
|
30468
|
+
const records = /* @__PURE__ */ new Map();
|
|
30469
|
+
const flat = [(0, import_react.createElement)(Header, {
|
|
30470
|
+
key: "header",
|
|
30471
|
+
resumed
|
|
30472
|
+
})];
|
|
30473
|
+
for (let index = 0; index < settled; index++) {
|
|
30474
|
+
const entry = entries[index];
|
|
30475
|
+
const record = buildSettledRow(entry, index, showReasoning);
|
|
30476
|
+
records.set(entry, record);
|
|
30477
|
+
if (record.before !== void 0) flat.push(record.before);
|
|
30478
|
+
flat.push(record.box);
|
|
30479
|
+
if (record.after !== void 0) flat.push(record.after);
|
|
30480
|
+
}
|
|
30481
|
+
return {
|
|
30482
|
+
cache: {
|
|
30483
|
+
entries: entries.slice(0, settled),
|
|
30484
|
+
records,
|
|
30485
|
+
header: flat[0],
|
|
30486
|
+
resumed,
|
|
30487
|
+
showReasoning,
|
|
30488
|
+
epoch,
|
|
30489
|
+
flat
|
|
30490
|
+
},
|
|
30491
|
+
built: settled
|
|
30492
|
+
};
|
|
30493
|
+
}
|
|
30494
|
+
if (previous.showReasoning !== showReasoning) {
|
|
30495
|
+
const records = previous.records;
|
|
30496
|
+
const flat = [previous.header];
|
|
30497
|
+
let built = 0;
|
|
30498
|
+
for (let index = 0; index < previous.entries.length; index++) {
|
|
30499
|
+
const entry = previous.entries[index];
|
|
30500
|
+
const record = records.get(entry);
|
|
30501
|
+
const current = record.reasonSensitive ? {
|
|
30502
|
+
...record,
|
|
30503
|
+
box: (0, import_react.createElement)(Box, {
|
|
30504
|
+
key: index,
|
|
30505
|
+
paddingX: 1
|
|
30506
|
+
}, (0, import_react.createElement)(EntryLine, {
|
|
30507
|
+
entry,
|
|
30508
|
+
showReasoning,
|
|
30509
|
+
verbose: false
|
|
30510
|
+
})),
|
|
30511
|
+
showReasoning
|
|
30512
|
+
} : record;
|
|
30513
|
+
if (current !== record) {
|
|
30514
|
+
records.set(entry, current);
|
|
30515
|
+
built += 1;
|
|
30516
|
+
}
|
|
30517
|
+
if (current.before !== void 0) flat.push(current.before);
|
|
30518
|
+
flat.push(current.box);
|
|
30519
|
+
if (current.after !== void 0) flat.push(current.after);
|
|
30520
|
+
}
|
|
30521
|
+
return {
|
|
30522
|
+
cache: {
|
|
30523
|
+
...previous,
|
|
30524
|
+
records,
|
|
30525
|
+
showReasoning,
|
|
30526
|
+
flat
|
|
30527
|
+
},
|
|
30528
|
+
built
|
|
30529
|
+
};
|
|
30530
|
+
}
|
|
30531
|
+
if (settled === previous.entries.length) return {
|
|
30532
|
+
cache: previous,
|
|
30533
|
+
built: 0
|
|
30534
|
+
};
|
|
30535
|
+
const records = previous.records;
|
|
30536
|
+
const suffix = [];
|
|
30537
|
+
const added = [];
|
|
30538
|
+
for (let index = previous.entries.length; index < settled; index++) {
|
|
30539
|
+
const entry = entries[index];
|
|
30540
|
+
const record = buildSettledRow(entry, index, showReasoning);
|
|
30541
|
+
records.set(entry, record);
|
|
30542
|
+
suffix.push(entry);
|
|
30543
|
+
if (record.before !== void 0) added.push(record.before);
|
|
30544
|
+
added.push(record.box);
|
|
30545
|
+
if (record.after !== void 0) added.push(record.after);
|
|
30546
|
+
}
|
|
30547
|
+
return {
|
|
30548
|
+
cache: {
|
|
30549
|
+
entries: previous.entries.concat(suffix),
|
|
30550
|
+
records,
|
|
30551
|
+
header: previous.header,
|
|
30552
|
+
resumed: previous.resumed,
|
|
30553
|
+
showReasoning,
|
|
30554
|
+
epoch: previous.epoch,
|
|
30555
|
+
flat: previous.flat.concat(added)
|
|
30556
|
+
},
|
|
30557
|
+
built: settled - previous.entries.length
|
|
30558
|
+
};
|
|
30559
|
+
}
|
|
29182
30560
|
/** The whole terminal app; state arrives via the store, output via Ink. */
|
|
29183
30561
|
function App(props) {
|
|
29184
30562
|
const view = (0, import_react.useSyncExternalStore)(props.store.subscribe, props.store.getView);
|
|
29185
|
-
const
|
|
29186
|
-
const
|
|
30563
|
+
const readDescriptors = (0, import_react.useCallback)(() => props.commands.descriptors, [props.commands]);
|
|
30564
|
+
const readSkills = (0, import_react.useCallback)(() => props.skills.rows, [props.skills]);
|
|
30565
|
+
const descriptors = (0, import_react.useSyncExternalStore)(props.commands.subscribe, readDescriptors);
|
|
30566
|
+
const skills = (0, import_react.useSyncExternalStore)(props.skills.subscribe, readSkills);
|
|
29187
30567
|
const [modelLabel, setModelLabel] = (0, import_react.useState)(props.model);
|
|
29188
30568
|
const [modelOpen, setModelOpen] = (0, import_react.useState)(false);
|
|
30569
|
+
/** Nested /model stages; only one owns terminal input at a time. */
|
|
30570
|
+
const [providerOpen, setProviderOpen] = (0, import_react.useState)(false);
|
|
30571
|
+
const [providerAction, setProviderAction] = (0, import_react.useState)(void 0);
|
|
29189
30572
|
/** The model row whose effort levels the /model stage lists; undefined shows the model list. */
|
|
29190
30573
|
const [effortFor, setEffortFor] = (0, import_react.useState)(void 0);
|
|
29191
30574
|
/** Effective reasoning effort, shown in the /model picker and switch notice. */
|
|
@@ -29196,8 +30579,10 @@ function App(props) {
|
|
|
29196
30579
|
* per-style durations), then the band returns to static while the prompt
|
|
29197
30580
|
* marker keeps the tier accent. The trigger follows the applied model
|
|
29198
30581
|
* label (what the status bar actually shows), never the initial paint,
|
|
29199
|
-
* and the tier is derived from the label and cached at the switch.
|
|
29200
|
-
|
|
30582
|
+
* and the tier is derived from the label and cached at the switch. The
|
|
30583
|
+
* 33ms tick itself lives inside Input, so the sweep re-renders only the
|
|
30584
|
+
* composer row, not the whole tree, at 30fps; App owns the rarely-changing
|
|
30585
|
+
* tier/style and Input starts the sweep whenever that pair changes. */
|
|
29201
30586
|
const [waveTier, setWaveTier] = (0, import_react.useState)(null);
|
|
29202
30587
|
const [waveStyle, setWaveStyle] = (0, import_react.useState)(null);
|
|
29203
30588
|
const previousModel = (0, import_react.useRef)(void 0);
|
|
@@ -29212,7 +30597,6 @@ function App(props) {
|
|
|
29212
30597
|
if (!isOfficialDeepSeekLabel(modelLabel)) {
|
|
29213
30598
|
setWaveTier(null);
|
|
29214
30599
|
setWaveStyle(null);
|
|
29215
|
-
setWaveTick(null);
|
|
29216
30600
|
return;
|
|
29217
30601
|
}
|
|
29218
30602
|
if (modelChanged || effortChanged) {
|
|
@@ -29220,28 +30604,12 @@ function App(props) {
|
|
|
29220
30604
|
const nextStyle = deepseekWaveStyleRandom(previousStyle.current);
|
|
29221
30605
|
previousStyle.current = nextStyle;
|
|
29222
30606
|
setWaveStyle(nextStyle);
|
|
29223
|
-
setWaveTick(0);
|
|
29224
30607
|
}
|
|
29225
30608
|
}, [modelLabel, effortLabel]);
|
|
29226
|
-
const waveActive = waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
|
|
29227
|
-
(0, import_react.useEffect)(() => {
|
|
29228
|
-
if (!waveActive) return;
|
|
29229
|
-
const id = setInterval(() => {
|
|
29230
|
-
setWaveTick((current) => current === null ? 0 : current + 1);
|
|
29231
|
-
}, 33);
|
|
29232
|
-
return () => {
|
|
29233
|
-
clearInterval(id);
|
|
29234
|
-
};
|
|
29235
|
-
}, [waveActive]);
|
|
29236
|
-
(0, import_react.useEffect)(() => {
|
|
29237
|
-
if (waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 >= deepseekWaveDuration(waveTier, waveStyle)) setWaveTick(null);
|
|
29238
|
-
}, [
|
|
29239
|
-
waveTick,
|
|
29240
|
-
waveTier,
|
|
29241
|
-
waveStyle
|
|
29242
|
-
]);
|
|
29243
30609
|
const [directory, setDirectory] = (0, import_react.useState)(void 0);
|
|
29244
30610
|
const [modelError, setModelError] = (0, import_react.useState)(void 0);
|
|
30611
|
+
const [providerDirectory, setProviderDirectory] = (0, import_react.useState)(void 0);
|
|
30612
|
+
const [providerError, setProviderError] = (0, import_react.useState)(void 0);
|
|
29245
30613
|
const [modelLoadEpoch, setModelLoadEpoch] = (0, import_react.useState)(0);
|
|
29246
30614
|
const [notice, setNotice] = (0, import_react.useState)(void 0);
|
|
29247
30615
|
const notify = (0, import_react.useCallback)((text, tone = "info") => {
|
|
@@ -29271,11 +30639,39 @@ function App(props) {
|
|
|
29271
30639
|
modelLoadEpoch,
|
|
29272
30640
|
props.loadModels
|
|
29273
30641
|
]);
|
|
30642
|
+
(0, import_react.useEffect)(() => {
|
|
30643
|
+
if (!modelOpen || props.loadModelProviders === void 0) return;
|
|
30644
|
+
let cancelled = false;
|
|
30645
|
+
setProviderDirectory(void 0);
|
|
30646
|
+
setProviderError(void 0);
|
|
30647
|
+
Promise.resolve().then(() => props.loadModelProviders()).then((loaded) => {
|
|
30648
|
+
if (!cancelled) setProviderDirectory(loaded);
|
|
30649
|
+
}, (error) => {
|
|
30650
|
+
if (!cancelled) setProviderError(error instanceof Error ? error.message : String(error));
|
|
30651
|
+
});
|
|
30652
|
+
return () => {
|
|
30653
|
+
cancelled = true;
|
|
30654
|
+
};
|
|
30655
|
+
}, [
|
|
30656
|
+
modelOpen,
|
|
30657
|
+
modelLoadEpoch,
|
|
30658
|
+
props.loadModelProviders
|
|
30659
|
+
]);
|
|
30660
|
+
(0, import_react.useEffect)(() => {
|
|
30661
|
+
const subscribe = props.subscribeModelProviders;
|
|
30662
|
+
if (!modelOpen || subscribe === void 0) return;
|
|
30663
|
+
try {
|
|
30664
|
+
return subscribe(() => setModelLoadEpoch((epoch) => epoch + 1));
|
|
30665
|
+
} catch (error) {
|
|
30666
|
+
setProviderError(error instanceof Error ? error.message : String(error));
|
|
30667
|
+
}
|
|
30668
|
+
}, [modelOpen, props.subscribeModelProviders]);
|
|
29274
30669
|
const busy = view.busy;
|
|
29275
30670
|
const [showReasoning, setShowReasoning] = (0, import_react.useState)(false);
|
|
29276
30671
|
const [verboseOpen, setVerboseOpen] = (0, import_react.useState)(false);
|
|
29277
30672
|
const [helpOpen, setHelpOpen] = (0, import_react.useState)(false);
|
|
29278
30673
|
const [modeOpen, setModeOpen] = (0, import_react.useState)(false);
|
|
30674
|
+
const [permissionOpen, setPermissionOpen] = (0, import_react.useState)(false);
|
|
29279
30675
|
const [resumeOpen, setResumeOpen] = (0, import_react.useState)(false);
|
|
29280
30676
|
const [pluginOpen, setPluginOpen] = (0, import_react.useState)(false);
|
|
29281
30677
|
const [pluginQuery, setPluginQuery] = (0, import_react.useState)("");
|
|
@@ -29295,20 +30691,39 @@ function App(props) {
|
|
|
29295
30691
|
const historyConsumed = (0, import_react.useCallback)(() => {
|
|
29296
30692
|
setHistoryFill(void 0);
|
|
29297
30693
|
}, []);
|
|
29298
|
-
/**
|
|
29299
|
-
|
|
30694
|
+
/** The append-only flush boundary (see `settledEntryCount`): entries below
|
|
30695
|
+
* this index are final and ride the `<Static>` scrollback; everything at or
|
|
30696
|
+
* beyond stays in the live tree. Pending inbox rows always live at
|
|
30697
|
+
* index >= settled, so the queued-inbox scan below only walks the mutable
|
|
30698
|
+
* tail instead of the whole history. */
|
|
30699
|
+
const settled = (0, import_react.useMemo)(() => settledEntryCount(view.entries), [view.entries]);
|
|
30700
|
+
/** Live queued inbox rows (event-sourced from `agent/inbox/spliced`). The
|
|
30701
|
+
* projection only appends and removes pending rows at index >= settled, so
|
|
30702
|
+
* a bounded tail scan replaces an unconditional O(history) filter on every
|
|
30703
|
+
* event. */
|
|
30704
|
+
const queuedRows = (0, import_react.useMemo)(() => {
|
|
30705
|
+
const rows = [];
|
|
30706
|
+
for (let index = settled; index < view.entries.length; index++) {
|
|
30707
|
+
const entry = view.entries[index];
|
|
30708
|
+
if (entry.kind === "pending") rows.push(entry);
|
|
30709
|
+
}
|
|
30710
|
+
return rows;
|
|
30711
|
+
}, [view.entries, settled]);
|
|
29300
30712
|
const [refreshEpoch, setRefreshEpoch] = (0, import_react.useState)(0);
|
|
29301
30713
|
const approvalSnapshot = (0, import_react.useSyncExternalStore)(props.approval.subscribe, props.approval.getSnapshot);
|
|
29302
30714
|
const questionSnapshot = (0, import_react.useSyncExternalStore)(props.questions.subscribe, props.questions.getSnapshot);
|
|
29303
30715
|
const approvalPending = approvalSnapshot.pending !== void 0;
|
|
29304
30716
|
const questionPending = questionSnapshot.pending !== void 0;
|
|
29305
|
-
const inputActive = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
30717
|
+
const inputActive = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
29306
30718
|
(0, import_react.useEffect)(() => {
|
|
29307
30719
|
if (!approvalPending && !questionPending) return;
|
|
29308
30720
|
setModelOpen(false);
|
|
30721
|
+
setProviderOpen(false);
|
|
30722
|
+
setProviderAction(void 0);
|
|
29309
30723
|
setEffortFor(void 0);
|
|
29310
30724
|
setHelpOpen(false);
|
|
29311
30725
|
setModeOpen(false);
|
|
30726
|
+
setPermissionOpen(false);
|
|
29312
30727
|
setResumeOpen(false);
|
|
29313
30728
|
setPluginOpen(false);
|
|
29314
30729
|
setStatuslineOpen(false);
|
|
@@ -29316,38 +30731,17 @@ function App(props) {
|
|
|
29316
30731
|
setHistoryOpen(false);
|
|
29317
30732
|
setVerboseOpen(false);
|
|
29318
30733
|
}, [approvalPending, questionPending]);
|
|
29319
|
-
const
|
|
30734
|
+
const settledRowsCache = (0, import_react.useRef)(void 0);
|
|
29320
30735
|
const settledRows = (0, import_react.useMemo)(() => {
|
|
29321
|
-
const
|
|
29322
|
-
|
|
29323
|
-
|
|
29324
|
-
})];
|
|
29325
|
-
view.entries.slice(0, settled).forEach((entry, index) => {
|
|
29326
|
-
const row = (0, import_react.createElement)(EntryLine, {
|
|
29327
|
-
entry,
|
|
29328
|
-
showReasoning,
|
|
29329
|
-
verbose: false
|
|
29330
|
-
});
|
|
29331
|
-
const roomyPrompt = entry.kind === "user" && !entry.notice;
|
|
29332
|
-
if (roomyPrompt) rows.push((0, import_react.createElement)(Box, {
|
|
29333
|
-
key: `prompt-before-${index}`,
|
|
29334
|
-
paddingX: 1
|
|
29335
|
-
}, (0, import_react.createElement)(Text, null, " ")));
|
|
29336
|
-
rows.push((0, import_react.createElement)(Box, {
|
|
29337
|
-
key: index,
|
|
29338
|
-
paddingX: 1
|
|
29339
|
-
}, row));
|
|
29340
|
-
if (roomyPrompt) rows.push((0, import_react.createElement)(Box, {
|
|
29341
|
-
key: `prompt-after-${index}`,
|
|
29342
|
-
paddingX: 1
|
|
29343
|
-
}, (0, import_react.createElement)(Text, null, " ")));
|
|
29344
|
-
});
|
|
29345
|
-
return rows;
|
|
30736
|
+
const result = computeSettledRows(settledRowsCache.current, view.entries, settled, showReasoning, props.resumed, refreshEpoch);
|
|
30737
|
+
settledRowsCache.current = result.cache;
|
|
30738
|
+
return result.cache.flat;
|
|
29346
30739
|
}, [
|
|
29347
30740
|
view.entries,
|
|
29348
30741
|
settled,
|
|
29349
30742
|
showReasoning,
|
|
29350
|
-
props.resumed
|
|
30743
|
+
props.resumed,
|
|
30744
|
+
refreshEpoch
|
|
29351
30745
|
]);
|
|
29352
30746
|
const appStdout = useStdout().stdout;
|
|
29353
30747
|
const [terminalSize, setTerminalSize] = (0, import_react.useState)(() => ({
|
|
@@ -29394,8 +30788,8 @@ function App(props) {
|
|
|
29394
30788
|
const streamRows = Math.max(1, dynamicRows - visibleLiveLines.length);
|
|
29395
30789
|
const reasoningRows = view.streamingReasoning === "" ? 0 : view.streaming === "" ? streamRows : streamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(streamRows / 3)) : 1;
|
|
29396
30790
|
const answerRows = view.streaming === "" ? 0 : Math.max(1, streamRows - reasoningRows);
|
|
29397
|
-
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
29398
|
-
const modalVisible = modelOpen || helpOpen || modeOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || verboseOpen && !approvalPending && !questionPending || approvalPending || questionPending;
|
|
30791
|
+
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
30792
|
+
const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || verboseOpen && !approvalPending && !questionPending || approvalPending || questionPending;
|
|
29399
30793
|
const closeInspector = (0, import_react.useCallback)(() => {
|
|
29400
30794
|
setVerboseOpen(false);
|
|
29401
30795
|
}, []);
|
|
@@ -29411,11 +30805,120 @@ function App(props) {
|
|
|
29411
30805
|
setEffortLabel(effortId);
|
|
29412
30806
|
notify(`model → next step uses ${label}${effortId === void 0 || effortId === "" ? "" : `@${effortId}`}`);
|
|
29413
30807
|
setModelOpen(false);
|
|
30808
|
+
setProviderOpen(false);
|
|
30809
|
+
setProviderAction(void 0);
|
|
29414
30810
|
setEffortFor(void 0);
|
|
29415
30811
|
} catch (error) {
|
|
29416
30812
|
notify(`model switch failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
29417
30813
|
}
|
|
29418
30814
|
};
|
|
30815
|
+
const reloadModelSurfaces = () => {
|
|
30816
|
+
setModelLoadEpoch((epoch) => epoch + 1);
|
|
30817
|
+
};
|
|
30818
|
+
const closeModelSurface = () => {
|
|
30819
|
+
setModelOpen(false);
|
|
30820
|
+
setProviderOpen(false);
|
|
30821
|
+
setProviderAction(void 0);
|
|
30822
|
+
setEffortFor(void 0);
|
|
30823
|
+
};
|
|
30824
|
+
let modelSurface;
|
|
30825
|
+
if (modelOpen && !approvalPending && !questionPending) {
|
|
30826
|
+
if (providerAction?.kind === "credential" && props.saveModelProviderCredential !== void 0) modelSurface = (0, import_react.createElement)(ProviderCredentialPanel, {
|
|
30827
|
+
target: providerAction.target,
|
|
30828
|
+
save: props.saveModelProviderCredential,
|
|
30829
|
+
done: () => {
|
|
30830
|
+
const target = providerAction.target;
|
|
30831
|
+
setProviderAction(void 0);
|
|
30832
|
+
setProviderOpen(false);
|
|
30833
|
+
reloadModelSurfaces();
|
|
30834
|
+
notify(`API key saved for ${target.displayName}; select a model`);
|
|
30835
|
+
},
|
|
30836
|
+
back: () => setProviderAction(void 0)
|
|
30837
|
+
});
|
|
30838
|
+
else if (providerAction?.kind === "unset" && props.unsetModelProviderCredential !== void 0) modelSurface = (0, import_react.createElement)(ProviderConfirmPanel, {
|
|
30839
|
+
target: providerAction.target,
|
|
30840
|
+
kind: "credential",
|
|
30841
|
+
confirm: props.unsetModelProviderCredential,
|
|
30842
|
+
done: () => {
|
|
30843
|
+
const target = providerAction.target;
|
|
30844
|
+
setProviderAction(void 0);
|
|
30845
|
+
setProviderOpen(true);
|
|
30846
|
+
reloadModelSurfaces();
|
|
30847
|
+
notify(`API key removed for ${target.displayName}`);
|
|
30848
|
+
},
|
|
30849
|
+
back: () => setProviderAction(void 0)
|
|
30850
|
+
});
|
|
30851
|
+
else if (providerAction?.kind === "remove" && props.removeModelProvider !== void 0) modelSurface = (0, import_react.createElement)(ProviderConfirmPanel, {
|
|
30852
|
+
target: providerAction.target,
|
|
30853
|
+
kind: "provider",
|
|
30854
|
+
confirm: props.removeModelProvider,
|
|
30855
|
+
done: () => {
|
|
30856
|
+
const target = providerAction.target;
|
|
30857
|
+
setProviderAction(void 0);
|
|
30858
|
+
setProviderOpen(true);
|
|
30859
|
+
reloadModelSurfaces();
|
|
30860
|
+
notify(`provider removed: ${target.displayName}`);
|
|
30861
|
+
},
|
|
30862
|
+
back: () => setProviderAction(void 0)
|
|
30863
|
+
});
|
|
30864
|
+
else if (providerOpen) modelSurface = (0, import_react.createElement)(ProviderPanel, {
|
|
30865
|
+
directory: providerDirectory,
|
|
30866
|
+
error: providerError,
|
|
30867
|
+
onCredential: (target) => {
|
|
30868
|
+
if (props.saveModelProviderCredential === void 0) {
|
|
30869
|
+
notify("API key storage is unavailable in this profile", "warning");
|
|
30870
|
+
return;
|
|
30871
|
+
}
|
|
30872
|
+
setProviderAction({
|
|
30873
|
+
kind: "credential",
|
|
30874
|
+
target
|
|
30875
|
+
});
|
|
30876
|
+
},
|
|
30877
|
+
onUnset: (target) => {
|
|
30878
|
+
if (props.unsetModelProviderCredential === void 0) {
|
|
30879
|
+
notify("API key removal is unavailable in this profile", "warning");
|
|
30880
|
+
return;
|
|
30881
|
+
}
|
|
30882
|
+
setProviderAction({
|
|
30883
|
+
kind: "unset",
|
|
30884
|
+
target
|
|
30885
|
+
});
|
|
30886
|
+
},
|
|
30887
|
+
onRemove: (target) => {
|
|
30888
|
+
if (props.removeModelProvider === void 0) {
|
|
30889
|
+
notify("provider removal is unavailable in this profile", "warning");
|
|
30890
|
+
return;
|
|
30891
|
+
}
|
|
30892
|
+
setProviderAction({
|
|
30893
|
+
kind: "remove",
|
|
30894
|
+
target
|
|
30895
|
+
});
|
|
30896
|
+
},
|
|
30897
|
+
onRetry: reloadModelSurfaces,
|
|
30898
|
+
onBack: () => setProviderOpen(false)
|
|
30899
|
+
});
|
|
30900
|
+
else if (effortFor !== void 0) modelSurface = (0, import_react.createElement)(EffortPanel, {
|
|
30901
|
+
row: effortFor,
|
|
30902
|
+
current: effortLabel,
|
|
30903
|
+
select: (effortId) => applyModel(effortFor, effortId),
|
|
30904
|
+
back: () => setEffortFor(void 0)
|
|
30905
|
+
});
|
|
30906
|
+
else modelSurface = (0, import_react.createElement)(ModelPanel, {
|
|
30907
|
+
directory,
|
|
30908
|
+
error: modelError,
|
|
30909
|
+
onSelect: (row) => {
|
|
30910
|
+
if (row.reasoning !== void 0 && row.reasoning.efforts.length > 1) {
|
|
30911
|
+
setEffortFor(row);
|
|
30912
|
+
return;
|
|
30913
|
+
}
|
|
30914
|
+
const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0].id : void 0;
|
|
30915
|
+
applyModel(row, effortId);
|
|
30916
|
+
},
|
|
30917
|
+
...props.loadModelProviders === void 0 || props.saveModelProviderCredential === void 0 ? {} : { onProviders: () => setProviderOpen(true) },
|
|
30918
|
+
onRetry: reloadModelSurfaces,
|
|
30919
|
+
onClose: closeModelSurface
|
|
30920
|
+
});
|
|
30921
|
+
}
|
|
29419
30922
|
return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(MemoStaticTranscript, {
|
|
29420
30923
|
key: refreshEpoch,
|
|
29421
30924
|
items: settledRows
|
|
@@ -29424,7 +30927,8 @@ function App(props) {
|
|
|
29424
30927
|
paddingX: 2
|
|
29425
30928
|
}, visibleLiveLines.length === 0 ? void 0 : (0, import_react.createElement)(StyledRows, { lines: visibleLiveLines }), view.streamingReasoning !== "" && reasoningRows > 0 ? (0, import_react.createElement)(StreamTail, {
|
|
29426
30929
|
text: showReasoning ? view.streamingReasoning : "Thinking…",
|
|
29427
|
-
prefix: "
|
|
30930
|
+
prefix: "✻ ",
|
|
30931
|
+
continuationPrefix: " ",
|
|
29428
30932
|
dim: true,
|
|
29429
30933
|
maxRows: reasoningRows
|
|
29430
30934
|
}) : void 0, view.streaming !== "" && answerRows > 0 ? (0, import_react.createElement)(StreamTail, {
|
|
@@ -29439,30 +30943,7 @@ function App(props) {
|
|
|
29439
30943
|
}), (0, import_react.createElement)(ApprovalBar, {
|
|
29440
30944
|
snapshot: approvalSnapshot,
|
|
29441
30945
|
locked: questionPending
|
|
29442
|
-
}),
|
|
29443
|
-
directory,
|
|
29444
|
-
error: modelError,
|
|
29445
|
-
onSelect: (row) => {
|
|
29446
|
-
if (row.reasoning !== void 0 && row.reasoning.efforts.length > 1) {
|
|
29447
|
-
setEffortFor(row);
|
|
29448
|
-
return;
|
|
29449
|
-
}
|
|
29450
|
-
const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0].id : void 0;
|
|
29451
|
-
applyModel(row, effortId);
|
|
29452
|
-
},
|
|
29453
|
-
onRetry: () => {
|
|
29454
|
-
setModelLoadEpoch((epoch) => epoch + 1);
|
|
29455
|
-
},
|
|
29456
|
-
onClose: () => {
|
|
29457
|
-
setModelOpen(false);
|
|
29458
|
-
setEffortFor(void 0);
|
|
29459
|
-
}
|
|
29460
|
-
}) : (0, import_react.createElement)(EffortPanel, {
|
|
29461
|
-
row: effortFor,
|
|
29462
|
-
current: effortLabel,
|
|
29463
|
-
select: (effortId) => applyModel(effortFor, effortId),
|
|
29464
|
-
back: () => setEffortFor(void 0)
|
|
29465
|
-
}) : void 0, helpOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(HelpPanel, {
|
|
30946
|
+
}), modelSurface, helpOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(HelpPanel, {
|
|
29466
30947
|
descriptors,
|
|
29467
30948
|
skills,
|
|
29468
30949
|
commandError: props.commands.error,
|
|
@@ -29483,6 +30964,19 @@ function App(props) {
|
|
|
29483
30964
|
}, (reason) => notify(`mode switch failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error"));
|
|
29484
30965
|
},
|
|
29485
30966
|
close: () => setModeOpen(false)
|
|
30967
|
+
}) : void 0, permissionOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(PermissionPanel, {
|
|
30968
|
+
current: props.permission,
|
|
30969
|
+
load: props.loadPermissions,
|
|
30970
|
+
select: (id) => {
|
|
30971
|
+
try {
|
|
30972
|
+
const selected = props.setPermission(id);
|
|
30973
|
+
notify(`permission → ${selected}`);
|
|
30974
|
+
setPermissionOpen(false);
|
|
30975
|
+
} catch (reason) {
|
|
30976
|
+
notify(`permission change failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
|
|
30977
|
+
}
|
|
30978
|
+
},
|
|
30979
|
+
close: () => setPermissionOpen(false)
|
|
29486
30980
|
}) : void 0, resumeOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(ResumePanel, {
|
|
29487
30981
|
currentCwd: props.workspaceRoot,
|
|
29488
30982
|
load: props.loadSessions,
|
|
@@ -29542,6 +31036,10 @@ function App(props) {
|
|
|
29542
31036
|
openModel: () => {
|
|
29543
31037
|
setDirectory(void 0);
|
|
29544
31038
|
setModelError(void 0);
|
|
31039
|
+
setProviderDirectory(void 0);
|
|
31040
|
+
setProviderError(void 0);
|
|
31041
|
+
setProviderOpen(false);
|
|
31042
|
+
setProviderAction(void 0);
|
|
29545
31043
|
setEffortFor(void 0);
|
|
29546
31044
|
setModelOpen(true);
|
|
29547
31045
|
},
|
|
@@ -29572,6 +31070,7 @@ function App(props) {
|
|
|
29572
31070
|
setHelpOpen(true);
|
|
29573
31071
|
},
|
|
29574
31072
|
openMode: () => setModeOpen(true),
|
|
31073
|
+
openPermission: () => setPermissionOpen(true),
|
|
29575
31074
|
openResume: () => setResumeOpen(true),
|
|
29576
31075
|
openPlugin: (query = "") => {
|
|
29577
31076
|
setPluginQuery(query);
|
|
@@ -29609,7 +31108,6 @@ function App(props) {
|
|
|
29609
31108
|
cancelQueued: props.cancelQueued,
|
|
29610
31109
|
historyFill,
|
|
29611
31110
|
historyConsumed,
|
|
29612
|
-
waveTick,
|
|
29613
31111
|
waveTier,
|
|
29614
31112
|
waveStyle
|
|
29615
31113
|
}), (0, import_react.createElement)(StatusLine, {
|
|
@@ -29621,7 +31119,7 @@ function App(props) {
|
|
|
29621
31119
|
sessionId: props.sessionId,
|
|
29622
31120
|
title: view.title,
|
|
29623
31121
|
plan: view.plan,
|
|
29624
|
-
permission: view.permission,
|
|
31122
|
+
permission: view.permission !== "" ? view.permission : props.permission,
|
|
29625
31123
|
sandbox: view.sandbox,
|
|
29626
31124
|
goal: view.goal === void 0 ? void 0 : {
|
|
29627
31125
|
phase: view.goal.phase,
|
|
@@ -29665,16 +31163,22 @@ function mountApprovalAnswerer(ctx, owns, preview) {
|
|
|
29665
31163
|
if (request.signal?.aborted === true) return Promise.resolve("cancelled");
|
|
29666
31164
|
let resolved = false;
|
|
29667
31165
|
let settle;
|
|
31166
|
+
const signal = request.signal;
|
|
31167
|
+
const onAbort = () => withdraw();
|
|
31168
|
+
const detachAbort = () => {
|
|
31169
|
+
if (signal !== void 0) signal.removeEventListener("abort", onAbort);
|
|
31170
|
+
};
|
|
29668
31171
|
const withdraw = () => {
|
|
29669
31172
|
if (resolved) return;
|
|
29670
31173
|
resolved = true;
|
|
31174
|
+
detachAbort();
|
|
29671
31175
|
set({
|
|
29672
31176
|
pending: void 0,
|
|
29673
31177
|
answered: false
|
|
29674
31178
|
});
|
|
29675
31179
|
settle("cancelled");
|
|
29676
31180
|
};
|
|
29677
|
-
if (
|
|
31181
|
+
if (signal !== void 0) signal.addEventListener("abort", onAbort, { once: true });
|
|
29678
31182
|
const pending = {
|
|
29679
31183
|
headline: request.reason ?? `tool ${request.toolName} asks for your approval`,
|
|
29680
31184
|
toolName: request.toolName,
|
|
@@ -29682,6 +31186,7 @@ function mountApprovalAnswerer(ctx, owns, preview) {
|
|
|
29682
31186
|
answer: (outcome) => {
|
|
29683
31187
|
if (resolved) return;
|
|
29684
31188
|
resolved = true;
|
|
31189
|
+
detachAbort();
|
|
29685
31190
|
set({
|
|
29686
31191
|
pending,
|
|
29687
31192
|
answered: true
|
|
@@ -29914,6 +31419,292 @@ async function loadModelDirectory(ctx) {
|
|
|
29914
31419
|
};
|
|
29915
31420
|
}
|
|
29916
31421
|
//#endregion
|
|
31422
|
+
//#region src/provider-settings.ts
|
|
31423
|
+
/** Human text for a rejection value (mirrors the web page's `messageOf`). */
|
|
31424
|
+
function messageOf(error) {
|
|
31425
|
+
return error instanceof Error ? error.message : String(error);
|
|
31426
|
+
}
|
|
31427
|
+
/** Collapse every whitespace/control run to one space so a notice stays one line. */
|
|
31428
|
+
function singleLine(message) {
|
|
31429
|
+
return message.replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim();
|
|
31430
|
+
}
|
|
31431
|
+
/** Keep a misbehaving credential provider from reflecting the submitted secret. */
|
|
31432
|
+
function credentialWriteMessage(error, secret) {
|
|
31433
|
+
const message = singleLine(messageOf(error));
|
|
31434
|
+
return message.includes(secret) ? "credentials service rejected the API key" : message;
|
|
31435
|
+
}
|
|
31436
|
+
/** Obvious shell-assignment paste; mirrors the official Web Models editor. */
|
|
31437
|
+
const ENV_ASSIGNMENT = /^[A-Z][A-Z0-9_]*=[^=]/;
|
|
31438
|
+
/** Whether the whole draft is wrapped in one matching quote pair. */
|
|
31439
|
+
function hasWrappingQuotes(value) {
|
|
31440
|
+
const first = value[0];
|
|
31441
|
+
return (first === "\"" || first === "'" || first === "`") && value.length > 1 && value.endsWith(first);
|
|
31442
|
+
}
|
|
31443
|
+
/** Read the value at a path through plain objects; undefined when any segment misses. */
|
|
31444
|
+
function getPath(value, path) {
|
|
31445
|
+
let current = value;
|
|
31446
|
+
for (const segment of path) {
|
|
31447
|
+
if (typeof current !== "object" || current === null) return void 0;
|
|
31448
|
+
current = current[segment];
|
|
31449
|
+
}
|
|
31450
|
+
return current;
|
|
31451
|
+
}
|
|
31452
|
+
/** Whether a path resolves to a defined value (the empty path reads the root). */
|
|
31453
|
+
function hasPath(value, path) {
|
|
31454
|
+
return path.length === 0 ? value !== void 0 : getPath(value, path) !== void 0;
|
|
31455
|
+
}
|
|
31456
|
+
/** The credential reference a resolved profile names (its `apiKeyEnv` field). */
|
|
31457
|
+
function profileRefOf(profile) {
|
|
31458
|
+
if (typeof profile !== "object" || profile === null) return void 0;
|
|
31459
|
+
const ref = profile.apiKeyEnv;
|
|
31460
|
+
return typeof ref === "string" && ref.length > 0 ? ref : void 0;
|
|
31461
|
+
}
|
|
31462
|
+
/**
|
|
31463
|
+
* The conventional credential reference for a provider route: `<ROUTE>_API_KEY`
|
|
31464
|
+
* with the route uppercased and every non-alphanumeric run collapsed to one
|
|
31465
|
+
* underscore — the exact derivation the official Models page uses
|
|
31466
|
+
* (`deriveKeyRef` in `ui-settings-models`), so a key saved here is found there.
|
|
31467
|
+
* @param provider - provider route id (e.g. `pi-ai`, `minimax-cn`).
|
|
31468
|
+
* @returns the derived reference name (e.g. `PI_AI_API_KEY`).
|
|
31469
|
+
*/
|
|
31470
|
+
function deriveCredentialRef(provider) {
|
|
31471
|
+
return `${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`;
|
|
31472
|
+
}
|
|
31473
|
+
/** Events that invalidate the official Models provider/settings/credential join. */
|
|
31474
|
+
const PROVIDER_SETTINGS_EVENTS = [
|
|
31475
|
+
"credentials/updated",
|
|
31476
|
+
"settings/document-updated",
|
|
31477
|
+
"llm/adapters-updated"
|
|
31478
|
+
];
|
|
31479
|
+
/** Subscribe to the same provider-directory invalidations as the official Web Models page. */
|
|
31480
|
+
function subscribeProviderSettings(ctx, listener) {
|
|
31481
|
+
const events = ctx;
|
|
31482
|
+
const disposers = PROVIDER_SETTINGS_EVENTS.map((event) => events.on(event, () => listener()));
|
|
31483
|
+
return () => {
|
|
31484
|
+
for (const dispose of disposers) dispose();
|
|
31485
|
+
};
|
|
31486
|
+
}
|
|
31487
|
+
/** A single-line, bounded error from the provider-management adapter. */
|
|
31488
|
+
var ProviderSettingsError = class extends Error {
|
|
31489
|
+
constructor(message) {
|
|
31490
|
+
super(message);
|
|
31491
|
+
this.name = "ProviderSettingsError";
|
|
31492
|
+
}
|
|
31493
|
+
};
|
|
31494
|
+
/**
|
|
31495
|
+
* Join the configurable-provider directory, the redacted settings
|
|
31496
|
+
* namespaces, and the referenced credentials into panel rows, web-parity:
|
|
31497
|
+
* - directory entries merge with `listProviders()` to mark each live or
|
|
31498
|
+
* dormant, and routes registered without a directory declaration appear as
|
|
31499
|
+
* read-only/unmanaged rows (no settings address);
|
|
31500
|
+
* - a whole-section entry is configured whenever its namespace resolves;
|
|
31501
|
+
* a path-addressed one only when the profile resolves there;
|
|
31502
|
+
* - a row is removable when the user layer alone carries its profile;
|
|
31503
|
+
* - only refs named by resolved profiles are described, and a per-ref failure
|
|
31504
|
+
* degrades to that row's bounded error instead of losing it.
|
|
31505
|
+
* Absent `settings`/`credentials` services are tolerated the same way.
|
|
31506
|
+
* @param ctx - context carrying the `llm` service (settings/credentials optional).
|
|
31507
|
+
* @returns the resolved directory; empty rows when `llm` is unavailable.
|
|
31508
|
+
*/
|
|
31509
|
+
async function loadProviderSettings(ctx) {
|
|
31510
|
+
const llm = ctx.get("llm");
|
|
31511
|
+
if (llm === void 0) return {
|
|
31512
|
+
rows: [],
|
|
31513
|
+
writable: false,
|
|
31514
|
+
failures: []
|
|
31515
|
+
};
|
|
31516
|
+
const registered = llm.listProviders();
|
|
31517
|
+
const failures = [];
|
|
31518
|
+
const directoryEntries = [];
|
|
31519
|
+
if (llm.listConfigurableProviders !== void 0) try {
|
|
31520
|
+
directoryEntries.push(...llm.listConfigurableProviders());
|
|
31521
|
+
} catch (error) {
|
|
31522
|
+
failures.push(`configurable-provider directory failed: ${singleLine(messageOf(error))}`);
|
|
31523
|
+
}
|
|
31524
|
+
const settings = ctx.get("settings");
|
|
31525
|
+
let descriptors = [];
|
|
31526
|
+
let writable = false;
|
|
31527
|
+
if (settings !== void 0) try {
|
|
31528
|
+
descriptors = settings.describe({ redactSecrets: true });
|
|
31529
|
+
writable = settings.writable === true;
|
|
31530
|
+
} catch (error) {
|
|
31531
|
+
failures.push(`settings describe failed: ${singleLine(messageOf(error))}`);
|
|
31532
|
+
}
|
|
31533
|
+
const namespaces = new Map(descriptors.map((descriptor) => [descriptor.ns, descriptor]));
|
|
31534
|
+
const active = new Set(registered.map((provider) => provider.id));
|
|
31535
|
+
const declared = new Set(directoryEntries.map((entry) => entry.provider));
|
|
31536
|
+
const rows = [...directoryEntries.map((entry) => ({
|
|
31537
|
+
provider: entry.provider,
|
|
31538
|
+
displayName: entry.displayName,
|
|
31539
|
+
active: active.has(entry.provider),
|
|
31540
|
+
settingsNs: entry.settingsNs,
|
|
31541
|
+
settingsPath: entry.settingsPath,
|
|
31542
|
+
...entry.declared === void 0 ? {} : { declared: entry.declared }
|
|
31543
|
+
})), ...registered.filter((provider) => !declared.has(provider.id)).map((provider) => ({
|
|
31544
|
+
provider: provider.id,
|
|
31545
|
+
displayName: provider.name,
|
|
31546
|
+
active: true,
|
|
31547
|
+
settingsNs: "",
|
|
31548
|
+
settingsPath: []
|
|
31549
|
+
}))].map((base) => {
|
|
31550
|
+
const namespace = base.settingsNs.length === 0 ? void 0 : namespaces.get(base.settingsNs);
|
|
31551
|
+
const profile = namespace === void 0 ? void 0 : base.settingsPath.length === 0 ? namespace.value : getPath(namespace.value, base.settingsPath);
|
|
31552
|
+
const configured = namespace !== void 0 && (base.settingsPath.length === 0 || profile !== void 0);
|
|
31553
|
+
const removable = namespace !== void 0 && base.settingsPath.length > 0 && hasPath(namespace.user, base.settingsPath) && !hasPath(namespace.base, base.settingsPath);
|
|
31554
|
+
const credentialRef = profileRefOf(profile);
|
|
31555
|
+
return {
|
|
31556
|
+
provider: base.provider,
|
|
31557
|
+
displayName: base.displayName,
|
|
31558
|
+
active: base.active,
|
|
31559
|
+
settingsNs: base.settingsNs,
|
|
31560
|
+
settingsPath: base.settingsPath,
|
|
31561
|
+
settingsRevision: namespace?.revision ?? 0,
|
|
31562
|
+
configured,
|
|
31563
|
+
removable,
|
|
31564
|
+
...credentialRef === void 0 ? {} : { credentialRef },
|
|
31565
|
+
suggestedRef: deriveCredentialRef(base.provider),
|
|
31566
|
+
...base.declared === void 0 ? {} : { declared: base.declared }
|
|
31567
|
+
};
|
|
31568
|
+
});
|
|
31569
|
+
const refs = [...new Set(rows.flatMap((row) => row.credentialRef === void 0 ? [] : [row.credentialRef]))];
|
|
31570
|
+
const credentialViews = /* @__PURE__ */ new Map();
|
|
31571
|
+
const credentials = ctx.get("credentials");
|
|
31572
|
+
if (refs.length > 0) {
|
|
31573
|
+
if (credentials === void 0) for (const ref of refs) credentialViews.set(ref, {
|
|
31574
|
+
kind: "error",
|
|
31575
|
+
message: "credentials service is unavailable"
|
|
31576
|
+
});
|
|
31577
|
+
else await Promise.all(refs.map(async (ref) => {
|
|
31578
|
+
try {
|
|
31579
|
+
const facts = await credentials.describe(ref);
|
|
31580
|
+
credentialViews.set(ref, {
|
|
31581
|
+
kind: "facts",
|
|
31582
|
+
configured: facts.configured,
|
|
31583
|
+
writable: facts.writable,
|
|
31584
|
+
...facts.source === void 0 ? {} : { source: facts.source }
|
|
31585
|
+
});
|
|
31586
|
+
} catch (error) {
|
|
31587
|
+
credentialViews.set(ref, {
|
|
31588
|
+
kind: "error",
|
|
31589
|
+
message: singleLine(messageOf(error))
|
|
31590
|
+
});
|
|
31591
|
+
}
|
|
31592
|
+
}));
|
|
31593
|
+
}
|
|
31594
|
+
return {
|
|
31595
|
+
rows: rows.map((row) => ({
|
|
31596
|
+
...row,
|
|
31597
|
+
credential: row.credentialRef === void 0 ? void 0 : credentialViews.get(row.credentialRef) ?? {
|
|
31598
|
+
kind: "error",
|
|
31599
|
+
message: "credential describe returned no view"
|
|
31600
|
+
}
|
|
31601
|
+
})),
|
|
31602
|
+
writable,
|
|
31603
|
+
failures
|
|
31604
|
+
};
|
|
31605
|
+
}
|
|
31606
|
+
/**
|
|
31607
|
+
* Store a provider API key, web-parity: validate with `normalizeApiKey`
|
|
31608
|
+
* (single-line, actionable errors that never echo the key), materialize the
|
|
31609
|
+
* profile/`apiKeyEnv` through `settings.mutate` first when the resolved
|
|
31610
|
+
* profile names no reference (dormant route or ref-less profile), then store
|
|
31611
|
+
* under the trusted named ref or the derived conventional ref. An existing
|
|
31612
|
+
* whole-section DeepSeek whose resolved profile already names
|
|
31613
|
+
* `DEEPSEEK_API_KEY` needs no settings mutation. Env-supplied read-only keys
|
|
31614
|
+
* are refused before any service call.
|
|
31615
|
+
* @param ctx - context carrying `settings` (when materializing) and `credentials`.
|
|
31616
|
+
* @param target - the joined row to write through.
|
|
31617
|
+
* @param rawKey - the key exactly as typed; surrounding whitespace is trimmed.
|
|
31618
|
+
* @throws {@link ProviderSettingsError} with a single-line, key-free message.
|
|
31619
|
+
*/
|
|
31620
|
+
async function saveProviderCredential(ctx, target, rawKey) {
|
|
31621
|
+
const trimmed = rawKey.trim();
|
|
31622
|
+
if (ENV_ASSIGNMENT.test(trimmed) || hasWrappingQuotes(trimmed)) throw new ProviderSettingsError("paste only the API key, without an environment-variable name or wrapping quotes");
|
|
31623
|
+
const checked = normalizeApiKey(rawKey);
|
|
31624
|
+
if (!checked.ok) throw new ProviderSettingsError(checked.reason === "empty" ? "the API key is empty after trimming surrounding whitespace" : "the API key contains characters an HTTP header cannot carry; type a plain printable-ASCII key");
|
|
31625
|
+
if (target.settingsNs.length === 0) throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; configure it in settings.yaml`);
|
|
31626
|
+
if (target.credential?.kind === "facts" && target.credential.writable === false) throw new ProviderSettingsError(`the key for provider "${target.provider}" is supplied read-only by the environment; unset it in the shell instead of overwriting it here`);
|
|
31627
|
+
const credentials = ctx.get("credentials");
|
|
31628
|
+
if (credentials === void 0) throw new ProviderSettingsError("credentials service is unavailable; cannot store the API key");
|
|
31629
|
+
const ref = target.credentialRef ?? deriveCredentialRef(target.provider);
|
|
31630
|
+
if (target.credentialRef === void 0) {
|
|
31631
|
+
const settings = ctx.get("settings");
|
|
31632
|
+
if (settings === void 0) throw new ProviderSettingsError("settings service is unavailable; cannot materialize the credential reference");
|
|
31633
|
+
try {
|
|
31634
|
+
await settings.mutate(target.settingsNs, [{
|
|
31635
|
+
op: "set",
|
|
31636
|
+
path: [...target.settingsPath, "apiKeyEnv"],
|
|
31637
|
+
value: ref
|
|
31638
|
+
}]);
|
|
31639
|
+
} catch (error) {
|
|
31640
|
+
throw new ProviderSettingsError(singleLine(messageOf(error)));
|
|
31641
|
+
}
|
|
31642
|
+
}
|
|
31643
|
+
try {
|
|
31644
|
+
await credentials.set(ref, checked.value);
|
|
31645
|
+
} catch (error) {
|
|
31646
|
+
throw new ProviderSettingsError(credentialWriteMessage(error, checked.value));
|
|
31647
|
+
}
|
|
31648
|
+
}
|
|
31649
|
+
/**
|
|
31650
|
+
* Remove the currently named credential without touching the provider
|
|
31651
|
+
* profile. Only the resolved profile's own reference is unset; a dormant or
|
|
31652
|
+
* ref-less row (nothing to remove), an already-absent key, and an
|
|
31653
|
+
* env-supplied read-only key are rejected safely before any service call.
|
|
31654
|
+
* @param ctx - context carrying the `credentials` service.
|
|
31655
|
+
* @param target - the joined row whose named credential to unset.
|
|
31656
|
+
* @throws {@link ProviderSettingsError} with a single-line, key-free message.
|
|
31657
|
+
*/
|
|
31658
|
+
async function unsetProviderCredential(ctx, target) {
|
|
31659
|
+
const ref = target.credentialRef;
|
|
31660
|
+
if (ref === void 0) throw new ProviderSettingsError(`provider "${target.provider}" names no credential reference to remove`);
|
|
31661
|
+
const facts = target.credential;
|
|
31662
|
+
if (facts?.kind === "facts" && facts.configured === false) throw new ProviderSettingsError(`provider "${target.provider}" has no configured credential to remove`);
|
|
31663
|
+
if (facts?.kind === "facts" && facts.writable === false) throw new ProviderSettingsError(`the key for provider "${target.provider}" is supplied read-only by the environment; unset it in the shell instead`);
|
|
31664
|
+
const credentials = ctx.get("credentials");
|
|
31665
|
+
if (credentials === void 0) throw new ProviderSettingsError("credentials service is unavailable; cannot remove the API key");
|
|
31666
|
+
try {
|
|
31667
|
+
await credentials.unset(ref);
|
|
31668
|
+
} catch (error) {
|
|
31669
|
+
throw new ProviderSettingsError(singleLine(messageOf(error)));
|
|
31670
|
+
}
|
|
31671
|
+
}
|
|
31672
|
+
/**
|
|
31673
|
+
* Remove a user-added provider profile, web-parity: only `removable` rows may
|
|
31674
|
+
* be removed; a page-managed credential — the derived ref, configured and
|
|
31675
|
+
* writable — is unset first (so a second-step failure leaves the row visible
|
|
31676
|
+
* and the operation retryable), then `settings.mutate` unsets
|
|
31677
|
+
* `target.settingsPath`. Both steps are idempotent. A hand-named credential
|
|
31678
|
+
* ref may be shared elsewhere and is left alone.
|
|
31679
|
+
* @param ctx - context carrying `credentials` and `settings`.
|
|
31680
|
+
* @param target - the joined row to remove.
|
|
31681
|
+
* @throws {@link ProviderSettingsError} with a single-line, key-free message.
|
|
31682
|
+
*/
|
|
31683
|
+
async function removeProviderSettings(ctx, target) {
|
|
31684
|
+
if (!target.removable) throw new ProviderSettingsError(`provider "${target.provider}" is not removable from the user settings layer`);
|
|
31685
|
+
if (target.settingsNs.length === 0) throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings profile to remove`);
|
|
31686
|
+
const managedRef = target.credentialRef === target.suggestedRef && target.credential?.kind === "facts" && target.credential.configured === true && target.credential.writable === true ? target.credentialRef : void 0;
|
|
31687
|
+
if (managedRef !== void 0) {
|
|
31688
|
+
const credentials = ctx.get("credentials");
|
|
31689
|
+
if (credentials === void 0) throw new ProviderSettingsError("credentials service is unavailable; cannot remove the managed API key");
|
|
31690
|
+
try {
|
|
31691
|
+
await credentials.unset(managedRef);
|
|
31692
|
+
} catch (error) {
|
|
31693
|
+
throw new ProviderSettingsError(singleLine(messageOf(error)));
|
|
31694
|
+
}
|
|
31695
|
+
}
|
|
31696
|
+
const settings = ctx.get("settings");
|
|
31697
|
+
if (settings === void 0) throw new ProviderSettingsError("settings service is unavailable; cannot remove the provider profile");
|
|
31698
|
+
try {
|
|
31699
|
+
await settings.mutate(target.settingsNs, [{
|
|
31700
|
+
op: "unset",
|
|
31701
|
+
path: [...target.settingsPath]
|
|
31702
|
+
}]);
|
|
31703
|
+
} catch (error) {
|
|
31704
|
+
throw new ProviderSettingsError(singleLine(messageOf(error)));
|
|
31705
|
+
}
|
|
31706
|
+
}
|
|
31707
|
+
//#endregion
|
|
29917
31708
|
//#region src/mentions.ts
|
|
29918
31709
|
/**
|
|
29919
31710
|
* Workspace @mention support: file and directory candidates from a bounded
|
|
@@ -30106,11 +31897,6 @@ function mountQuestionProvider(ctx) {
|
|
|
30106
31897
|
};
|
|
30107
31898
|
if (service !== void 0) service.registerProvider({ ask(request) {
|
|
30108
31899
|
return new Promise((resolve, reject) => {
|
|
30109
|
-
const pending = {
|
|
30110
|
-
request,
|
|
30111
|
-
resolve,
|
|
30112
|
-
reject
|
|
30113
|
-
};
|
|
30114
31900
|
const onAbort = () => {
|
|
30115
31901
|
if (active === pending) {
|
|
30116
31902
|
active = void 0;
|
|
@@ -30122,6 +31908,15 @@ function mountQuestionProvider(ctx) {
|
|
|
30122
31908
|
}
|
|
30123
31909
|
reject(ABORT_ERROR);
|
|
30124
31910
|
};
|
|
31911
|
+
const detachAbort = () => {
|
|
31912
|
+
if (request.signal !== void 0) request.signal.removeEventListener("abort", onAbort);
|
|
31913
|
+
};
|
|
31914
|
+
const pending = {
|
|
31915
|
+
request,
|
|
31916
|
+
resolve,
|
|
31917
|
+
reject,
|
|
31918
|
+
detachAbort
|
|
31919
|
+
};
|
|
30125
31920
|
if (request.signal?.aborted === true) {
|
|
30126
31921
|
reject(ABORT_ERROR);
|
|
30127
31922
|
return;
|
|
@@ -30147,6 +31942,7 @@ function mountQuestionProvider(ctx) {
|
|
|
30147
31942
|
if (active !== pending) return;
|
|
30148
31943
|
active = void 0;
|
|
30149
31944
|
set({ pending: void 0 });
|
|
31945
|
+
pending.detachAbort?.();
|
|
30150
31946
|
pending.resolve(answers);
|
|
30151
31947
|
advance();
|
|
30152
31948
|
},
|
|
@@ -30154,6 +31950,7 @@ function mountQuestionProvider(ctx) {
|
|
|
30154
31950
|
if (active !== pending) return;
|
|
30155
31951
|
active = void 0;
|
|
30156
31952
|
set({ pending: void 0 });
|
|
31953
|
+
pending.detachAbort?.();
|
|
30157
31954
|
pending.reject(ABORT_ERROR);
|
|
30158
31955
|
advance();
|
|
30159
31956
|
}
|
|
@@ -30174,6 +31971,15 @@ function mountQuestionProvider(ctx) {
|
|
|
30174
31971
|
function createTranscriptStore(replay) {
|
|
30175
31972
|
let view = replay === void 0 ? createTranscriptView() : projectEvents(replay);
|
|
30176
31973
|
const listeners = /* @__PURE__ */ new Set();
|
|
31974
|
+
let scheduled = false;
|
|
31975
|
+
const notify = () => {
|
|
31976
|
+
if (scheduled) return;
|
|
31977
|
+
scheduled = true;
|
|
31978
|
+
queueMicrotask(() => {
|
|
31979
|
+
scheduled = false;
|
|
31980
|
+
for (const listener of listeners) listener();
|
|
31981
|
+
});
|
|
31982
|
+
};
|
|
30177
31983
|
return {
|
|
30178
31984
|
getView: () => view,
|
|
30179
31985
|
subscribe(listener) {
|
|
@@ -30186,11 +31992,11 @@ function createTranscriptStore(replay) {
|
|
|
30186
31992
|
const next = projectEvent(view, event);
|
|
30187
31993
|
if (next === view) return;
|
|
30188
31994
|
view = next;
|
|
30189
|
-
|
|
31995
|
+
notify();
|
|
30190
31996
|
},
|
|
30191
31997
|
reset() {
|
|
30192
31998
|
view = createTranscriptView();
|
|
30193
|
-
|
|
31999
|
+
notify();
|
|
30194
32000
|
}
|
|
30195
32001
|
};
|
|
30196
32002
|
}
|
|
@@ -30219,12 +32025,13 @@ function watchSkills(ctx) {
|
|
|
30219
32025
|
let error;
|
|
30220
32026
|
const listeners = /* @__PURE__ */ new Set();
|
|
30221
32027
|
const reload = () => {
|
|
30222
|
-
const
|
|
30223
|
-
if (skills === void 0 ||
|
|
32028
|
+
const target = agent;
|
|
32029
|
+
if (skills === void 0 || target === void 0) return;
|
|
30224
32030
|
Promise.resolve().then(() => skills.list({
|
|
30225
|
-
cwd:
|
|
30226
|
-
scope:
|
|
32031
|
+
cwd: target.session.header.cwd,
|
|
32032
|
+
scope: target
|
|
30227
32033
|
})).then((summaries) => {
|
|
32034
|
+
if (agent !== target) return;
|
|
30228
32035
|
const next = toRows(summaries);
|
|
30229
32036
|
const unchanged = next.length === rows.length && next.every((row, index) => row.name === rows[index]?.name);
|
|
30230
32037
|
rows = next;
|
|
@@ -30233,6 +32040,7 @@ function watchSkills(ctx) {
|
|
|
30233
32040
|
if (unchanged && !recovered) return;
|
|
30234
32041
|
for (const listener of listeners) listener();
|
|
30235
32042
|
}).catch((cause) => {
|
|
32043
|
+
if (agent !== target) return;
|
|
30236
32044
|
rows = [...rows];
|
|
30237
32045
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
30238
32046
|
for (const listener of listeners) listener();
|
|
@@ -30393,6 +32201,13 @@ function resolvePreset(session) {
|
|
|
30393
32201
|
}
|
|
30394
32202
|
return session.header.agentPreset ?? "standard";
|
|
30395
32203
|
}
|
|
32204
|
+
/** Resolve a pre-session choice, or recompose an active blank Agent. */
|
|
32205
|
+
async function selectPreset(service, agent, presetId) {
|
|
32206
|
+
if (agent !== void 0) return switchPreset(service, agent, presetId);
|
|
32207
|
+
const preset = await service.resolve(presetId);
|
|
32208
|
+
if (preset.broken !== void 0) throw new Error(preset.broken);
|
|
32209
|
+
return preset;
|
|
32210
|
+
}
|
|
30396
32211
|
/** Recompose atomically from the caller's perspective, logging only success. */
|
|
30397
32212
|
async function switchPreset(service, agent, presetId) {
|
|
30398
32213
|
if (!isBlankSession(agent.session.events)) throw new Error("mode is locked after the first turn; use /new <mode>");
|
|
@@ -30401,6 +32216,51 @@ async function switchPreset(service, agent, presetId) {
|
|
|
30401
32216
|
return preset;
|
|
30402
32217
|
}
|
|
30403
32218
|
//#endregion
|
|
32219
|
+
//#region src/permissions.ts
|
|
32220
|
+
/** Read the optional Harness service without importing its runtime package. */
|
|
32221
|
+
function permissionPresetsFrom(ctx) {
|
|
32222
|
+
return ctx.get("permissionPresets");
|
|
32223
|
+
}
|
|
32224
|
+
/** Effective label for either an active session or the not-yet-created first one. */
|
|
32225
|
+
function effectivePermission(service, session, pending) {
|
|
32226
|
+
return session === void 0 ? pending ?? service.defaultPreset : service.current(session.events);
|
|
32227
|
+
}
|
|
32228
|
+
/** Validate a preset and write it only when a durable session already exists. */
|
|
32229
|
+
function selectPermission(service, session, preset) {
|
|
32230
|
+
service.resolve(preset);
|
|
32231
|
+
if (session !== void 0) service.set(session, preset);
|
|
32232
|
+
return preset;
|
|
32233
|
+
}
|
|
32234
|
+
/** Cycle table order from the active, pending, or configured-default value. */
|
|
32235
|
+
function cyclePermission(service, session, pending) {
|
|
32236
|
+
if (service.names.length === 0) return "";
|
|
32237
|
+
const at = service.names.indexOf(effectivePermission(service, session, pending));
|
|
32238
|
+
const next = service.names[(at + 1) % service.names.length] ?? "";
|
|
32239
|
+
return next === "" ? "" : selectPermission(service, session, next);
|
|
32240
|
+
}
|
|
32241
|
+
/** Materialize a pre-session choice after Harness creates the first session. */
|
|
32242
|
+
function applyPendingPermission(service, session, pending) {
|
|
32243
|
+
if (pending !== void 0 && effectivePermission(service, session, void 0) !== pending) selectPermission(service, session, pending);
|
|
32244
|
+
}
|
|
32245
|
+
/**
|
|
32246
|
+
* List every switchable preset for the /permission panel, table order kept.
|
|
32247
|
+
* Description lookup failures degrade to an undocumented row, never a failed
|
|
32248
|
+
* panel load — `optionOf` rejects names its table no longer knows.
|
|
32249
|
+
*/
|
|
32250
|
+
function listPermissionRows(service) {
|
|
32251
|
+
return service.names.map((id) => {
|
|
32252
|
+
if (service.optionOf === void 0) return { id };
|
|
32253
|
+
try {
|
|
32254
|
+
return {
|
|
32255
|
+
id,
|
|
32256
|
+
description: service.optionOf(id)?.description
|
|
32257
|
+
};
|
|
32258
|
+
} catch {
|
|
32259
|
+
return { id };
|
|
32260
|
+
}
|
|
32261
|
+
});
|
|
32262
|
+
}
|
|
32263
|
+
//#endregion
|
|
30404
32264
|
//#region src/plugin-inventory.ts
|
|
30405
32265
|
const PHASES = {
|
|
30406
32266
|
0: "pending",
|
|
@@ -30429,16 +32289,46 @@ function listPluginRows(ctx) {
|
|
|
30429
32289
|
//#endregion
|
|
30430
32290
|
//#region src/session-directory.ts
|
|
30431
32291
|
/** Lightweight session-directory projection for the /resume picker. */
|
|
32292
|
+
/** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
|
|
32293
|
+
const CASE_INSENSITIVE_FS = process.platform === "win32" || process.platform === "darwin";
|
|
32294
|
+
/** True when the header describes a subagent conversation (durable lineage). */
|
|
32295
|
+
function isSubagentSession(header) {
|
|
32296
|
+
return header.origin === "subagent" || header.parentSession !== void 0;
|
|
32297
|
+
}
|
|
32298
|
+
function comparablePath(value) {
|
|
32299
|
+
const resolved = resolve(value);
|
|
32300
|
+
return CASE_INSENSITIVE_FS ? resolved.toLowerCase() : resolved;
|
|
32301
|
+
}
|
|
32302
|
+
/** Platform-consistent path equality for session cwd comparisons. */
|
|
30432
32303
|
function samePath(left, right) {
|
|
30433
32304
|
if (left === void 0) return false;
|
|
30434
|
-
return
|
|
32305
|
+
return comparablePath(left) === comparablePath(right);
|
|
32306
|
+
}
|
|
32307
|
+
/**
|
|
32308
|
+
* Unique header match by exact id or unique id prefix (root and subagent
|
|
32309
|
+
* headers alike); the caller applies any lineage gate.
|
|
32310
|
+
* @param headers - the persisted headers.
|
|
32311
|
+
* @param wanted - the id or id prefix.
|
|
32312
|
+
* @returns the uniquely matched header.
|
|
32313
|
+
* @throws when nothing matches or the prefix is ambiguous.
|
|
32314
|
+
*/
|
|
32315
|
+
function matchSessionId(headers, wanted) {
|
|
32316
|
+
const exact = headers.filter((header) => header.id === wanted);
|
|
32317
|
+
const matches = exact.length > 0 ? exact : headers.filter((header) => header.id.startsWith(wanted));
|
|
32318
|
+
if (matches.length === 0) throw new Error(`no persisted session matches "${wanted}"`);
|
|
32319
|
+
if (matches.length > 1) throw new Error(`session prefix "${wanted}" is ambiguous (${matches.length} matches): use more of the id`);
|
|
32320
|
+
return matches[0];
|
|
32321
|
+
}
|
|
32322
|
+
/** The newest persisted ROOT session pinned to this cwd, or undefined. */
|
|
32323
|
+
function newestRootForCwd(headers, cwd) {
|
|
32324
|
+
return headers.filter((header) => !isSubagentSession(header) && samePath(header.cwd, cwd)).sort((left, right) => right.createdAt - left.createdAt)[0];
|
|
30435
32325
|
}
|
|
30436
32326
|
/** Filter/sort header-only records. No session log is loaded here. */
|
|
30437
32327
|
function projectSessionRows(records, options) {
|
|
30438
32328
|
const needle = options.query.trim().toLowerCase();
|
|
30439
|
-
return records.filter((record) => options.sessions === "all" || record.header
|
|
32329
|
+
return records.filter((record) => options.sessions === "all" || !isSubagentSession(record.header)).filter((record) => options.cwd === "all" || samePath(record.header.cwd, options.currentCwd)).map((record) => {
|
|
30440
32330
|
const cwd = record.header.cwd ?? "";
|
|
30441
|
-
const subagent = record.header
|
|
32331
|
+
const subagent = isSubagentSession(record.header);
|
|
30442
32332
|
return {
|
|
30443
32333
|
id: record.header.id,
|
|
30444
32334
|
createdAt: record.header.createdAt,
|
|
@@ -30510,6 +32400,46 @@ function gitBranch(cwd) {
|
|
|
30510
32400
|
}
|
|
30511
32401
|
}
|
|
30512
32402
|
/**
|
|
32403
|
+
* Reduce a session id to a filename-safe /export default-name suffix. Session
|
|
32404
|
+
* ids are normally minted `session-<uuid>`, but `--session` accepts arbitrary
|
|
32405
|
+
* user text: path separators must never leak into the default export filename
|
|
32406
|
+
* (which would escape the session cwd).
|
|
32407
|
+
* @param id - the session id.
|
|
32408
|
+
* @returns at most the last 8 filename-safe characters.
|
|
32409
|
+
*/
|
|
32410
|
+
function exportSessionIdSuffix(id) {
|
|
32411
|
+
return id.replace(/[^a-zA-Z0-9._-]/gu, "_").slice(-8);
|
|
32412
|
+
}
|
|
32413
|
+
/**
|
|
32414
|
+
* Run the ordered quit cleanup, then request exit. Every step rejection is
|
|
32415
|
+
* contained (reported through `onError`) so a failed flush or dispose never
|
|
32416
|
+
* skips the remaining cleanup; the exit request is always reached exactly
|
|
32417
|
+
* once.
|
|
32418
|
+
* @param steps - the cleanup steps in dependency order (settle the visible
|
|
32419
|
+
* session, await the final in-flight composition, await durable recall).
|
|
32420
|
+
* @param exit - the terminal exit request (code 0).
|
|
32421
|
+
* @param onError - optional failure sink; called once per failing step and
|
|
32422
|
+
* itself contained, so a throwing sink cannot abort the sequence.
|
|
32423
|
+
* @returns the names of the steps that started, in order (for tests).
|
|
32424
|
+
*/
|
|
32425
|
+
async function runQuitSequence(steps, exit, onError) {
|
|
32426
|
+
const started = [];
|
|
32427
|
+
for (const step of steps) {
|
|
32428
|
+
started.push(step.name);
|
|
32429
|
+
try {
|
|
32430
|
+
await step.run();
|
|
32431
|
+
} catch (error) {
|
|
32432
|
+
try {
|
|
32433
|
+
onError?.(step.name, error);
|
|
32434
|
+
} catch {}
|
|
32435
|
+
}
|
|
32436
|
+
}
|
|
32437
|
+
try {
|
|
32438
|
+
exit(0);
|
|
32439
|
+
} catch {}
|
|
32440
|
+
return started;
|
|
32441
|
+
}
|
|
32442
|
+
/**
|
|
30513
32443
|
* Resolve the invocation's target session against the persisted headers.
|
|
30514
32444
|
* @param startup - the parsed startup flags.
|
|
30515
32445
|
* @param persistence - the persistence service; required for resume/latest.
|
|
@@ -30523,28 +32453,30 @@ async function resolveTarget(startup, persistence, cwd) {
|
|
|
30523
32453
|
resume: false,
|
|
30524
32454
|
mode: startup.mode
|
|
30525
32455
|
};
|
|
30526
|
-
if (startup.kind === "named")
|
|
30527
|
-
|
|
30528
|
-
|
|
30529
|
-
|
|
30530
|
-
|
|
32456
|
+
if (startup.kind === "named") {
|
|
32457
|
+
if (persistence !== void 0) {
|
|
32458
|
+
if ((await persistence.list()).some((header) => header.id === startup.sessionId)) throw new Error(`session "${startup.sessionId}" already exists; use --resume to continue it`);
|
|
32459
|
+
}
|
|
32460
|
+
return {
|
|
32461
|
+
sessionId: startup.sessionId,
|
|
32462
|
+
resume: false,
|
|
32463
|
+
mode: startup.mode
|
|
32464
|
+
};
|
|
32465
|
+
}
|
|
30531
32466
|
if (persistence === void 0) throw new Error("cannot resolve the requested session: session persistence is not configured");
|
|
30532
32467
|
const headers = await persistence.list();
|
|
30533
32468
|
if (startup.kind === "resume") {
|
|
30534
|
-
const
|
|
30535
|
-
|
|
30536
|
-
const matches = exact.length > 0 ? exact : headers.filter((header) => header.id.startsWith(wanted));
|
|
30537
|
-
if (matches.length === 0) throw new Error(`no persisted session matches "${wanted}"`);
|
|
30538
|
-
if (matches.length > 1) throw new Error(`session prefix "${wanted}" is ambiguous (${matches.length} matches): use more of the id`);
|
|
32469
|
+
const matched = matchSessionId(headers, startup.sessionId);
|
|
32470
|
+
if (isSubagentSession(matched)) throw new Error("subagent conversations are read-only; resume a root session");
|
|
30539
32471
|
return {
|
|
30540
|
-
sessionId:
|
|
32472
|
+
sessionId: matched.id,
|
|
30541
32473
|
resume: true
|
|
30542
32474
|
};
|
|
30543
32475
|
}
|
|
30544
|
-
const
|
|
30545
|
-
if (
|
|
32476
|
+
const newest = newestRootForCwd(headers, cwd);
|
|
32477
|
+
if (newest === void 0) throw new Error(`no persisted session for this directory (${cwd}); start one without --continue`);
|
|
30546
32478
|
return {
|
|
30547
|
-
sessionId:
|
|
32479
|
+
sessionId: newest.id,
|
|
30548
32480
|
resume: true
|
|
30549
32481
|
};
|
|
30550
32482
|
}
|
|
@@ -30583,12 +32515,13 @@ async function run(ctx, startup, io) {
|
|
|
30583
32515
|
const defaults = defaultModel.currentSelection();
|
|
30584
32516
|
const presets = agentPresetsFrom(ctx);
|
|
30585
32517
|
if (presets === void 0) throw new Error("agent preset service is unavailable; check the dsh-code bundle patch");
|
|
32518
|
+
const permissionPresets = permissionPresetsFrom(ctx);
|
|
30586
32519
|
const lazy = startup.kind === "fresh" && startup.mode === void 0;
|
|
30587
32520
|
/** Prepare a complete next session before disturbing the currently visible one. */
|
|
30588
32521
|
const prepare = async (next) => {
|
|
30589
32522
|
const nextCwd = next.cwd ?? cwd;
|
|
30590
32523
|
const selectionState = pendingSelection === void 0 ? {} : { picked: pendingSelection };
|
|
30591
|
-
let mode = next.mode;
|
|
32524
|
+
let mode = next.resume ? next.mode : next.mode ?? pendingMode;
|
|
30592
32525
|
if (!next.resume) mode = (await presets.resolve(mode)).id;
|
|
30593
32526
|
const setup = async (agentCtx) => {
|
|
30594
32527
|
const sessionPreset = next.resume ? resolvePreset(agentCtx.agent.session) : mode;
|
|
@@ -30609,6 +32542,7 @@ async function run(ctx, startup, io) {
|
|
|
30609
32542
|
provider: defaults.provider,
|
|
30610
32543
|
model: defaults.model
|
|
30611
32544
|
},
|
|
32545
|
+
signal: quitAbort.signal,
|
|
30612
32546
|
setup
|
|
30613
32547
|
}) : await agents.create({
|
|
30614
32548
|
sessionId: SessionId(next.sessionId),
|
|
@@ -30620,9 +32554,11 @@ async function run(ctx, startup, io) {
|
|
|
30620
32554
|
provider: defaults.provider,
|
|
30621
32555
|
model: defaults.model
|
|
30622
32556
|
},
|
|
32557
|
+
signal: quitAbort.signal,
|
|
30623
32558
|
setup
|
|
30624
32559
|
});
|
|
30625
32560
|
const session = handle.agent.session;
|
|
32561
|
+
if (!next.resume && permissionPresets !== void 0) applyPendingPermission(permissionPresets, session, pendingPermission);
|
|
30626
32562
|
const sessionCwd = session.header.cwd ?? nextCwd;
|
|
30627
32563
|
return {
|
|
30628
32564
|
handle,
|
|
@@ -30642,6 +32578,44 @@ async function run(ctx, startup, io) {
|
|
|
30642
32578
|
let mentions = createMentions(ctx, void 0, cwd);
|
|
30643
32579
|
/** Explicit model pick made before any session exists (a bare launch). */
|
|
30644
32580
|
let pendingSelection;
|
|
32581
|
+
/** Agent preset selected before the first session exists. */
|
|
32582
|
+
let pendingMode;
|
|
32583
|
+
/** Ordered pre-session preset resolutions; first composition awaits them. */
|
|
32584
|
+
let pendingModeWork = Promise.resolve();
|
|
32585
|
+
/** Permission preset selected before the first session exists. */
|
|
32586
|
+
let pendingPermission;
|
|
32587
|
+
/**
|
|
32588
|
+
* Monotonic session epoch: bumped on every successful activation, on every
|
|
32589
|
+
* first-session creation, and on quit. Async callbacks (mention prepares,
|
|
32590
|
+
* command executions) capture it at call time and drop their result when it
|
|
32591
|
+
* changed, so a stale callback can never deliver to an agent that is no
|
|
32592
|
+
* longer on screen.
|
|
32593
|
+
*/
|
|
32594
|
+
let epoch = 0;
|
|
32595
|
+
/** Aborted on quit: an in-flight agent composition (create/resume) races this signal. */
|
|
32596
|
+
const quitAbort = new AbortController();
|
|
32597
|
+
/** In-flight mention-prepare / command-execute controllers, aborted on any session transition. */
|
|
32598
|
+
const pendingControllers = /* @__PURE__ */ new Set();
|
|
32599
|
+
const abortPendingControllers = () => {
|
|
32600
|
+
for (const controller of [...pendingControllers]) {
|
|
32601
|
+
pendingControllers.delete(controller);
|
|
32602
|
+
controller.abort();
|
|
32603
|
+
}
|
|
32604
|
+
};
|
|
32605
|
+
/** The in-flight session-composition turn (create/resume/activate), if any. */
|
|
32606
|
+
let composing;
|
|
32607
|
+
/**
|
|
32608
|
+
* Run one session composition exclusively: concurrent compositions wait
|
|
32609
|
+
* their turn, so a bare-launch first-session creation and a /resume
|
|
32610
|
+
* activation can never compose agents in parallel (the loser would leak its
|
|
32611
|
+
* agent or mis-deliver). Errors propagate to the caller; the shared slot
|
|
32612
|
+
* always continues.
|
|
32613
|
+
*/
|
|
32614
|
+
const compose = (work) => {
|
|
32615
|
+
const turn = (composing ?? Promise.resolve()).catch(() => {}).then(work);
|
|
32616
|
+
composing = turn.catch(() => {});
|
|
32617
|
+
return turn;
|
|
32618
|
+
};
|
|
30645
32619
|
if (!lazy) {
|
|
30646
32620
|
const prepared = await prepare(await resolveTarget(startup, persistence, cwd));
|
|
30647
32621
|
active = prepared;
|
|
@@ -30696,16 +32670,12 @@ async function run(ctx, startup, io) {
|
|
|
30696
32670
|
} catch {
|
|
30697
32671
|
inputHistory = [];
|
|
30698
32672
|
}
|
|
32673
|
+
/** Serialized history writes: each submission rewrites the latest in-memory snapshot. */
|
|
32674
|
+
let historyWriteChain = Promise.resolve();
|
|
30699
32675
|
const recordHistory = (text) => {
|
|
30700
32676
|
if (text === "") return;
|
|
30701
|
-
inputHistory = [...inputHistory, text].slice(-
|
|
30702
|
-
|
|
30703
|
-
try {
|
|
30704
|
-
current = readFileSync(historyPath, "utf8");
|
|
30705
|
-
} catch {
|
|
30706
|
-
current = "";
|
|
30707
|
-
}
|
|
30708
|
-
mkdir(dirname(historyPath), { recursive: true }).then(() => writeFile(historyPath, appendHistoryContent(current, text), "utf8")).catch((writeError) => {
|
|
32677
|
+
inputHistory = [...inputHistory, text].slice(-100);
|
|
32678
|
+
historyWriteChain = historyWriteChain.then(() => mkdir(dirname(historyPath), { recursive: true })).then(() => writeFile(historyPath, serializeHistoryList(inputHistory), "utf8")).catch((writeError) => {
|
|
30709
32679
|
bridge.notify("history save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
|
|
30710
32680
|
});
|
|
30711
32681
|
};
|
|
@@ -30724,30 +32694,40 @@ async function run(ctx, startup, io) {
|
|
|
30724
32694
|
if (quitting) return;
|
|
30725
32695
|
quitting = true;
|
|
30726
32696
|
switchQueue.cancel();
|
|
32697
|
+
abortPendingControllers();
|
|
32698
|
+
quitAbort.abort();
|
|
32699
|
+
epoch += 1;
|
|
30727
32700
|
off();
|
|
30728
32701
|
mountRef.current?.unmount();
|
|
30729
32702
|
const currentSession = session;
|
|
30730
32703
|
const currentActive = active;
|
|
30731
|
-
|
|
30732
|
-
|
|
30733
|
-
|
|
30734
|
-
|
|
30735
|
-
|
|
30736
|
-
|
|
30737
|
-
|
|
30738
|
-
|
|
30739
|
-
|
|
30740
|
-
|
|
30741
|
-
|
|
32704
|
+
const report = (name, error) => {
|
|
32705
|
+
internals.stderr.write(`dsh: quit ${name} failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
32706
|
+
};
|
|
32707
|
+
runQuitSequence([
|
|
32708
|
+
...currentSession === void 0 || currentActive === void 0 ? [] : [{
|
|
32709
|
+
name: "flush",
|
|
32710
|
+
run: async () => {
|
|
32711
|
+
await sessions.flush(currentSession);
|
|
32712
|
+
}
|
|
32713
|
+
}, {
|
|
32714
|
+
name: "dispose",
|
|
32715
|
+
run: () => currentActive.handle.dispose()
|
|
32716
|
+
}],
|
|
32717
|
+
{
|
|
32718
|
+
name: "composing",
|
|
32719
|
+
run: () => composing ?? Promise.resolve()
|
|
32720
|
+
},
|
|
32721
|
+
{
|
|
32722
|
+
name: "history",
|
|
32723
|
+
run: () => historyWriteChain
|
|
32724
|
+
}
|
|
32725
|
+
], io.exit, report);
|
|
30742
32726
|
};
|
|
30743
32727
|
/** Run one slash line through the command registry (closed namespace). */
|
|
30744
32728
|
const runSlash = (line) => {
|
|
30745
32729
|
const currentAgent = agent;
|
|
30746
32730
|
if (currentAgent === void 0) return;
|
|
30747
|
-
if (line.startsWith("/mode ")) {
|
|
30748
|
-
switchModeAction(line.slice(6).trim());
|
|
30749
|
-
return;
|
|
30750
|
-
}
|
|
30751
32731
|
if (line.startsWith("/resume ")) {
|
|
30752
32732
|
requestResume(line.slice(8).trim());
|
|
30753
32733
|
return;
|
|
@@ -30758,7 +32738,14 @@ async function run(ctx, startup, io) {
|
|
|
30758
32738
|
return;
|
|
30759
32739
|
}
|
|
30760
32740
|
const controller = new AbortController();
|
|
32741
|
+
const atEpoch = epoch;
|
|
32742
|
+
pendingControllers.add(controller);
|
|
32743
|
+
const finish = () => {
|
|
32744
|
+
pendingControllers.delete(controller);
|
|
32745
|
+
};
|
|
30761
32746
|
Promise.resolve().then(() => registry.execute(currentAgent, line, controller.signal)).then((execution) => {
|
|
32747
|
+
finish();
|
|
32748
|
+
if (epoch !== atEpoch || agent !== currentAgent) return;
|
|
30762
32749
|
if (execution === void 0) try {
|
|
30763
32750
|
currentAgent.followup(createUserMessage({
|
|
30764
32751
|
content: [{
|
|
@@ -30771,6 +32758,8 @@ async function run(ctx, startup, io) {
|
|
|
30771
32758
|
bridge.notify(`command fallback failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
30772
32759
|
}
|
|
30773
32760
|
}, (error) => {
|
|
32761
|
+
finish();
|
|
32762
|
+
if (epoch !== atEpoch || agent !== currentAgent) return;
|
|
30774
32763
|
bridge.notify(`command failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
30775
32764
|
});
|
|
30776
32765
|
};
|
|
@@ -30789,7 +32778,9 @@ async function run(ctx, startup, io) {
|
|
|
30789
32778
|
bridge.notify(`invalid session reference: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
30790
32779
|
return;
|
|
30791
32780
|
}
|
|
32781
|
+
const atEpoch = epoch;
|
|
30792
32782
|
const deliver = (readable, context) => {
|
|
32783
|
+
if (epoch !== atEpoch || agent !== currentAgent) return;
|
|
30793
32784
|
try {
|
|
30794
32785
|
if (context !== void 0) currentAgent.inject(context);
|
|
30795
32786
|
const message = createUserMessage({
|
|
@@ -30810,49 +32801,78 @@ async function run(ctx, startup, io) {
|
|
|
30810
32801
|
return;
|
|
30811
32802
|
}
|
|
30812
32803
|
const controller = new AbortController();
|
|
32804
|
+
pendingControllers.add(controller);
|
|
30813
32805
|
currentMentions.prepare(parsed, controller.signal).then((prepared) => {
|
|
32806
|
+
pendingControllers.delete(controller);
|
|
30814
32807
|
deliver(prepared.text, prepared.additionalContext);
|
|
30815
32808
|
}, (error) => {
|
|
30816
|
-
|
|
32809
|
+
pendingControllers.delete(controller);
|
|
32810
|
+
if (controller.signal.aborted || epoch !== atEpoch) return;
|
|
30817
32811
|
bridge.notify(`session reference failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
30818
32812
|
});
|
|
30819
32813
|
};
|
|
30820
32814
|
const pendingInputs = [];
|
|
30821
|
-
let creating;
|
|
32815
|
+
let creating = false;
|
|
30822
32816
|
const ensureSession = (mode) => {
|
|
30823
|
-
if (creating
|
|
30824
|
-
creating =
|
|
30825
|
-
|
|
30826
|
-
|
|
30827
|
-
|
|
30828
|
-
|
|
30829
|
-
|
|
30830
|
-
|
|
30831
|
-
|
|
30832
|
-
|
|
32817
|
+
if (creating) return;
|
|
32818
|
+
creating = true;
|
|
32819
|
+
compose(async () => {
|
|
32820
|
+
try {
|
|
32821
|
+
await pendingModeWork;
|
|
32822
|
+
if (session !== void 0) {
|
|
32823
|
+
const queued = pendingInputs.splice(0);
|
|
32824
|
+
for (const item of queued) deliverLine(item.text, item.mode);
|
|
32825
|
+
return;
|
|
32826
|
+
}
|
|
32827
|
+
const next = await prepare({
|
|
32828
|
+
sessionId: `session-${randomUUID()}`,
|
|
32829
|
+
resume: false,
|
|
32830
|
+
...mode === void 0 ? {} : { mode }
|
|
32831
|
+
});
|
|
32832
|
+
if (quitting) {
|
|
32833
|
+
next.handle.dispose().catch(() => {});
|
|
32834
|
+
return;
|
|
32835
|
+
}
|
|
32836
|
+
active = next;
|
|
32837
|
+
agent = next.agent;
|
|
32838
|
+
session = next.session;
|
|
32839
|
+
store = next.store;
|
|
32840
|
+
mentions = next.mentions;
|
|
32841
|
+
pendingMode = void 0;
|
|
32842
|
+
pendingPermission = void 0;
|
|
32843
|
+
commands.setAgent(agent);
|
|
32844
|
+
skills.setAgent(agent);
|
|
32845
|
+
process.stdout.write("\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H");
|
|
32846
|
+
renderCurrent();
|
|
32847
|
+
abortPendingControllers();
|
|
32848
|
+
epoch += 1;
|
|
32849
|
+
const queued = pendingInputs.splice(0);
|
|
32850
|
+
for (const item of queued) deliverLine(item.text, item.mode);
|
|
32851
|
+
} finally {
|
|
32852
|
+
creating = false;
|
|
30833
32853
|
}
|
|
30834
|
-
|
|
30835
|
-
agent = next.agent;
|
|
30836
|
-
session = next.session;
|
|
30837
|
-
store = next.store;
|
|
30838
|
-
mentions = next.mentions;
|
|
30839
|
-
commands.setAgent(agent);
|
|
30840
|
-
skills.setAgent(agent);
|
|
30841
|
-
process.stdout.write("\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H");
|
|
30842
|
-
renderCurrent();
|
|
30843
|
-
const queued = pendingInputs.splice(0);
|
|
30844
|
-
for (const item of queued) deliverLine(item.text, item.mode);
|
|
30845
|
-
})().catch((error) => {
|
|
32854
|
+
}).catch((error) => {
|
|
30846
32855
|
pendingInputs.length = 0;
|
|
30847
32856
|
bridge.notify(`session creation failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
30848
|
-
}).finally(() => {
|
|
30849
|
-
creating = void 0;
|
|
30850
32857
|
});
|
|
30851
32858
|
};
|
|
30852
32859
|
/** Deliver one readable line to the agent, expanding session mentions first. */
|
|
30853
32860
|
const send = (text, mode) => {
|
|
30854
32861
|
const line = text.trim();
|
|
30855
32862
|
if (line === "") return;
|
|
32863
|
+
if (line.startsWith("/mode ")) {
|
|
32864
|
+
switchModeAction(line.slice(6).trim()).then((selected) => bridge.notify(`mode → ${selected}`), (error) => bridge.notify(`mode switch failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
|
|
32865
|
+
return;
|
|
32866
|
+
}
|
|
32867
|
+
if (line.startsWith("/permission ")) {
|
|
32868
|
+
try {
|
|
32869
|
+
const selected = setPermissionAction(line.slice(12).trim());
|
|
32870
|
+
bridge.notify(`permission → ${selected}`);
|
|
32871
|
+
} catch (error) {
|
|
32872
|
+
bridge.notify(`permission change failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
32873
|
+
}
|
|
32874
|
+
return;
|
|
32875
|
+
}
|
|
30856
32876
|
if (session === void 0) {
|
|
30857
32877
|
pendingInputs.push({
|
|
30858
32878
|
text: line,
|
|
@@ -30887,23 +32907,33 @@ async function run(ctx, startup, io) {
|
|
|
30887
32907
|
return false;
|
|
30888
32908
|
}
|
|
30889
32909
|
};
|
|
32910
|
+
/** Select one permission preset before the first session or on the active one. */
|
|
32911
|
+
const setPermissionAction = (id) => {
|
|
32912
|
+
if (permissionPresets === void 0 || permissionPresets.names.length === 0) throw new Error("permission presets are not mounted in this composition");
|
|
32913
|
+
if (id === "") throw new Error("usage: /permission <preset>");
|
|
32914
|
+
const selected = selectPermission(permissionPresets, session, id);
|
|
32915
|
+
if (session === void 0) {
|
|
32916
|
+
pendingPermission = selected;
|
|
32917
|
+
renderCurrent();
|
|
32918
|
+
}
|
|
32919
|
+
return selected;
|
|
32920
|
+
};
|
|
30890
32921
|
/**
|
|
30891
|
-
* Cycle to the next permission preset (Shift+Tab
|
|
30892
|
-
*
|
|
30893
|
-
*
|
|
32922
|
+
* Cycle to the next permission preset (Shift+Tab). Before the first session,
|
|
32923
|
+
* the choice remains process-local and is materialized when Harness creates
|
|
32924
|
+
* that session; afterwards the canonical service writes durable events.
|
|
30894
32925
|
*/
|
|
30895
|
-
const cyclePermission = () => {
|
|
30896
|
-
if (
|
|
30897
|
-
const service = ctx.get("permissionPresets");
|
|
30898
|
-
if (service === void 0 || service.names.length === 0) {
|
|
32926
|
+
const cyclePermission$1 = () => {
|
|
32927
|
+
if (permissionPresets === void 0 || permissionPresets.names.length === 0) {
|
|
30899
32928
|
bridge.notify("permission presets are not mounted in this composition", "warning");
|
|
30900
32929
|
return "";
|
|
30901
32930
|
}
|
|
30902
|
-
const at = service.names.indexOf(service.current(session.events));
|
|
30903
|
-
const next = service.names[(at + 1) % service.names.length] ?? "";
|
|
30904
|
-
if (next === "") return "";
|
|
30905
32931
|
try {
|
|
30906
|
-
|
|
32932
|
+
const next = cyclePermission(permissionPresets, session, pendingPermission);
|
|
32933
|
+
if (session === void 0 && next !== "") {
|
|
32934
|
+
pendingPermission = next;
|
|
32935
|
+
renderCurrent();
|
|
32936
|
+
}
|
|
30907
32937
|
return next;
|
|
30908
32938
|
} catch (error) {
|
|
30909
32939
|
bridge.notify(`permission change failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
@@ -30934,7 +32964,7 @@ async function run(ctx, startup, io) {
|
|
|
30934
32964
|
}
|
|
30935
32965
|
const wanted = argument.trim();
|
|
30936
32966
|
const sessionCwd = session.header.cwd ?? cwd;
|
|
30937
|
-
const defaultName = `dsh-session-${session.id
|
|
32967
|
+
const defaultName = `dsh-session-${exportSessionIdSuffix(session.id)}.md`;
|
|
30938
32968
|
const target = wanted === "" ? join(sessionCwd, defaultName) : /^[a-zA-Z]:[\\/]/u.test(wanted) || wanted.startsWith("/") ? wanted : join(sessionCwd, wanted);
|
|
30939
32969
|
const markdown = buildExportMarkdown(store.getView(), session.id);
|
|
30940
32970
|
try {
|
|
@@ -30977,56 +33007,78 @@ async function run(ctx, startup, io) {
|
|
|
30977
33007
|
const switchModeAction = async (id) => {
|
|
30978
33008
|
if (id === "") throw new Error("usage: /mode <preset>");
|
|
30979
33009
|
const currentAgent = agent;
|
|
30980
|
-
|
|
30981
|
-
|
|
30982
|
-
|
|
30983
|
-
|
|
33010
|
+
if (currentAgent === void 0) {
|
|
33011
|
+
const choice = pendingModeWork.then(async () => {
|
|
33012
|
+
const preset = await selectPreset(presets, void 0, id);
|
|
33013
|
+
if (agent === void 0) {
|
|
33014
|
+
pendingMode = preset.id;
|
|
33015
|
+
renderCurrent();
|
|
33016
|
+
}
|
|
33017
|
+
return preset.id;
|
|
33018
|
+
});
|
|
33019
|
+
pendingModeWork = choice.then(() => {}, () => {});
|
|
33020
|
+
return choice;
|
|
33021
|
+
}
|
|
33022
|
+
const preset = await selectPreset(presets, currentAgent, id);
|
|
33023
|
+
if (active === void 0) throw new Error("active Agent has no session state");
|
|
33024
|
+
active.mode = preset.id;
|
|
30984
33025
|
commands.setAgent(currentAgent);
|
|
30985
33026
|
skills.setAgent(currentAgent);
|
|
30986
33027
|
renderCurrent();
|
|
30987
33028
|
return preset.id;
|
|
30988
33029
|
};
|
|
30989
|
-
const activate =
|
|
30990
|
-
|
|
30991
|
-
|
|
30992
|
-
|
|
30993
|
-
|
|
30994
|
-
|
|
30995
|
-
|
|
30996
|
-
|
|
30997
|
-
|
|
30998
|
-
|
|
30999
|
-
|
|
31000
|
-
|
|
31001
|
-
|
|
31002
|
-
|
|
31003
|
-
|
|
31004
|
-
|
|
31005
|
-
|
|
31006
|
-
|
|
31007
|
-
|
|
31008
|
-
|
|
31009
|
-
|
|
31010
|
-
|
|
31011
|
-
|
|
31012
|
-
|
|
31013
|
-
|
|
31014
|
-
|
|
31015
|
-
|
|
31016
|
-
|
|
31017
|
-
|
|
31018
|
-
|
|
31019
|
-
|
|
31020
|
-
|
|
31021
|
-
|
|
31022
|
-
|
|
31023
|
-
|
|
31024
|
-
|
|
31025
|
-
|
|
31026
|
-
|
|
31027
|
-
|
|
31028
|
-
|
|
31029
|
-
|
|
33030
|
+
const activate = (nextTarget) => {
|
|
33031
|
+
if (quitting) return Promise.resolve();
|
|
33032
|
+
return compose(async () => {
|
|
33033
|
+
const previous = active;
|
|
33034
|
+
const next = await prepare(nextTarget);
|
|
33035
|
+
if (quitting) {
|
|
33036
|
+
await next.handle.dispose().catch(() => {});
|
|
33037
|
+
return;
|
|
33038
|
+
}
|
|
33039
|
+
active = next;
|
|
33040
|
+
agent = next.agent;
|
|
33041
|
+
session = next.session;
|
|
33042
|
+
store = next.store;
|
|
33043
|
+
mentions = next.mentions;
|
|
33044
|
+
pendingMode = void 0;
|
|
33045
|
+
pendingPermission = void 0;
|
|
33046
|
+
commands.setAgent(agent);
|
|
33047
|
+
skills.setAgent(agent);
|
|
33048
|
+
try {
|
|
33049
|
+
process.stdout.write("\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H");
|
|
33050
|
+
renderCurrent();
|
|
33051
|
+
} catch (error) {
|
|
33052
|
+
active = previous;
|
|
33053
|
+
agent = previous?.agent;
|
|
33054
|
+
session = previous?.session;
|
|
33055
|
+
store = previous === void 0 ? createTranscriptStore() : previous.store;
|
|
33056
|
+
mentions = previous === void 0 ? createMentions(ctx, void 0, cwd) : previous.mentions;
|
|
33057
|
+
if (agent !== void 0) commands.setAgent(agent);
|
|
33058
|
+
if (agent !== void 0) skills.setAgent(agent);
|
|
33059
|
+
await next.handle.dispose();
|
|
33060
|
+
if (!quitting) renderCurrent();
|
|
33061
|
+
throw error;
|
|
33062
|
+
}
|
|
33063
|
+
abortPendingControllers();
|
|
33064
|
+
epoch += 1;
|
|
33065
|
+
if (previous === void 0) {
|
|
33066
|
+
bridge.notify(`${next.resumed ? "resumed" : "created"} ${next.session.id.slice(-12)} · mode ${next.mode}`);
|
|
33067
|
+
return;
|
|
33068
|
+
}
|
|
33069
|
+
let cleanupWarning;
|
|
33070
|
+
try {
|
|
33071
|
+
await sessions.flush(previous.session);
|
|
33072
|
+
} catch (error) {
|
|
33073
|
+
cleanupWarning = `previous session flush failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
33074
|
+
}
|
|
33075
|
+
try {
|
|
33076
|
+
await previous.handle.dispose();
|
|
33077
|
+
} catch (error) {
|
|
33078
|
+
cleanupWarning = `${cleanupWarning === void 0 ? "" : `${cleanupWarning}; `}previous agent release failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
33079
|
+
}
|
|
33080
|
+
bridge.notify(cleanupWarning === void 0 ? `${next.resumed ? "resumed" : "created"} ${next.session.id.slice(-12)} · mode ${next.mode}` : `switched to ${next.session.id.slice(-12)}, but ${cleanupWarning}`, cleanupWarning === void 0 ? "info" : "warning");
|
|
33081
|
+
});
|
|
31030
33082
|
};
|
|
31031
33083
|
const switchQueue = new SessionSwitchQueue(async (request) => {
|
|
31032
33084
|
if (!quitting) await activate(request.target);
|
|
@@ -31052,9 +33104,10 @@ async function run(ctx, startup, io) {
|
|
|
31052
33104
|
const matches = exact.length > 0 ? exact : records.filter((record) => record.header.id.startsWith(wanted));
|
|
31053
33105
|
if (matches.length === 0) throw new Error(`no session matches "${wanted}"`);
|
|
31054
33106
|
if (matches.length > 1) throw new Error(`session prefix "${wanted}" is ambiguous (${matches.length} matches)`);
|
|
31055
|
-
|
|
31056
|
-
if (
|
|
31057
|
-
|
|
33107
|
+
const matched = matches[0];
|
|
33108
|
+
if (isSubagentSession(matched.header)) throw new Error("subagent conversations are read-only; resume a root session");
|
|
33109
|
+
if (session !== void 0 && agents.get(SessionId(matched.header.id)) !== void 0 && matched.header.id !== session.id) throw new Error("that session is already live in another owner");
|
|
33110
|
+
return matched.header.id;
|
|
31058
33111
|
};
|
|
31059
33112
|
const requestResume = (wanted) => {
|
|
31060
33113
|
resolveResumeId(wanted).then((id) => {
|
|
@@ -31102,8 +33155,10 @@ async function run(ctx, startup, io) {
|
|
|
31102
33155
|
};
|
|
31103
33156
|
const appElement = () => {
|
|
31104
33157
|
const sessionCwd = session?.header.cwd ?? cwd;
|
|
31105
|
-
const
|
|
33158
|
+
const currentView = store.getView();
|
|
33159
|
+
const model = currentView.model !== "" ? currentView.model : pendingSelection !== void 0 ? `${pendingSelection.provider}/${pendingSelection.model}` : `${defaults.provider}/${defaults.model}`;
|
|
31106
33160
|
const effort = resolveEffectiveSelection(active?.selection.picked ?? pendingSelection, session?.requestHeader()?.config, defaults).reasoningEffort;
|
|
33161
|
+
const permission = permissionPresets === void 0 ? currentView.permission : effectivePermission(permissionPresets, session, pendingPermission);
|
|
31107
33162
|
return (0, import_react.createElement)(App, {
|
|
31108
33163
|
key: session?.id ?? "pending",
|
|
31109
33164
|
store,
|
|
@@ -31118,19 +33173,27 @@ async function run(ctx, startup, io) {
|
|
|
31118
33173
|
branch: gitBranch(sessionCwd),
|
|
31119
33174
|
sessionId: session === void 0 ? "" : session.id.slice(-8),
|
|
31120
33175
|
resumed: active?.resumed ?? false,
|
|
31121
|
-
mode: active?.mode ??
|
|
33176
|
+
mode: active?.mode ?? pendingMode ?? presets.defaultId,
|
|
33177
|
+
permission,
|
|
31122
33178
|
dispatch,
|
|
31123
33179
|
steer,
|
|
31124
33180
|
interrupt,
|
|
31125
33181
|
quit,
|
|
31126
33182
|
loadModels: () => loadModelDirectory(ctx),
|
|
33183
|
+
loadModelProviders: () => loadProviderSettings(ctx),
|
|
33184
|
+
subscribeModelProviders: (listener) => subscribeProviderSettings(ctx, listener),
|
|
33185
|
+
saveModelProviderCredential: (target, key) => saveProviderCredential(ctx, target, key),
|
|
33186
|
+
unsetModelProviderCredential: (target) => unsetProviderCredential(ctx, target),
|
|
33187
|
+
removeModelProvider: (target) => removeProviderSettings(ctx, target),
|
|
31127
33188
|
loadMentions: (query, signal) => mentions.candidates(query, signal),
|
|
31128
|
-
cyclePermission,
|
|
33189
|
+
cyclePermission: cyclePermission$1,
|
|
33190
|
+
setPermission: setPermissionAction,
|
|
31129
33191
|
selectModel,
|
|
31130
33192
|
exportTranscript,
|
|
31131
33193
|
renameTitle,
|
|
31132
33194
|
loadPresets: () => presets.list(),
|
|
31133
33195
|
switchMode: switchModeAction,
|
|
33196
|
+
loadPermissions: () => permissionPresets === void 0 ? Promise.reject(/* @__PURE__ */ new Error("permission presets are not mounted in this composition")) : Promise.resolve(listPermissionRows(permissionPresets)),
|
|
31134
33197
|
createSession,
|
|
31135
33198
|
loadSessions,
|
|
31136
33199
|
loadSessionTranscript,
|
|
@@ -31194,4 +33257,4 @@ function apply(ctx, config) {
|
|
|
31194
33257
|
});
|
|
31195
33258
|
}
|
|
31196
33259
|
//#endregion
|
|
31197
|
-
export { Config, apply, inject, name };
|
|
33260
|
+
export { Config, apply, exportSessionIdSuffix, inject, name, resolveTarget, runQuitSequence };
|