dsh-code 1.4.0 → 1.5.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 +10 -8
- package/README.md +10 -8
- package/lib/index.mjs +2001 -1688
- package/lib/session-query.mjs +18 -1
- package/lib/types/app.d.ts +21 -15
- package/lib/types/composer.d.ts +1 -1
- package/lib/types/locales/en.d.ts +4 -4
- package/lib/types/render/inspector.d.ts +8 -0
- package/lib/types/render/projection-events.d.ts +33 -0
- package/lib/types/render/transcript-viewport.d.ts +32 -0
- package/lib/types/runner/session-io.d.ts +11 -0
- package/lib/types/session-query.d.ts +11 -0
- package/package.json +3 -2
- package/src/app.ts +286 -123
- package/src/composer.ts +6 -6
- package/src/locales/en.ts +4 -4
- package/src/locales/zh.ts +4 -4
- package/src/render/inspector.ts +12 -0
- package/src/render/lines.ts +45 -18
- package/src/render/projection-events.ts +109 -0
- package/src/render/projection.ts +11 -2
- package/src/render/tool-preview.ts +35 -2
- package/src/render/transcript-viewport.ts +61 -0
- package/src/render/usage.ts +1 -2
- package/src/runner/session-io.ts +43 -4
- package/src/session-query.ts +22 -0
package/lib/index.mjs
CHANGED
|
@@ -24336,6 +24336,15 @@ function panelAccent(id, border, title = border) {
|
|
|
24336
24336
|
}
|
|
24337
24337
|
//#endregion
|
|
24338
24338
|
//#region src/render/inspector.ts
|
|
24339
|
+
/**
|
|
24340
|
+
* Remove assistant settlements that contain reasoning but no final response.
|
|
24341
|
+
* They remain in the durable transcript and in the Ctrl/Alt+R fold; excluding
|
|
24342
|
+
* them only from Ctrl+O navigation prevents short progress thoughts from
|
|
24343
|
+
* appearing as a long run of misleading `reply` entries.
|
|
24344
|
+
*/
|
|
24345
|
+
function inspectableTranscriptEntries(entries) {
|
|
24346
|
+
return entries.filter((entry) => !(entry.kind === "assistant" && entry.text.trim() === "" && entry.reasoning.trim() !== ""));
|
|
24347
|
+
}
|
|
24339
24348
|
/** Composer (3) + two-row status chrome, plus one optional fixed-height local notice row. */
|
|
24340
24349
|
const INSPECTOR_CHROME_ROWS = 6;
|
|
24341
24350
|
/** One transcript-to-composer gutter, collapsed on short terminals. */
|
|
@@ -24895,7 +24904,7 @@ const en = {
|
|
|
24895
24904
|
"cmd.theme": "switch the color theme",
|
|
24896
24905
|
"cmd.language": "switch the interface language (/language [en|zh])",
|
|
24897
24906
|
"cmd.rainbow": "reroll or pin the rainbow palette (/rainbow [seed])",
|
|
24898
|
-
"cmd.animation": "toggle
|
|
24907
|
+
"cmd.animation": "toggle decorative animations; shimmer and activity stay live (/animation [on|off])",
|
|
24899
24908
|
"cmd.history": "search and recall past prompts",
|
|
24900
24909
|
"cmd.queue": "manage messages queued for the next turn",
|
|
24901
24910
|
"cmd.usage": "show this session token usage",
|
|
@@ -25031,12 +25040,12 @@ const en = {
|
|
|
25031
25040
|
"notice.imageCancelled": "image submission cancelled",
|
|
25032
25041
|
"notice.themeConfigUnreadable": "theme config unreadable, using dark: {message}",
|
|
25033
25042
|
"notice.languageConfigUnreadable": "language config unreadable, using english: {message}",
|
|
25034
|
-
"notice.animationsConfigUnreadable": "
|
|
25043
|
+
"notice.animationsConfigUnreadable": "decorative animation config unreadable, effects stay on: {message}",
|
|
25035
25044
|
"notice.statuslineConfigUnreadable": "statusline config unreadable, using defaults: {message}",
|
|
25036
25045
|
"notice.sessionSearchUnavailable": "session search is unavailable in this deployment",
|
|
25037
25046
|
"notice.subagentsReadOnly": "subagent conversations are read-only",
|
|
25038
25047
|
"notice.statuslineSaveFailed": "statusline save failed: {message}",
|
|
25039
|
-
"notice.animationsSaveFailed": "
|
|
25048
|
+
"notice.animationsSaveFailed": "decorative animation save failed: {message}",
|
|
25040
25049
|
"notice.commandRegistryMissing": "no command registry is mounted in this composition",
|
|
25041
25050
|
"notice.commandFallbackFailed": "command fallback failed: {message}",
|
|
25042
25051
|
"notice.commandFailed": "command failed: {message}",
|
|
@@ -25087,7 +25096,7 @@ const en = {
|
|
|
25087
25096
|
"notice.commandAttachments": "commands cannot carry attachments; the line will be sent to the model as a prompt",
|
|
25088
25097
|
"notice.copyFailed": "copy failed: {message}",
|
|
25089
25098
|
"notice.vscodeKeysFailed": "vscode-keys failed: {message}",
|
|
25090
|
-
"notice.animationState": "animations {state}",
|
|
25099
|
+
"notice.animationState": "decorative animations {state}; activity indicators stay live",
|
|
25091
25100
|
"notice.deleteFailed": "delete failed: {message}",
|
|
25092
25101
|
"notice.modelChangedPlaceholder": "model → {model} · image history will be sent as text placeholders",
|
|
25093
25102
|
"notice.modelNextStep": "model → next step uses {model}",
|
|
@@ -25398,7 +25407,7 @@ const zh = {
|
|
|
25398
25407
|
"cmd.theme": "切换配色主题",
|
|
25399
25408
|
"cmd.language": "切换界面语言(/language [en|zh])",
|
|
25400
25409
|
"cmd.rainbow": "重掷或指定彩虹配色(/rainbow [seed])",
|
|
25401
|
-
"cmd.animation": "
|
|
25410
|
+
"cmd.animation": "开关装饰动画,shimmer 与活动指示始终保留(/animation [on|off])",
|
|
25402
25411
|
"cmd.history": "搜索并复用历史提示词",
|
|
25403
25412
|
"cmd.queue": "管理等待下一轮的消息",
|
|
25404
25413
|
"cmd.usage": "查看本会话的 token 用量",
|
|
@@ -25534,12 +25543,12 @@ const zh = {
|
|
|
25534
25543
|
"notice.imageCancelled": "图片提交已取消",
|
|
25535
25544
|
"notice.themeConfigUnreadable": "主题配置无法读取,使用暗色:{message}",
|
|
25536
25545
|
"notice.languageConfigUnreadable": "语言配置无法读取,使用英文:{message}",
|
|
25537
|
-
"notice.animationsConfigUnreadable": "
|
|
25546
|
+
"notice.animationsConfigUnreadable": "装饰动画配置无法读取,保持开启:{message}",
|
|
25538
25547
|
"notice.statuslineConfigUnreadable": "状态栏配置无法读取,使用默认:{message}",
|
|
25539
25548
|
"notice.sessionSearchUnavailable": "此部署未启用会话检索",
|
|
25540
25549
|
"notice.subagentsReadOnly": "子代理会话为只读",
|
|
25541
25550
|
"notice.statuslineSaveFailed": "状态栏保存失败:{message}",
|
|
25542
|
-
"notice.animationsSaveFailed": "
|
|
25551
|
+
"notice.animationsSaveFailed": "装饰动画保存失败:{message}",
|
|
25543
25552
|
"notice.commandRegistryMissing": "当前组合未加载命令注册表",
|
|
25544
25553
|
"notice.commandFallbackFailed": "命令回退失败:{message}",
|
|
25545
25554
|
"notice.commandFailed": "命令执行失败:{message}",
|
|
@@ -25590,7 +25599,7 @@ const zh = {
|
|
|
25590
25599
|
"notice.commandAttachments": "命令不能携带附件;该行将作为提示发送给模型",
|
|
25591
25600
|
"notice.copyFailed": "复制失败:{message}",
|
|
25592
25601
|
"notice.vscodeKeysFailed": "vscode-keys 失败:{message}",
|
|
25593
|
-
"notice.animationState": "
|
|
25602
|
+
"notice.animationState": "装饰动画{state};活动指示保持开启",
|
|
25594
25603
|
"notice.deleteFailed": "删除失败:{message}",
|
|
25595
25604
|
"notice.modelChangedPlaceholder": "模型 → {model} · 历史图片将以文本占位符发送",
|
|
25596
25605
|
"notice.modelNextStep": "模型 → 下一步使用 {model}",
|
|
@@ -26501,6 +26510,94 @@ function useTerminalTitle(title, options = {}) {
|
|
|
26501
26510
|
}, [stdout, title]);
|
|
26502
26511
|
}
|
|
26503
26512
|
//#endregion
|
|
26513
|
+
//#region src/render/projection-events.ts
|
|
26514
|
+
/** Events folded directly into `TranscriptView`. */
|
|
26515
|
+
const TRANSCRIPT_SESSION_EVENT_TYPES = [
|
|
26516
|
+
"agent/inbox/spliced",
|
|
26517
|
+
"assistant/attempt",
|
|
26518
|
+
"assistant/message",
|
|
26519
|
+
"command/done",
|
|
26520
|
+
"command/run",
|
|
26521
|
+
"compaction/end",
|
|
26522
|
+
"compaction/prune",
|
|
26523
|
+
"compaction/summary",
|
|
26524
|
+
"goal/change",
|
|
26525
|
+
"llm/retry",
|
|
26526
|
+
"llm/retry-started",
|
|
26527
|
+
"permission/preset",
|
|
26528
|
+
"plan/mode",
|
|
26529
|
+
"request/context",
|
|
26530
|
+
"request/header",
|
|
26531
|
+
"sandbox/mode",
|
|
26532
|
+
"schedule/change",
|
|
26533
|
+
"session/title",
|
|
26534
|
+
"step/start",
|
|
26535
|
+
"system/message",
|
|
26536
|
+
"todo/write",
|
|
26537
|
+
"tool-workflow/agent-end",
|
|
26538
|
+
"tool-workflow/agent-start",
|
|
26539
|
+
"tool-workflow/run-end",
|
|
26540
|
+
"tool-workflow/run-start",
|
|
26541
|
+
"tool/call",
|
|
26542
|
+
"tool/ptc-dispatch",
|
|
26543
|
+
"tool/ptc-dispatch-start",
|
|
26544
|
+
"tool/result",
|
|
26545
|
+
"turn/end",
|
|
26546
|
+
"turn/start",
|
|
26547
|
+
"user/message"
|
|
26548
|
+
];
|
|
26549
|
+
/**
|
|
26550
|
+
* Events consumed by another TUI adapter rather than the transcript fold.
|
|
26551
|
+
* Keeping these separate documents that they are visible behavior, not
|
|
26552
|
+
* accidental no-ops.
|
|
26553
|
+
*/
|
|
26554
|
+
const OTHER_SURFACE_SESSION_EVENT_TYPES = [
|
|
26555
|
+
"agent-preset/selected",
|
|
26556
|
+
"model/selection",
|
|
26557
|
+
"subagent/catalog"
|
|
26558
|
+
];
|
|
26559
|
+
/**
|
|
26560
|
+
* Kernel events deliberately omitted from terminal presentation. Their
|
|
26561
|
+
* model-visible consequences arrive through projected message/tool/session
|
|
26562
|
+
* events, while audit-only facts do not need a second transcript row.
|
|
26563
|
+
*/
|
|
26564
|
+
const IGNORED_SESSION_EVENT_TYPES = [
|
|
26565
|
+
"approval/asked",
|
|
26566
|
+
"approval/decided",
|
|
26567
|
+
"approval/policy",
|
|
26568
|
+
"compaction/start",
|
|
26569
|
+
"deliverables/presented",
|
|
26570
|
+
"feedback/message-delete",
|
|
26571
|
+
"feedback/message-put",
|
|
26572
|
+
"feedback/record",
|
|
26573
|
+
"hook/invoked",
|
|
26574
|
+
"hook/result",
|
|
26575
|
+
"session-log-deepseek/delivery-accepted",
|
|
26576
|
+
"session/end-seed",
|
|
26577
|
+
"session/title-llm-request",
|
|
26578
|
+
"step/end",
|
|
26579
|
+
"subagent/descriptor",
|
|
26580
|
+
"subagent/model-selection-policy",
|
|
26581
|
+
"team/member",
|
|
26582
|
+
"team/message/delivered",
|
|
26583
|
+
"team/message/queued",
|
|
26584
|
+
"team/task",
|
|
26585
|
+
"web/deepseek-search-llm-request"
|
|
26586
|
+
];
|
|
26587
|
+
const EVENT_DISPOSITIONS = new Map([
|
|
26588
|
+
...TRANSCRIPT_SESSION_EVENT_TYPES.map((type) => [type, "transcript"]),
|
|
26589
|
+
...OTHER_SURFACE_SESSION_EVENT_TYPES.map((type) => [type, "other-surface"]),
|
|
26590
|
+
...IGNORED_SESSION_EVENT_TYPES.map((type) => [type, "ignored"])
|
|
26591
|
+
]);
|
|
26592
|
+
/**
|
|
26593
|
+
* Classify a durable event without rejecting merge-extensible plugin events.
|
|
26594
|
+
* `unknown` remains a runtime no-op for forward/plugin compatibility; the
|
|
26595
|
+
* known-event coverage test is what makes a first-party addition fail CI.
|
|
26596
|
+
*/
|
|
26597
|
+
function sessionEventDisposition(type) {
|
|
26598
|
+
return EVENT_DISPOSITIONS.get(type) ?? "unknown";
|
|
26599
|
+
}
|
|
26600
|
+
//#endregion
|
|
26504
26601
|
//#region src/render/tool-preview.ts
|
|
26505
26602
|
/**
|
|
26506
26603
|
* Bounded preview line for a tool invocation's raw JSON arguments: the first
|
|
@@ -26517,10 +26614,12 @@ function useTerminalTitle(title, options = {}) {
|
|
|
26517
26614
|
const PREVIEW_KEYS = [
|
|
26518
26615
|
"command",
|
|
26519
26616
|
"cmd",
|
|
26617
|
+
"file_path",
|
|
26520
26618
|
"description",
|
|
26521
26619
|
"path",
|
|
26522
26620
|
"pattern",
|
|
26523
|
-
"query"
|
|
26621
|
+
"query",
|
|
26622
|
+
"name"
|
|
26524
26623
|
];
|
|
26525
26624
|
/**
|
|
26526
26625
|
* Raw arguments longer than this are skipped without parsing and fall back
|
|
@@ -26532,6 +26631,28 @@ const MAX_PARSE_CHARS = 4096;
|
|
|
26532
26631
|
function boundedRawPreview(args) {
|
|
26533
26632
|
return args.length > 80 ? `${args.slice(0, 77)}...` : args;
|
|
26534
26633
|
}
|
|
26634
|
+
/** Escape a fixed preview key before embedding it in a bounded regex. */
|
|
26635
|
+
function regexKey(key) {
|
|
26636
|
+
return key.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
26637
|
+
}
|
|
26638
|
+
/**
|
|
26639
|
+
* Recover one complete JSON string property from the bounded head/tail of a
|
|
26640
|
+
* large argument object. Write/edit payloads commonly put a multi-kilobyte
|
|
26641
|
+
* `content` first and `file_path` last; parsing the whole payload is forbidden
|
|
26642
|
+
* on the render path, but the short trailing path is still safe to decode.
|
|
26643
|
+
*/
|
|
26644
|
+
function boundedStringProperty(args, key) {
|
|
26645
|
+
const slices = [args.slice(0, MAX_PARSE_CHARS), args.slice(-4096)];
|
|
26646
|
+
const pattern = new RegExp(`"${regexKey(key)}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`, "u");
|
|
26647
|
+
for (const slice of slices) {
|
|
26648
|
+
const literal = pattern.exec(slice)?.[1];
|
|
26649
|
+
if (literal === void 0) continue;
|
|
26650
|
+
try {
|
|
26651
|
+
const value = JSON.parse(literal);
|
|
26652
|
+
if (typeof value === "string" && value !== "") return value;
|
|
26653
|
+
} catch {}
|
|
26654
|
+
}
|
|
26655
|
+
}
|
|
26535
26656
|
/**
|
|
26536
26657
|
* Resolve one bounded preview for raw tool arguments.
|
|
26537
26658
|
* @param args - raw JSON arguments string as the model produced it.
|
|
@@ -26540,7 +26661,13 @@ function boundedRawPreview(args) {
|
|
|
26540
26661
|
*/
|
|
26541
26662
|
function toolArgumentsPreview(args, toolName) {
|
|
26542
26663
|
if (args === "") return toolName;
|
|
26543
|
-
if (args.length > MAX_PARSE_CHARS)
|
|
26664
|
+
if (args.length > MAX_PARSE_CHARS) {
|
|
26665
|
+
for (const key of PREVIEW_KEYS) {
|
|
26666
|
+
const value = boundedStringProperty(args, key);
|
|
26667
|
+
if (value !== void 0) return value;
|
|
26668
|
+
}
|
|
26669
|
+
return boundedRawPreview(args);
|
|
26670
|
+
}
|
|
26544
26671
|
try {
|
|
26545
26672
|
const parsed = JSON.parse(args);
|
|
26546
26673
|
if (parsed !== null && typeof parsed === "object") {
|
|
@@ -27139,6 +27266,7 @@ function replayProjectEvent(acc, event) {
|
|
|
27139
27266
|
}
|
|
27140
27267
|
};
|
|
27141
27268
|
}
|
|
27269
|
+
if (sessionEventDisposition(event.type) !== "transcript") return shadow.changed;
|
|
27142
27270
|
switch (event.type) {
|
|
27143
27271
|
case "user/message": {
|
|
27144
27272
|
const message = event.data;
|
|
@@ -27646,7 +27774,7 @@ function replayProjectEvent(acc, event) {
|
|
|
27646
27774
|
}));
|
|
27647
27775
|
return true;
|
|
27648
27776
|
}
|
|
27649
|
-
|
|
27777
|
+
throw new Error(`missing transcript replay projection for session event "${type}"`);
|
|
27650
27778
|
}
|
|
27651
27779
|
}
|
|
27652
27780
|
}
|
|
@@ -30209,13 +30337,13 @@ function reasoningLines(text, columns) {
|
|
|
30209
30337
|
* diff/read/web/raw rows align under them instead of floating two columns
|
|
30210
30338
|
* shallower.
|
|
30211
30339
|
*/
|
|
30212
|
-
function toolDetailLines(detail, columns) {
|
|
30340
|
+
function toolDetailLines(detail, columns, rawStyle) {
|
|
30213
30341
|
switch (detail.kind) {
|
|
30214
30342
|
case "diff": return detail.diffs.flatMap((diff) => [...prefixedTextLines(`${diff.path}${diff.truncated ? " (diff truncated)" : ""}`, columns, " ── ", "dim"), ...fillDiffLineBars(diff.lines.flatMap((line) => prefixedTextLines(`${line.mark}${line.text}`, columns, " ", line.mark === "+" ? "diffAdd" : line.mark === "-" ? "diffDel" : "dim")), columns)]);
|
|
30215
30343
|
case "read": return [...prefixedTextLines(`${detail.path} · lines ${detail.offset}-${detail.lines.length > 0 ? detail.lines[detail.lines.length - 1].number : detail.offset - 1} of ${detail.totalLines}${detail.truncated ? " (window truncated)" : ""}`, columns, " ── ", "dim"), ...detail.lines.flatMap((line) => prefixedTextLines(`${String(line.number).padStart(5, " ")} | ${line.text}`, columns, " ", "dim"))];
|
|
30216
30344
|
case "web-search": return [...detail.sources.flatMap((source) => [...prefixedStyledLines([lineSegment(source.title ?? source.url, "brand"), lineSegment(` - ${source.url}`, "dim")], columns, " ? "), ...source.snippet === "" ? [] : prefixedTextLines(source.snippet, columns, " ", "dim")]), ...prefixedTextLines(`${detail.sources.length} sources${detail.truncated ? " (capped)" : ""}`, columns, " ", "dim")];
|
|
30217
30345
|
case "web-fetch": return prefixedTextLines(`${detail.url} · HTTP ${detail.statusCode}`, columns, " ", "dim");
|
|
30218
|
-
case "raw": return [...prefixedTextLines(detail.text, columns, " ",
|
|
30346
|
+
case "raw": return [...prefixedTextLines(detail.text, columns, " ", rawStyle), ...prefixedTextLines(detail.truncated ? "… (output truncated)" : "(end of output)", columns, " ", rawStyle)];
|
|
30219
30347
|
default: return detail;
|
|
30220
30348
|
}
|
|
30221
30349
|
}
|
|
@@ -30322,10 +30450,14 @@ function subDispatchLines(entry, columns) {
|
|
|
30322
30450
|
const width = Math.max(1, Math.floor(columns));
|
|
30323
30451
|
const rows = entry.subs.flatMap((sub) => {
|
|
30324
30452
|
const mark = sub.state === "running" ? "●" : sub.state === "error" ? "⨯" : "⏺";
|
|
30325
|
-
const
|
|
30326
|
-
|
|
30327
|
-
|
|
30328
|
-
|
|
30453
|
+
const markStyle = sub.state === "running" ? "brand" : sub.state === "error" ? "error" : "success";
|
|
30454
|
+
return hangingStyledLines([
|
|
30455
|
+
lineSegment(`${mark} `, markStyle),
|
|
30456
|
+
lineSegment(sub.name, "brand"),
|
|
30457
|
+
lineSegment(sub.preview === "" ? "" : ` ${sub.preview}`, "dim"),
|
|
30458
|
+
lineSegment(sub.state === "error" && sub.summary !== "" ? ` · ${sub.summary}` : "", sub.state === "error" ? "error" : "dim"),
|
|
30459
|
+
lineSegment(subDispatchSeconds(sub.durationMs), "dim")
|
|
30460
|
+
], width, " ┆ ", "dim", " ┆ ", "dim");
|
|
30329
30461
|
});
|
|
30330
30462
|
if (entry.subsDropped === 0) return rows;
|
|
30331
30463
|
return [...rows, ...textLines(` ┆ … ${entry.subsDropped} earlier dispatch${entry.subsDropped === 1 ? "" : "es"}`, width, "dim")];
|
|
@@ -30375,18 +30507,32 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
|
|
|
30375
30507
|
const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
|
|
30376
30508
|
const markStyle = entry.state === "running" ? "brand" : entry.state === "error" ? "error" : "success";
|
|
30377
30509
|
const summaryStyle = entry.state === "error" ? "error" : "dim";
|
|
30378
|
-
const
|
|
30510
|
+
const invocation = [
|
|
30379
30511
|
...hangingStyledLines([
|
|
30380
30512
|
lineSegment(`[${entry.ordinal}] `, "dim"),
|
|
30381
30513
|
lineSegment(entry.name, "brand"),
|
|
30382
30514
|
lineSegment(entry.preview === "" ? "" : ` ${entry.preview}`, "dim")
|
|
30383
30515
|
], width, `${mark} `, markStyle, " ", "plain"),
|
|
30384
30516
|
...entry.prompt === "" ? [] : hangingTextLines(entry.prompt, width, " └ ", "dim", " "),
|
|
30385
|
-
...subDispatchLines(entry, width)
|
|
30386
|
-
|
|
30387
|
-
|
|
30517
|
+
...subDispatchLines(entry, width)
|
|
30518
|
+
];
|
|
30519
|
+
const summary = entry.summary === "" ? [] : hangingTextLines(entry.state === "error" ? `call ${entry.ordinal}: ${entry.summary}` : entry.summary, width, " ⎿ ", summaryStyle, " ", summaryStyle);
|
|
30520
|
+
if (!showToolDetails) {
|
|
30521
|
+
const rawFullySummarized = entry.detail?.kind === "raw" && !entry.detail.truncated && entry.detail.text.trim() === entry.summary.trim();
|
|
30522
|
+
const hiddenDetail = entry.detail !== void 0 && !rawFullySummarized ? textLines(" … output hidden · Ctrl/Alt+R", width, "dim").slice(0, 1) : [];
|
|
30523
|
+
return compactToolLines([
|
|
30524
|
+
...invocation,
|
|
30525
|
+
...summary,
|
|
30526
|
+
...hiddenDetail
|
|
30527
|
+
], width);
|
|
30528
|
+
}
|
|
30529
|
+
const detail = entry.detail === void 0 ? [] : toolDetailLines(entry.detail, width, entry.state === "error" ? "error" : "dim");
|
|
30530
|
+
const detailSupersedesSummary = entry.detail?.kind === "raw" || entry.detail?.kind === "read" || entry.detail?.kind === "diff";
|
|
30531
|
+
return [
|
|
30532
|
+
...invocation,
|
|
30533
|
+
...detailSupersedesSummary ? [] : summary,
|
|
30534
|
+
...detail
|
|
30388
30535
|
];
|
|
30389
|
-
return showToolDetails ? lines : compactToolLines(lines, width);
|
|
30390
30536
|
}
|
|
30391
30537
|
case "command": {
|
|
30392
30538
|
const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
|
|
@@ -30401,9 +30547,9 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
|
|
|
30401
30547
|
...hangingStyledLines([lineSegment("workflow ", "brand"), lineSegment(entry.name, "dim")], width, `${mark} `, markStyle, " ", "plain"),
|
|
30402
30548
|
...entry.members.flatMap((member) => {
|
|
30403
30549
|
const state = member.outcome === "running" ? "●" : member.outcome === "failed" ? "⨯" : member.outcome === "cancelled" ? "⏹" : "⏺";
|
|
30404
|
-
const
|
|
30550
|
+
const stateStyle = member.outcome === "running" ? "brand" : member.outcome === "failed" ? "error" : member.outcome === "cancelled" ? "dim" : "success";
|
|
30405
30551
|
const phase = member.phase === "" ? "" : ` [${member.phase}]`;
|
|
30406
|
-
return
|
|
30552
|
+
return hangingStyledLines([lineSegment(`${state} `, stateStyle), lineSegment(`${member.label}${phase}`, "dim")], width, " ┆ ", "dim", " ┆ ", "dim");
|
|
30407
30553
|
}),
|
|
30408
30554
|
...entry.membersDropped === 0 ? [] : textLines(` ┆ … ${entry.membersDropped} earlier member${entry.membersDropped === 1 ? "" : "s"}`, width, "dim")
|
|
30409
30555
|
];
|
|
@@ -30460,1743 +30606,1743 @@ function clampLiveAllocation(allocation, dynamicRows) {
|
|
|
30460
30606
|
};
|
|
30461
30607
|
}
|
|
30462
30608
|
//#endregion
|
|
30463
|
-
//#region src/render/
|
|
30464
|
-
/**
|
|
30465
|
-
|
|
30466
|
-
|
|
30467
|
-
|
|
30468
|
-
|
|
30469
|
-
|
|
30470
|
-
|
|
30471
|
-
|
|
30472
|
-
*
|
|
30473
|
-
* @module @deepseek-ai/dsh-tui/render/status
|
|
30474
|
-
*/
|
|
30609
|
+
//#region src/render/usage.ts
|
|
30610
|
+
/** Prompt-side tokens the provider billed: uncached input plus both cache buckets. */
|
|
30611
|
+
function billedInputTokens(totals) {
|
|
30612
|
+
return totals.uncachedInputTokens + totals.cacheReadTokens + totals.cacheWriteTokens;
|
|
30613
|
+
}
|
|
30614
|
+
/** Prompt plus completion tokens over the whole log. */
|
|
30615
|
+
function usageTotalTokens(totals) {
|
|
30616
|
+
return billedInputTokens(totals) + totals.outputTokens;
|
|
30617
|
+
}
|
|
30475
30618
|
/**
|
|
30476
|
-
*
|
|
30477
|
-
* @param
|
|
30478
|
-
* @returns
|
|
30619
|
+
* Cache-hit share of billed prompt-side input.
|
|
30620
|
+
* @param totals - cumulative provider-reported buckets.
|
|
30621
|
+
* @returns percent rounded to one decimal place, or null when nothing was billed.
|
|
30479
30622
|
*/
|
|
30480
|
-
function
|
|
30481
|
-
const
|
|
30482
|
-
|
|
30483
|
-
const whole = Math.round(s);
|
|
30484
|
-
const hours = Math.floor(whole / 3600);
|
|
30485
|
-
const minutes = Math.floor(whole % 3600 / 60);
|
|
30486
|
-
const seconds = whole % 60;
|
|
30487
|
-
if (hours > 0) return `${hours}h${minutes}m${seconds === 0 ? "" : `${seconds}s`}`;
|
|
30488
|
-
return `${minutes}m${seconds}s`;
|
|
30623
|
+
function usageCacheHitPercent(totals) {
|
|
30624
|
+
const billed = billedInputTokens(totals);
|
|
30625
|
+
return billed === 0 ? null : Math.round(totals.cacheReadTokens / billed * 1e3) / 10;
|
|
30489
30626
|
}
|
|
30490
30627
|
/**
|
|
30491
|
-
*
|
|
30492
|
-
*
|
|
30493
|
-
*
|
|
30494
|
-
* @
|
|
30628
|
+
* Split a durable log into COMPLETE turns. A turn still running has no
|
|
30629
|
+
* `turn/end` yet, and an unfinished attempt has no exact accounting, so the
|
|
30630
|
+
* trailing slice is dropped rather than guessed at.
|
|
30631
|
+
* @param events - the whole durable log, in log order.
|
|
30632
|
+
* @returns one slice per `turn/start`…`turn/end` span, oldest first.
|
|
30495
30633
|
*/
|
|
30496
|
-
function
|
|
30497
|
-
|
|
30498
|
-
|
|
30499
|
-
|
|
30634
|
+
function completedTurns(events) {
|
|
30635
|
+
const slices = [];
|
|
30636
|
+
let open;
|
|
30637
|
+
for (const event of events) {
|
|
30638
|
+
if (event.type === "turn/start") {
|
|
30639
|
+
open = {
|
|
30640
|
+
turn: event.data.turn,
|
|
30641
|
+
events: [event]
|
|
30642
|
+
};
|
|
30643
|
+
continue;
|
|
30644
|
+
}
|
|
30645
|
+
if (open === void 0) continue;
|
|
30646
|
+
open.events.push(event);
|
|
30647
|
+
if (event.type === "turn/end") {
|
|
30648
|
+
slices.push(open);
|
|
30649
|
+
open = void 0;
|
|
30650
|
+
}
|
|
30651
|
+
}
|
|
30652
|
+
return slices;
|
|
30500
30653
|
}
|
|
30501
30654
|
/**
|
|
30502
|
-
*
|
|
30503
|
-
*
|
|
30504
|
-
*
|
|
30505
|
-
* @
|
|
30506
|
-
*
|
|
30655
|
+
* Exact usage for every completed turn that can be proven.
|
|
30656
|
+
* @param events - the whole durable log, in log order.
|
|
30657
|
+
* @param derive - the meter's per-turn fold, injected so this module stays pure.
|
|
30658
|
+
* @returns one row per provable turn, oldest first; turns whose attempts did
|
|
30659
|
+
* not all report usage are omitted rather than estimated, and so is a turn
|
|
30660
|
+
* that billed nothing at all — an empty row is noise in a usage table.
|
|
30507
30661
|
*/
|
|
30508
|
-
function
|
|
30509
|
-
const
|
|
30510
|
-
|
|
30662
|
+
function turnUsages(events, derive) {
|
|
30663
|
+
const rows = [];
|
|
30664
|
+
for (const slice of completedTurns(events)) {
|
|
30665
|
+
const usage = derive(slice.events);
|
|
30666
|
+
if (usage === void 0 || usage.totalTokens === 0) continue;
|
|
30667
|
+
rows.push({
|
|
30668
|
+
turn: slice.turn,
|
|
30669
|
+
usage,
|
|
30670
|
+
model: turnModel(slice, usage)
|
|
30671
|
+
});
|
|
30672
|
+
}
|
|
30673
|
+
return rows;
|
|
30511
30674
|
}
|
|
30512
|
-
/**
|
|
30513
|
-
|
|
30514
|
-
|
|
30515
|
-
|
|
30516
|
-
|
|
30675
|
+
/** Pad to a column budget so labels line up across terminal widths. */
|
|
30676
|
+
function padColumns(text, columns) {
|
|
30677
|
+
return text + " ".repeat(Math.max(0, columns - visibleColumns(text)));
|
|
30678
|
+
}
|
|
30679
|
+
/** One `label` + `value` row of a summary block. */
|
|
30680
|
+
function pairLine(label, value, labelColumns, style = "plain") {
|
|
30681
|
+
const gap = " ".repeat(Math.max(1, labelColumns - visibleColumns(label)));
|
|
30682
|
+
return { segments: [lineSegment(" " + label + gap, "dim"), lineSegment(value, style)] };
|
|
30517
30683
|
}
|
|
30518
|
-
/** Occupancy at which the usage readout flips from brand blue to amber. */
|
|
30519
|
-
const CONTEXT_WARN_PERCENT = 90;
|
|
30520
|
-
/**
|
|
30521
|
-
* Narrowest bar width the drop ladder keeps before dropping the whole
|
|
30522
|
-
* context group: the bar shrinks to this floor first (the absolute readout
|
|
30523
|
-
* survives), and only past it does the readout degrade and the group go.
|
|
30524
|
-
*/
|
|
30525
|
-
const CONTEXT_MIN_WIDTH = 5;
|
|
30526
30684
|
/**
|
|
30527
|
-
*
|
|
30528
|
-
*
|
|
30529
|
-
*
|
|
30530
|
-
*
|
|
30531
|
-
*
|
|
30532
|
-
*
|
|
30533
|
-
* @
|
|
30534
|
-
* @param contextWindow - route capacity.
|
|
30535
|
-
* @param width - total bar columns.
|
|
30536
|
-
* @returns tone-split spans for the footer to paint.
|
|
30685
|
+
* Merge the per-turn rows by the model that billed them, biggest spender
|
|
30686
|
+
* first. A turn lands in exactly one group — the group named by every model it
|
|
30687
|
+
* used — so the sums stay additive and a mid-turn model switch never counts
|
|
30688
|
+
* the same tokens twice. Each bucket is merged over the turns that reported
|
|
30689
|
+
* it, and {@link ModelUsage.partial} records the ones that stayed silent.
|
|
30690
|
+
* @param turns - provable per-turn rows, oldest first.
|
|
30691
|
+
* @returns one row per model group.
|
|
30537
30692
|
*/
|
|
30538
|
-
function
|
|
30539
|
-
|
|
30540
|
-
const
|
|
30541
|
-
const
|
|
30542
|
-
|
|
30543
|
-
|
|
30544
|
-
|
|
30545
|
-
|
|
30546
|
-
|
|
30547
|
-
|
|
30548
|
-
|
|
30549
|
-
|
|
30550
|
-
|
|
30551
|
-
|
|
30693
|
+
function modelTotals(turns) {
|
|
30694
|
+
const groups = /* @__PURE__ */ new Map();
|
|
30695
|
+
for (const row of turns) groups.set(row.model, [...groups.get(row.model) ?? [], row]);
|
|
30696
|
+
const merged = [];
|
|
30697
|
+
for (const [model, rows] of groups) {
|
|
30698
|
+
const partial = [];
|
|
30699
|
+
const sum = (key) => {
|
|
30700
|
+
const reported = rows.flatMap((row) => row.usage[key] === void 0 ? [] : [row.usage[key]]);
|
|
30701
|
+
if (reported.length === 0) return void 0;
|
|
30702
|
+
if (reported.length < rows.length) partial.push(key);
|
|
30703
|
+
return reported.reduce((total, value) => total + value, 0);
|
|
30704
|
+
};
|
|
30705
|
+
const cacheReadTokens = sum("cacheReadTokens");
|
|
30706
|
+
const cacheWriteTokens = sum("cacheWriteTokens");
|
|
30707
|
+
const reasoningTokens = sum("reasoningTokens");
|
|
30708
|
+
merged.push({
|
|
30709
|
+
model,
|
|
30710
|
+
turns: rows.length,
|
|
30711
|
+
uncachedInputTokens: rows.reduce((sum, row) => sum + row.usage.uncachedInputTokens, 0),
|
|
30712
|
+
outputTokens: rows.reduce((sum, row) => sum + row.usage.outputTokens, 0),
|
|
30713
|
+
totalTokens: rows.reduce((sum, row) => sum + row.usage.totalTokens, 0),
|
|
30714
|
+
cacheReadTokens,
|
|
30715
|
+
cacheWriteTokens,
|
|
30716
|
+
reasoningTokens,
|
|
30717
|
+
partial
|
|
30718
|
+
});
|
|
30719
|
+
}
|
|
30720
|
+
return merged.sort((left, right) => left.model === "" ? 1 : right.model === "" ? -1 : right.totalTokens - left.totalTokens);
|
|
30721
|
+
}
|
|
30722
|
+
/** Cut one styled line to a column budget, keeping every style it fits. */
|
|
30723
|
+
function boundLine(line, width) {
|
|
30724
|
+
const segments = [];
|
|
30725
|
+
let left = width;
|
|
30726
|
+
for (const segment of line.segments) {
|
|
30727
|
+
if (left <= 0) break;
|
|
30728
|
+
const columns = visibleColumns(segment.text);
|
|
30729
|
+
if (columns <= left) {
|
|
30730
|
+
segments.push(segment);
|
|
30731
|
+
left -= columns;
|
|
30732
|
+
continue;
|
|
30733
|
+
}
|
|
30734
|
+
segments.push(lineSegment(truncateColumns(segment.text, left), segment.style));
|
|
30735
|
+
left = 0;
|
|
30736
|
+
}
|
|
30737
|
+
return { segments };
|
|
30738
|
+
}
|
|
30739
|
+
/** A block heading. */
|
|
30740
|
+
function heading(text) {
|
|
30741
|
+
return { segments: [lineSegment(text, "accentBold")] };
|
|
30742
|
+
}
|
|
30743
|
+
/** A dim explanatory or empty-state row. */
|
|
30744
|
+
function note(text) {
|
|
30745
|
+
return { segments: [lineSegment(" " + text, "dim")] };
|
|
30552
30746
|
}
|
|
30553
30747
|
/**
|
|
30554
|
-
*
|
|
30555
|
-
*
|
|
30556
|
-
*
|
|
30557
|
-
*
|
|
30558
|
-
*
|
|
30748
|
+
* Cache-hit share of one row's billed prompt side, or undefined when the row
|
|
30749
|
+
* has no cache read to report. An unreported cache-write bucket counts as
|
|
30750
|
+
* zero: the adapters that bill cache writes always report the bucket, so its
|
|
30751
|
+
* absence means the route never tracked one — the same reading the status
|
|
30752
|
+
* bar's own fold uses.
|
|
30559
30753
|
*/
|
|
30560
|
-
function
|
|
30561
|
-
|
|
30562
|
-
|
|
30563
|
-
|
|
30564
|
-
|
|
30565
|
-
spans.push(...contextBar(usedTokens, contextWindow, barWidth));
|
|
30566
|
-
if (readout === "none" || barWidth <= 0 || contextWindow <= 0) return spans;
|
|
30567
|
-
const used = Math.max(0, usedTokens);
|
|
30568
|
-
const percent = Math.round(used / contextWindow * 100);
|
|
30569
|
-
const text = readout === "full" ? `${formatTokens(used)}/${formatTokens(contextWindow)} ${percent}%` : `${percent}%`;
|
|
30570
|
-
spans.push({
|
|
30571
|
-
text: " ",
|
|
30572
|
-
tone: "label"
|
|
30573
|
-
}, {
|
|
30574
|
-
text,
|
|
30575
|
-
tone: percent >= CONTEXT_WARN_PERCENT ? "warn" : "value"
|
|
30576
|
-
});
|
|
30577
|
-
return spans;
|
|
30754
|
+
function hitShare(row) {
|
|
30755
|
+
if (row.cacheReadTokens === void 0) return void 0;
|
|
30756
|
+
const billed = row.uncachedInputTokens + row.cacheReadTokens + (row.cacheWriteTokens ?? 0);
|
|
30757
|
+
if (billed === 0) return void 0;
|
|
30758
|
+
return Math.round(row.cacheReadTokens / billed * 1e3) / 10;
|
|
30578
30759
|
}
|
|
30579
|
-
/** The
|
|
30580
|
-
|
|
30581
|
-
|
|
30582
|
-
|
|
30583
|
-
|
|
30584
|
-
|
|
30585
|
-
side: "left"
|
|
30586
|
-
},
|
|
30587
|
-
{
|
|
30588
|
-
id: "cwd",
|
|
30589
|
-
label: "cwd",
|
|
30590
|
-
description: "working-directory basename",
|
|
30591
|
-
side: "left"
|
|
30592
|
-
},
|
|
30593
|
-
{
|
|
30594
|
-
id: "mode",
|
|
30595
|
-
label: "mode",
|
|
30596
|
-
description: "agent preset composing the session",
|
|
30597
|
-
side: "left"
|
|
30598
|
-
},
|
|
30599
|
-
{
|
|
30600
|
-
id: "branch",
|
|
30601
|
-
label: "branch",
|
|
30602
|
-
description: "git branch inside a repository",
|
|
30603
|
-
side: "left"
|
|
30604
|
-
},
|
|
30605
|
-
{
|
|
30606
|
-
id: "context",
|
|
30607
|
-
label: "context",
|
|
30608
|
-
description: "context-window occupancy meter",
|
|
30609
|
-
side: "left"
|
|
30610
|
-
},
|
|
30611
|
-
{
|
|
30612
|
-
id: "permission",
|
|
30613
|
-
label: "permission",
|
|
30614
|
-
description: "permission preset badge with cycle hint",
|
|
30615
|
-
side: "right"
|
|
30616
|
-
},
|
|
30617
|
-
{
|
|
30618
|
-
id: "plan",
|
|
30619
|
-
label: "plan",
|
|
30620
|
-
description: "plan-mode state mark",
|
|
30621
|
-
side: "left"
|
|
30622
|
-
},
|
|
30760
|
+
/** The token columns, in display order (shared by both tables). */
|
|
30761
|
+
/** True when the row's bucket is a floor rather than a complete figure. */
|
|
30762
|
+
function isPartial(row, key) {
|
|
30763
|
+
return row.partial?.includes(key) === true;
|
|
30764
|
+
}
|
|
30765
|
+
const BUCKET_COLUMNS = [
|
|
30623
30766
|
{
|
|
30624
|
-
|
|
30625
|
-
|
|
30626
|
-
|
|
30627
|
-
side: "left"
|
|
30767
|
+
label: () => t("panel.usage.colTotal"),
|
|
30768
|
+
value: (row) => formatTokens(row.totalTokens),
|
|
30769
|
+
width: 9
|
|
30628
30770
|
},
|
|
30629
30771
|
{
|
|
30630
|
-
|
|
30631
|
-
|
|
30632
|
-
|
|
30633
|
-
side: "left"
|
|
30772
|
+
label: () => t("panel.usage.colUncached"),
|
|
30773
|
+
value: (row) => formatTokens(row.uncachedInputTokens),
|
|
30774
|
+
width: 9
|
|
30634
30775
|
},
|
|
30635
30776
|
{
|
|
30636
|
-
|
|
30637
|
-
|
|
30638
|
-
|
|
30639
|
-
side: "left"
|
|
30777
|
+
label: () => t("panel.usage.colCacheRead"),
|
|
30778
|
+
value: (row) => optionalTokens(row.cacheReadTokens, isPartial(row, "cacheReadTokens")),
|
|
30779
|
+
width: 9
|
|
30640
30780
|
},
|
|
30641
30781
|
{
|
|
30642
|
-
|
|
30643
|
-
|
|
30644
|
-
|
|
30645
|
-
side: "left"
|
|
30782
|
+
label: () => t("panel.usage.colCacheHit"),
|
|
30783
|
+
value: (row) => percent(hitShare(row), isPartial(row, "cacheReadTokens") || isPartial(row, "cacheWriteTokens")),
|
|
30784
|
+
width: 8
|
|
30646
30785
|
},
|
|
30647
30786
|
{
|
|
30648
|
-
|
|
30649
|
-
|
|
30650
|
-
|
|
30651
|
-
side: "left"
|
|
30787
|
+
label: () => t("panel.usage.colCacheWrite"),
|
|
30788
|
+
value: (row) => optionalTokens(row.cacheWriteTokens, isPartial(row, "cacheWriteTokens")),
|
|
30789
|
+
width: 9
|
|
30652
30790
|
},
|
|
30653
30791
|
{
|
|
30654
|
-
|
|
30655
|
-
|
|
30656
|
-
|
|
30657
|
-
side: "left"
|
|
30792
|
+
label: () => t("panel.usage.colOut"),
|
|
30793
|
+
value: (row) => formatTokens(row.outputTokens),
|
|
30794
|
+
width: 9
|
|
30658
30795
|
},
|
|
30659
30796
|
{
|
|
30660
|
-
|
|
30661
|
-
|
|
30662
|
-
|
|
30663
|
-
side: "left"
|
|
30797
|
+
label: () => t("panel.usage.colThink"),
|
|
30798
|
+
value: (row) => optionalTokens(row.reasoningTokens, isPartial(row, "reasoningTokens")),
|
|
30799
|
+
width: 9
|
|
30664
30800
|
}
|
|
30665
30801
|
];
|
|
30666
|
-
|
|
30667
|
-
|
|
30668
|
-
|
|
30669
|
-
description: "statusline.item.model.desc"
|
|
30670
|
-
},
|
|
30671
|
-
cwd: {
|
|
30672
|
-
label: "statusline.item.cwd",
|
|
30673
|
-
description: "statusline.item.cwd.desc"
|
|
30674
|
-
},
|
|
30675
|
-
mode: {
|
|
30676
|
-
label: "statusline.item.mode",
|
|
30677
|
-
description: "statusline.item.mode.desc"
|
|
30678
|
-
},
|
|
30679
|
-
branch: {
|
|
30680
|
-
label: "statusline.item.branch",
|
|
30681
|
-
description: "statusline.item.branch.desc"
|
|
30682
|
-
},
|
|
30683
|
-
context: {
|
|
30684
|
-
label: "statusline.item.context",
|
|
30685
|
-
description: "statusline.item.context.desc"
|
|
30686
|
-
},
|
|
30687
|
-
permission: {
|
|
30688
|
-
label: "statusline.item.permission",
|
|
30689
|
-
description: "statusline.item.permission.desc"
|
|
30690
|
-
},
|
|
30691
|
-
plan: {
|
|
30692
|
-
label: "statusline.item.plan",
|
|
30693
|
-
description: "statusline.item.plan.desc"
|
|
30694
|
-
},
|
|
30695
|
-
turns: {
|
|
30696
|
-
label: "statusline.item.turns",
|
|
30697
|
-
description: "statusline.item.turns.desc"
|
|
30698
|
-
},
|
|
30699
|
-
durations: {
|
|
30700
|
-
label: "statusline.item.durations",
|
|
30701
|
-
description: "statusline.item.durations.desc"
|
|
30702
|
-
},
|
|
30703
|
-
cache: {
|
|
30704
|
-
label: "statusline.item.cache",
|
|
30705
|
-
description: "statusline.item.cache.desc"
|
|
30706
|
-
},
|
|
30707
|
-
tokens: {
|
|
30708
|
-
label: "statusline.item.tokens",
|
|
30709
|
-
description: "statusline.item.tokens.desc"
|
|
30710
|
-
},
|
|
30711
|
-
title: {
|
|
30712
|
-
label: "statusline.item.title",
|
|
30713
|
-
description: "statusline.item.title.desc"
|
|
30714
|
-
},
|
|
30715
|
-
goal: {
|
|
30716
|
-
label: "statusline.item.goal",
|
|
30717
|
-
description: "statusline.item.goal.desc"
|
|
30718
|
-
},
|
|
30719
|
-
sandbox: {
|
|
30720
|
-
label: "statusline.item.sandbox",
|
|
30721
|
-
description: "statusline.item.sandbox.desc"
|
|
30722
|
-
}
|
|
30723
|
-
};
|
|
30724
|
-
/** Picker rows with labels/descriptions in the active interface language. */
|
|
30725
|
-
function localizedStatusItems() {
|
|
30726
|
-
return STATUS_ITEMS.map((item) => {
|
|
30727
|
-
const keys = STATUSLINE_ITEM_KEYS[item.id];
|
|
30728
|
-
return {
|
|
30729
|
-
...item,
|
|
30730
|
-
label: t(keys.label),
|
|
30731
|
-
description: t(keys.description)
|
|
30732
|
-
};
|
|
30733
|
-
});
|
|
30802
|
+
/** A percentage, or an em dash when the row cannot prove one; `+` for a floor. */
|
|
30803
|
+
function percent(value, partial = false) {
|
|
30804
|
+
return value === void 0 ? "—" : value + "%" + (partial ? "+" : "");
|
|
30734
30805
|
}
|
|
30735
30806
|
/**
|
|
30736
|
-
*
|
|
30737
|
-
*
|
|
30807
|
+
* A bucket as a table cell: an em dash when nothing reported it, and a `+`
|
|
30808
|
+
* when the figure counts only the turns of a group that did report it.
|
|
30738
30809
|
*/
|
|
30739
|
-
|
|
30810
|
+
function optionalTokens(value, partial = false) {
|
|
30811
|
+
if (value === void 0) return "—";
|
|
30812
|
+
return formatTokens(value) + (partial ? "+" : "");
|
|
30813
|
+
}
|
|
30814
|
+
/** What an unattributed turn or group is called in the tables. */
|
|
30815
|
+
function modelLabel(model) {
|
|
30816
|
+
return model === "" ? t("panel.usage.unknownModel") : model;
|
|
30817
|
+
}
|
|
30740
30818
|
/**
|
|
30741
|
-
*
|
|
30742
|
-
*
|
|
30743
|
-
*
|
|
30744
|
-
*
|
|
30745
|
-
* the bar degrades to its busy dot alone.
|
|
30746
|
-
* @param value - the raw parsed JSON value (expected string[]).
|
|
30747
|
-
* @returns the normalized ordered item list.
|
|
30819
|
+
* The model a turn's assistant messages came from: the one that produced most
|
|
30820
|
+
* of them, with a tie naming every model involved. This is the fallback for
|
|
30821
|
+
* turns whose meter `routes` were withheld, and it reads only what the log
|
|
30822
|
+
* recorded — nothing is inferred from the current selection.
|
|
30748
30823
|
*/
|
|
30749
|
-
function
|
|
30750
|
-
|
|
30751
|
-
const
|
|
30752
|
-
|
|
30753
|
-
|
|
30754
|
-
|
|
30824
|
+
function dominantModel(events) {
|
|
30825
|
+
const counts = /* @__PURE__ */ new Map();
|
|
30826
|
+
for (const event of events) {
|
|
30827
|
+
if (event.type !== "assistant/message") continue;
|
|
30828
|
+
const source = event.data.message.source;
|
|
30829
|
+
if (source.kind !== "model" || source.model === "") continue;
|
|
30830
|
+
counts.set(source.model, (counts.get(source.model) ?? 0) + 1);
|
|
30831
|
+
}
|
|
30832
|
+
if (counts.size === 0) return "";
|
|
30833
|
+
const most = Math.max(...counts.values());
|
|
30834
|
+
return [...counts.entries()].filter(([, count]) => count === most).map(([model]) => model).sort().join(" + ");
|
|
30835
|
+
}
|
|
30836
|
+
/** The model attribution for one turn: the meter's routes, else its messages. */
|
|
30837
|
+
function turnModel(slice, usage) {
|
|
30838
|
+
const routes = usage.routes ?? [];
|
|
30839
|
+
if (routes.length > 0) return routes.map((route) => route.model).join(" + ");
|
|
30840
|
+
return dominantModel(slice.events);
|
|
30755
30841
|
}
|
|
30756
|
-
/** Minimum blank gap kept between the leading and trailing sides. */
|
|
30757
|
-
const LEFT_RIGHT_GAP = 2;
|
|
30758
|
-
/** Column held back so Ink/yoga measurement drift can never force a wrap. */
|
|
30759
|
-
const WIDTH_SAFETY = 1;
|
|
30760
|
-
/** Column budget for the session title before it ellipsizes. */
|
|
30761
|
-
const TITLE_BUDGET = 48;
|
|
30762
30842
|
/**
|
|
30763
|
-
*
|
|
30764
|
-
*
|
|
30765
|
-
*
|
|
30766
|
-
*
|
|
30843
|
+
* Render one table: a dim header plus one row per entry. The free-text cell
|
|
30844
|
+
* (the model name) keeps a readable share of the width, so trailing numeric
|
|
30845
|
+
* columns drop first on a narrow terminal instead of leaving an ellipsised
|
|
30846
|
+
* model name — the name is what the reader came for.
|
|
30847
|
+
* @param columns - the numeric columns, most important first.
|
|
30848
|
+
* @param rows - one entry per line, already in display order.
|
|
30849
|
+
* @param text - how to read the free-text cell of one entry.
|
|
30850
|
+
* @param width - the column budget.
|
|
30851
|
+
* @param textFirst - true puts the free-text cell before the numbers (the
|
|
30852
|
+
* model-group table); false appends it (the per-turn table).
|
|
30853
|
+
* @returns the header row plus one row per entry.
|
|
30767
30854
|
*/
|
|
30768
|
-
|
|
30769
|
-
const
|
|
30770
|
-
const
|
|
30771
|
-
const
|
|
30772
|
-
|
|
30773
|
-
const
|
|
30774
|
-
|
|
30775
|
-
|
|
30776
|
-
|
|
30777
|
-
|
|
30778
|
-
const
|
|
30779
|
-
const
|
|
30855
|
+
function table(columns, rows, text, width, textFirst) {
|
|
30856
|
+
const budget = width - 2;
|
|
30857
|
+
const minText = 14;
|
|
30858
|
+
const kept = [];
|
|
30859
|
+
let used = 0;
|
|
30860
|
+
for (const column of columns) {
|
|
30861
|
+
if (used + column.width > budget - minText - 1) break;
|
|
30862
|
+
kept.push(column);
|
|
30863
|
+
used += column.width;
|
|
30864
|
+
}
|
|
30865
|
+
const textWidth = Math.min(24, Math.max(6, budget - used - 1));
|
|
30866
|
+
const numbers = (row) => kept.map((column) => padColumns(column.value(row), column.width)).join("");
|
|
30867
|
+
const header = kept.map((column) => padColumns(column.label(), column.width)).join("");
|
|
30868
|
+
const lines = [{ segments: [lineSegment(truncateColumns(" " + (textFirst ? padColumns(t("panel.usage.colModel"), textWidth) + " " + header : header + " " + t("panel.usage.colModel")), width), "dim")] }];
|
|
30869
|
+
for (const row of rows) {
|
|
30870
|
+
const label = truncateColumns(text(row), textWidth);
|
|
30871
|
+
lines.push({ segments: [lineSegment(" " + (textFirst ? padColumns(label, textWidth) + " " + numbers(row) : numbers(row) + " " + label), "plain")] });
|
|
30872
|
+
}
|
|
30873
|
+
return lines;
|
|
30874
|
+
}
|
|
30780
30875
|
/**
|
|
30781
|
-
*
|
|
30782
|
-
*
|
|
30783
|
-
*
|
|
30784
|
-
* @
|
|
30785
|
-
* @returns tone for the badge span.
|
|
30876
|
+
* Render the panel body.
|
|
30877
|
+
* @param view - the projection totals plus the derived per-turn rows.
|
|
30878
|
+
* @param columns - usable content columns inside the panel border.
|
|
30879
|
+
* @returns bounded, styled rows ready to draw.
|
|
30786
30880
|
*/
|
|
30787
|
-
function
|
|
30788
|
-
const
|
|
30789
|
-
|
|
30790
|
-
|
|
30791
|
-
|
|
30881
|
+
function usageLines(view, columns) {
|
|
30882
|
+
const width = Math.max(8, Math.floor(columns));
|
|
30883
|
+
const lines = [];
|
|
30884
|
+
lines.push(heading(t("panel.usage.totals")));
|
|
30885
|
+
const totals = view.totals;
|
|
30886
|
+
if (totals === void 0) lines.push(note(t("panel.usage.unavailable")));
|
|
30887
|
+
else {
|
|
30888
|
+
const label = 12;
|
|
30889
|
+
lines.push(pairLine(t("panel.usage.uncached"), formatTokens(totals.uncachedInputTokens), label));
|
|
30890
|
+
lines.push(pairLine(t("panel.usage.cacheWrite"), formatTokens(totals.cacheWriteTokens), label));
|
|
30891
|
+
lines.push(pairLine(t("panel.usage.cacheRead"), formatTokens(totals.cacheReadTokens), label));
|
|
30892
|
+
lines.push(pairLine(t("panel.usage.output"), formatTokens(totals.outputTokens), label));
|
|
30893
|
+
lines.push(pairLine(t("panel.usage.total"), formatTokens(usageTotalTokens(totals)), label, "bold"));
|
|
30894
|
+
const hit = usageCacheHitPercent(totals);
|
|
30895
|
+
if (hit !== null) lines.push(pairLine(t("panel.usage.cacheHit"), hit + "%", label));
|
|
30896
|
+
}
|
|
30897
|
+
lines.push({ segments: [] });
|
|
30898
|
+
const models = modelTotals(view.turns);
|
|
30899
|
+
lines.push(heading(models.length === 0 ? t("panel.usage.byModel") : `${t("panel.usage.byModel")} · ${models.length}`));
|
|
30900
|
+
if (models.length === 0) lines.push(note(t("panel.usage.noTurns")));
|
|
30901
|
+
else {
|
|
30902
|
+
lines.push(...table([{
|
|
30903
|
+
label: () => t("panel.usage.colTurns"),
|
|
30904
|
+
value: (row) => String(row.turns),
|
|
30905
|
+
width: 7
|
|
30906
|
+
}, ...BUCKET_COLUMNS.map((column) => ({
|
|
30907
|
+
...column,
|
|
30908
|
+
value: (row) => column.value(row)
|
|
30909
|
+
}))], models, (row) => modelLabel(row.model), width, true));
|
|
30910
|
+
if (models.some((row) => row.partial.length > 0)) lines.push(note(t("panel.usage.partialNote")));
|
|
30911
|
+
}
|
|
30912
|
+
lines.push({ segments: [] });
|
|
30913
|
+
lines.push(heading(`${t("panel.usage.turns")} · ${view.turns.length}`));
|
|
30914
|
+
if (view.turns.length === 0) lines.push(note(t("panel.usage.noTurns")));
|
|
30915
|
+
else {
|
|
30916
|
+
const withTurn = [{
|
|
30917
|
+
label: () => t("panel.usage.colTurn"),
|
|
30918
|
+
value: (row) => `#${row.turn}`,
|
|
30919
|
+
width: 7
|
|
30920
|
+
}, ...BUCKET_COLUMNS.map((column) => ({
|
|
30921
|
+
...column,
|
|
30922
|
+
value: (row) => column.value(row.usage)
|
|
30923
|
+
}))];
|
|
30924
|
+
lines.push(...table(withTurn, [...view.turns].reverse(), (row) => modelLabel(row.model), width, false));
|
|
30925
|
+
}
|
|
30926
|
+
return lines.map((line) => boundLine(line, width));
|
|
30792
30927
|
}
|
|
30793
|
-
|
|
30794
|
-
|
|
30795
|
-
|
|
30928
|
+
//#endregion
|
|
30929
|
+
//#region src/render/editor.ts
|
|
30930
|
+
/**
|
|
30931
|
+
* Pure composer editor model with Codex textarea semantics: a grapheme
|
|
30932
|
+
* cursor over a column-safe multiline layout, word/piece motion, single-entry
|
|
30933
|
+
* kill + yank, and the shell-recall boundary gate that keeps Up/Down usable
|
|
30934
|
+
* inside a multiline draft.
|
|
30935
|
+
*
|
|
30936
|
+
* The model is intentionally string-offset based (UTF-16 indices clamped to
|
|
30937
|
+
* grapheme boundaries) so the React state stays two primitives
|
|
30938
|
+
* (value, cursor) and every operation here stays pure and testable.
|
|
30939
|
+
*
|
|
30940
|
+
* Word motion follows Codex's piece semantics: whitespace separates runs,
|
|
30941
|
+
* punctuation runs stay atomic, and each Han grapheme is its own boundary.
|
|
30942
|
+
*
|
|
30943
|
+
* @module @deepseek-ai/dsh-code/render/editor
|
|
30944
|
+
*/
|
|
30945
|
+
const segmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter("en", { granularity: "grapheme" }) : void 0;
|
|
30946
|
+
/**
|
|
30947
|
+
* Split text into grapheme clusters. Falls back to code points when
|
|
30948
|
+
* Intl.Segmenter is unavailable; the fallback still keeps surrogate pairs
|
|
30949
|
+
* (emoji) atomic so the cursor can never split one.
|
|
30950
|
+
*/
|
|
30951
|
+
function splitGraphemes(text) {
|
|
30952
|
+
if (text === "") return [];
|
|
30953
|
+
const spans = [];
|
|
30954
|
+
if (segmenter !== void 0) {
|
|
30955
|
+
for (const piece of segmenter.segment(text)) spans.push({
|
|
30956
|
+
text: piece.segment,
|
|
30957
|
+
start: piece.index,
|
|
30958
|
+
end: piece.index + piece.segment.length,
|
|
30959
|
+
width: visibleColumns(piece.segment)
|
|
30960
|
+
});
|
|
30961
|
+
return spans;
|
|
30962
|
+
}
|
|
30963
|
+
let start = 0;
|
|
30964
|
+
for (const char of text) {
|
|
30965
|
+
spans.push({
|
|
30966
|
+
text: char,
|
|
30967
|
+
start,
|
|
30968
|
+
end: start + char.length,
|
|
30969
|
+
width: visibleColumns(char)
|
|
30970
|
+
});
|
|
30971
|
+
start += char.length;
|
|
30972
|
+
}
|
|
30973
|
+
return spans;
|
|
30796
30974
|
}
|
|
30797
|
-
/**
|
|
30798
|
-
function
|
|
30799
|
-
|
|
30800
|
-
|
|
30801
|
-
|
|
30802
|
-
|
|
30975
|
+
/** Round one UTF-16 offset down to the grapheme boundary at or before it. */
|
|
30976
|
+
function floorBoundary(spans, offset) {
|
|
30977
|
+
for (let index = spans.length - 1; index >= 0; index -= 1) {
|
|
30978
|
+
const span = spans[index];
|
|
30979
|
+
if (span.end <= offset) return span.end;
|
|
30980
|
+
if (span.start < offset) return span.start;
|
|
30981
|
+
}
|
|
30982
|
+
return 0;
|
|
30803
30983
|
}
|
|
30804
|
-
/**
|
|
30805
|
-
function
|
|
30806
|
-
|
|
30984
|
+
/** Clamp a cursor offset to the nearest grapheme boundary (surrogates, ZWJ, marks stay whole). */
|
|
30985
|
+
function clampCursor(value, offset) {
|
|
30986
|
+
if (value === "") return 0;
|
|
30987
|
+
const target = Math.max(0, Math.min(value.length, Math.floor(offset)));
|
|
30988
|
+
if (target === 0 || target === value.length) return target;
|
|
30989
|
+
const spans = splitGraphemes(value);
|
|
30990
|
+
const down = floorBoundary(spans, target);
|
|
30991
|
+
if (down === target) return target;
|
|
30992
|
+
const up = spans.find((span) => span.start >= target)?.start ?? value.length;
|
|
30993
|
+
return up - target < target - down ? up : down;
|
|
30807
30994
|
}
|
|
30808
30995
|
/**
|
|
30809
|
-
*
|
|
30810
|
-
*
|
|
30811
|
-
*
|
|
30996
|
+
* Delete the final grapheme cluster (append-only drafts without a cursor).
|
|
30997
|
+
* Surrogate pairs and multi-codepoint emoji stay whole instead of leaving a
|
|
30998
|
+
* lone trailing code unit behind.
|
|
30812
30999
|
*/
|
|
30813
|
-
function
|
|
30814
|
-
if (
|
|
30815
|
-
const
|
|
30816
|
-
|
|
30817
|
-
const modeSep = spans[spans.length - 3];
|
|
30818
|
-
if (last?.tone === "accent" && modeSep !== void 0 && isIdentitySep(modeSep) && before?.tone === "label") return spans.slice(0, -3);
|
|
30819
|
-
if ((last?.tone === "branch" || last?.tone === "path") && before !== void 0 && isIdentitySep(before)) return spans.slice(0, -2);
|
|
31000
|
+
function deleteLastGrapheme(text) {
|
|
31001
|
+
if (text === "") return "";
|
|
31002
|
+
const spans = splitGraphemes(text);
|
|
31003
|
+
return text.slice(0, spans[spans.length - 1].start);
|
|
30820
31004
|
}
|
|
30821
|
-
/**
|
|
30822
|
-
|
|
30823
|
-
|
|
30824
|
-
|
|
30825
|
-
|
|
31005
|
+
/**
|
|
31006
|
+
* Step the cursor by whole graphemes (negative steps left). The cursor is
|
|
31007
|
+
* assumed to sit on a boundary; any drift is clamped first.
|
|
31008
|
+
*/
|
|
31009
|
+
function moveCursorBy(value, offset, delta) {
|
|
31010
|
+
if (delta === 0 || value === "") return clampCursor(value, offset);
|
|
31011
|
+
const spans = splitGraphemes(value);
|
|
31012
|
+
const boundaries = [0];
|
|
31013
|
+
for (const span of spans) boundaries.push(span.end);
|
|
31014
|
+
const current = boundaries.indexOf(clampCursor(value, offset));
|
|
31015
|
+
if (current === -1) return clampCursor(value, offset);
|
|
31016
|
+
return boundaries[Math.max(0, Math.min(boundaries.length - 1, current + delta))];
|
|
30826
31017
|
}
|
|
30827
|
-
/**
|
|
30828
|
-
|
|
30829
|
-
|
|
30830
|
-
|
|
30831
|
-
|
|
30832
|
-
|
|
31018
|
+
/**
|
|
31019
|
+
* Normalize text entering the draft: CRLF/CR become LF, tabs become two
|
|
31020
|
+
* spaces (terminal tab stops are contextual and cannot join a deterministic
|
|
31021
|
+
* row budget), and every other C0 control byte plus DEL is REMOVED — the
|
|
31022
|
+
* draft is data, so a stray ESC (Windows Terminal file drops) disappears
|
|
31023
|
+
* instead of rendering as literal backslash-x-1-b text. Newlines survive.
|
|
31024
|
+
*/
|
|
31025
|
+
function sanitizeDraftText(text) {
|
|
31026
|
+
return text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "").replaceAll(" ", " ");
|
|
30833
31027
|
}
|
|
30834
|
-
/**
|
|
30835
|
-
|
|
30836
|
-
|
|
30837
|
-
|
|
30838
|
-
|
|
30839
|
-
|
|
30840
|
-
const
|
|
30841
|
-
|
|
30842
|
-
|
|
30843
|
-
|
|
30844
|
-
|
|
30845
|
-
|
|
30846
|
-
|
|
30847
|
-
|
|
30848
|
-
|
|
30849
|
-
|
|
31028
|
+
/**
|
|
31029
|
+
* Hard-wrap the draft into column-safe physical rows. Wide graphemes never
|
|
31030
|
+
* split across rows (a grapheme that does not fit flushes the row first) and
|
|
31031
|
+
* explicit newlines end their row without occupying a cell.
|
|
31032
|
+
*/
|
|
31033
|
+
function editorModel(value, columns) {
|
|
31034
|
+
const width = Math.max(1, Math.floor(columns));
|
|
31035
|
+
const spans = splitGraphemes(value);
|
|
31036
|
+
const rows = [];
|
|
31037
|
+
let text = "";
|
|
31038
|
+
let start = 0;
|
|
31039
|
+
let used = 0;
|
|
31040
|
+
const offsets = [0];
|
|
31041
|
+
const rowColumns = [0];
|
|
31042
|
+
const rowCuts = [0];
|
|
31043
|
+
const flush = (end) => {
|
|
31044
|
+
rows.push({
|
|
31045
|
+
text,
|
|
31046
|
+
start,
|
|
31047
|
+
end,
|
|
31048
|
+
offsets: [...offsets],
|
|
31049
|
+
columns: [...rowColumns],
|
|
31050
|
+
cuts: [...rowCuts]
|
|
30850
31051
|
});
|
|
30851
|
-
|
|
30852
|
-
|
|
30853
|
-
|
|
30854
|
-
|
|
30855
|
-
|
|
30856
|
-
}
|
|
30857
|
-
const
|
|
30858
|
-
|
|
30859
|
-
|
|
30860
|
-
|
|
30861
|
-
|
|
30862
|
-
|
|
30863
|
-
|
|
30864
|
-
|
|
30865
|
-
tone: "accent"
|
|
30866
|
-
});
|
|
30867
|
-
}
|
|
30868
|
-
const branch = safe(facts.branch);
|
|
30869
|
-
if (branch !== "" && enabled.has("branch")) push({
|
|
30870
|
-
text: "⑂ " + branch,
|
|
30871
|
-
tone: "branch"
|
|
30872
|
-
});
|
|
30873
|
-
const left = [{
|
|
30874
|
-
group: { spans: identity },
|
|
30875
|
-
rank: RANK_IDENTITY,
|
|
30876
|
-
id: "identity"
|
|
30877
|
-
}];
|
|
30878
|
-
const right = [];
|
|
30879
|
-
const row2 = [];
|
|
30880
|
-
if (stats.turns > 0 || stats.steps > 0) {
|
|
30881
|
-
if (enabled.has("turns")) {
|
|
30882
|
-
const counts = [];
|
|
30883
|
-
const pair = (label, value) => {
|
|
30884
|
-
if (counts.length > 0) counts.push(sep());
|
|
30885
|
-
counts.push({
|
|
30886
|
-
text: label + " ",
|
|
30887
|
-
tone: "label"
|
|
30888
|
-
}, {
|
|
30889
|
-
text: value,
|
|
30890
|
-
tone: "value"
|
|
30891
|
-
});
|
|
30892
|
-
};
|
|
30893
|
-
pair(t("status.label.turns"), String(stats.turns));
|
|
30894
|
-
pair(t("status.label.steps"), String(stats.steps));
|
|
30895
|
-
row2.push({
|
|
30896
|
-
group: { spans: counts },
|
|
30897
|
-
rank: RANK_COUNTS,
|
|
30898
|
-
id: "turns"
|
|
30899
|
-
});
|
|
31052
|
+
text = "";
|
|
31053
|
+
used = 0;
|
|
31054
|
+
offsets.length = 0;
|
|
31055
|
+
rowColumns.length = 0;
|
|
31056
|
+
rowCuts.length = 0;
|
|
31057
|
+
};
|
|
31058
|
+
for (const span of spans) {
|
|
31059
|
+
if (span.text === "\n") {
|
|
31060
|
+
flush(span.start);
|
|
31061
|
+
start = span.end;
|
|
31062
|
+
offsets.push(span.end);
|
|
31063
|
+
rowColumns.push(0);
|
|
31064
|
+
rowCuts.push(0);
|
|
31065
|
+
continue;
|
|
30900
31066
|
}
|
|
30901
|
-
if (
|
|
30902
|
-
|
|
30903
|
-
|
|
30904
|
-
|
|
30905
|
-
|
|
30906
|
-
|
|
30907
|
-
tone: "label"
|
|
30908
|
-
}, {
|
|
30909
|
-
text: value,
|
|
30910
|
-
tone: "value"
|
|
30911
|
-
});
|
|
30912
|
-
};
|
|
30913
|
-
if (stats.llmMs > 0) pair(t("status.label.modelTime"), formatDuration(stats.llmMs));
|
|
30914
|
-
if (stats.ttftSteps > 0) pair(t("status.label.latency"), formatDuration(stats.ttftMs / stats.ttftSteps));
|
|
30915
|
-
if (stats.decodeMs > 0 && stats.decodeTokens > 0) {
|
|
30916
|
-
if (durations.length > 0) durations.push(sep());
|
|
30917
|
-
durations.push({
|
|
30918
|
-
text: formatRate(stats.decodeTokens / (stats.decodeMs / 1e3)),
|
|
30919
|
-
tone: "value"
|
|
30920
|
-
}, {
|
|
30921
|
-
text: t("status.label.tokensPerSec"),
|
|
30922
|
-
tone: "label"
|
|
30923
|
-
});
|
|
30924
|
-
}
|
|
30925
|
-
if (stats.toolMs > 0) pair(t("status.label.tool"), formatDuration(stats.toolMs));
|
|
30926
|
-
if (durations.length > 0) row2.push({
|
|
30927
|
-
group: { spans: durations },
|
|
30928
|
-
rank: RANK2_DURATIONS,
|
|
30929
|
-
id: "durations"
|
|
30930
|
-
});
|
|
31067
|
+
if (used > 0 && used + span.width > width) {
|
|
31068
|
+
flush(span.start);
|
|
31069
|
+
start = span.start;
|
|
31070
|
+
offsets.push(span.start);
|
|
31071
|
+
rowColumns.push(0);
|
|
31072
|
+
rowCuts.push(0);
|
|
30931
31073
|
}
|
|
31074
|
+
text += span.text;
|
|
31075
|
+
used += span.width;
|
|
31076
|
+
offsets.push(span.end);
|
|
31077
|
+
rowColumns.push(used);
|
|
31078
|
+
rowCuts.push(text.length);
|
|
30932
31079
|
}
|
|
30933
|
-
|
|
30934
|
-
if (
|
|
30935
|
-
|
|
30936
|
-
|
|
30937
|
-
|
|
30938
|
-
|
|
30939
|
-
|
|
30940
|
-
|
|
30941
|
-
tone: "value"
|
|
30942
|
-
}, sep());
|
|
30943
|
-
spans.push({
|
|
30944
|
-
text: cacheHit + "%",
|
|
30945
|
-
tone: "value"
|
|
30946
|
-
});
|
|
30947
|
-
row2.push({
|
|
30948
|
-
group: { spans },
|
|
30949
|
-
rank: RANK2_CACHE,
|
|
30950
|
-
id: "cache"
|
|
30951
|
-
});
|
|
30952
|
-
}
|
|
30953
|
-
if (stats.contextWindow > 0 && stats.lastPromptTokens > 0 && enabled.has("context")) left.push({
|
|
30954
|
-
group: { spans: contextGroupSpans(stats.lastPromptTokens, stats.contextWindow, contextWidth, "full") },
|
|
30955
|
-
rank: RANK_CONTEXT,
|
|
30956
|
-
id: "context"
|
|
31080
|
+
if (text !== "" || rows.length === 0) flush(value.length);
|
|
31081
|
+
else if (offsets.length > 0) rows.push({
|
|
31082
|
+
text: "",
|
|
31083
|
+
start,
|
|
31084
|
+
end: value.length,
|
|
31085
|
+
offsets: [...offsets],
|
|
31086
|
+
columns: [...rowColumns],
|
|
31087
|
+
cuts: [...rowCuts]
|
|
30957
31088
|
});
|
|
30958
|
-
|
|
30959
|
-
|
|
30960
|
-
|
|
30961
|
-
|
|
30962
|
-
|
|
30963
|
-
|
|
30964
|
-
|
|
30965
|
-
|
|
30966
|
-
|
|
30967
|
-
|
|
30968
|
-
|
|
31089
|
+
return {
|
|
31090
|
+
rows,
|
|
31091
|
+
length: value.length
|
|
31092
|
+
};
|
|
31093
|
+
}
|
|
31094
|
+
/** Split one row around the authoritative caret; every other row stays whole. */
|
|
31095
|
+
function editorRowParts(row, rowIndex, caretRow, cursor, caretEnabled = true) {
|
|
31096
|
+
if (!caretEnabled || rowIndex !== caretRow) return {
|
|
31097
|
+
before: "",
|
|
31098
|
+
caret: "",
|
|
31099
|
+
after: row.text,
|
|
31100
|
+
hasCaret: false
|
|
31101
|
+
};
|
|
31102
|
+
const at = row.offsets.indexOf(cursor);
|
|
31103
|
+
if (at < 0) return {
|
|
31104
|
+
before: "",
|
|
31105
|
+
caret: "",
|
|
31106
|
+
after: row.text,
|
|
31107
|
+
hasCaret: false
|
|
31108
|
+
};
|
|
31109
|
+
return {
|
|
31110
|
+
before: at > 0 ? row.text.slice(0, row.cuts[at]) : "",
|
|
31111
|
+
caret: at < row.cuts.length - 1 ? row.text.slice(row.cuts[at], row.cuts[at + 1]) : " ",
|
|
31112
|
+
after: at < row.cuts.length - 1 ? row.text.slice(row.cuts[at + 1]) : "",
|
|
31113
|
+
hasCaret: true
|
|
31114
|
+
};
|
|
31115
|
+
}
|
|
31116
|
+
/** Map a cursor offset to its caret site on the wrapped rows. */
|
|
31117
|
+
function caretSite(model, offset) {
|
|
31118
|
+
const target = Math.max(0, Math.min(model.length, offset));
|
|
31119
|
+
for (let index = model.rows.length - 1; index >= 0; index -= 1) {
|
|
31120
|
+
const row = model.rows[index];
|
|
31121
|
+
const at = row.offsets.indexOf(target);
|
|
31122
|
+
if (at >= 0) return {
|
|
31123
|
+
row: index,
|
|
31124
|
+
column: row.columns[at]
|
|
30969
31125
|
};
|
|
30970
|
-
pair(t("status.label.in"), formatTokens(stats.usage.uncachedInputTokens));
|
|
30971
|
-
pair(t("status.label.out"), formatTokens(stats.usage.outputTokens));
|
|
30972
|
-
row2.push({
|
|
30973
|
-
group: { spans: tokens },
|
|
30974
|
-
rank: RANK_TOKENS,
|
|
30975
|
-
id: "tokens"
|
|
30976
|
-
});
|
|
30977
|
-
}
|
|
30978
|
-
const label = truncateColumns(safe(facts.title !== void 0 && facts.title !== "" ? facts.title : facts.sessionId), TITLE_BUDGET);
|
|
30979
|
-
if (label !== "" && enabled.has("title")) row2.push({
|
|
30980
|
-
group: { spans: [{
|
|
30981
|
-
text: label,
|
|
30982
|
-
tone: "meta"
|
|
30983
|
-
}] },
|
|
30984
|
-
rank: RANK_TITLE,
|
|
30985
|
-
id: "title"
|
|
30986
|
-
});
|
|
30987
|
-
if (facts.goal !== void 0 && enabled.has("goal")) row2.push({
|
|
30988
|
-
group: { spans: [{
|
|
30989
|
-
text: facts.goal.phase === "active" ? t("status.goal.round", {
|
|
30990
|
-
current: facts.goal.rounds,
|
|
30991
|
-
max: facts.goal.max
|
|
30992
|
-
}) : t("status.goal.phase", { phase: safe(facts.goal.phase) }),
|
|
30993
|
-
tone: "accent"
|
|
30994
|
-
}] },
|
|
30995
|
-
rank: RANK_GOAL,
|
|
30996
|
-
id: "goal"
|
|
30997
|
-
});
|
|
30998
|
-
const sandbox = safe(facts.sandbox ?? "");
|
|
30999
|
-
if (sandbox !== "" && sandbox.toLowerCase() !== facts.permission.toLowerCase() && enabled.has("sandbox")) row2.push({
|
|
31000
|
-
group: { spans: [{
|
|
31001
|
-
text: t("status.label.sandbox") + " " + sandbox,
|
|
31002
|
-
tone: "warn"
|
|
31003
|
-
}] },
|
|
31004
|
-
rank: RANK_SANDBOX,
|
|
31005
|
-
id: "sandbox"
|
|
31006
|
-
});
|
|
31007
|
-
const permission = safe(facts.permission);
|
|
31008
|
-
let badge = -1;
|
|
31009
|
-
const planStation = facts.plan && permissionTone(permission) === "success";
|
|
31010
|
-
if (permission !== "" && enabled.has("permission")) {
|
|
31011
|
-
right.push({
|
|
31012
|
-
span: planStation ? {
|
|
31013
|
-
text: t("status.plan.on"),
|
|
31014
|
-
tone: "plan"
|
|
31015
|
-
} : {
|
|
31016
|
-
text: permission,
|
|
31017
|
-
tone: permissionTone(permission)
|
|
31018
|
-
},
|
|
31019
|
-
rank: RANK_BADGE,
|
|
31020
|
-
id: "permission"
|
|
31021
|
-
});
|
|
31022
|
-
badge = right.length - 1;
|
|
31023
31126
|
}
|
|
31024
|
-
|
|
31025
|
-
group: { spans: [{
|
|
31026
|
-
text: t("status.plan.mark"),
|
|
31027
|
-
tone: "accent"
|
|
31028
|
-
}] },
|
|
31029
|
-
rank: RANK2_PLAN,
|
|
31030
|
-
id: "plan"
|
|
31031
|
-
});
|
|
31127
|
+
const last = model.rows[model.rows.length - 1];
|
|
31032
31128
|
return {
|
|
31033
|
-
|
|
31034
|
-
|
|
31035
|
-
badge,
|
|
31036
|
-
row2
|
|
31129
|
+
row: model.rows.length - 1,
|
|
31130
|
+
column: last === void 0 ? 0 : last.columns[last.columns.length - 1]
|
|
31037
31131
|
};
|
|
31038
31132
|
}
|
|
31039
31133
|
/**
|
|
31040
|
-
*
|
|
31041
|
-
*
|
|
31042
|
-
*
|
|
31043
|
-
* identity facts before removing the context group or the permission badge.
|
|
31044
|
-
* Row 2 fits all secondary figures and state within its own budget.
|
|
31045
|
-
* @param facts - identity facts resolved by the runner.
|
|
31046
|
-
* @param stats - session figures folded from the durable log.
|
|
31047
|
-
* @param columns - usable columns for each row (before their left padding).
|
|
31048
|
-
* @param options - 'busy' hides the cycle hint while a turn runs (Codex
|
|
31049
|
-
* keeps mode hints idle-only); 'items' is the ordered enabled-item config
|
|
31050
|
-
* from /statusline (defaults to the full catalog). Display order follows the
|
|
31051
|
-
* config per side while the drop ladder keeps its fixed ranks.
|
|
31052
|
-
* @returns the two rows to render; row1.left is never empty.
|
|
31134
|
+
* Move the caret across physical rows keeping a preferred display column
|
|
31135
|
+
* (Codex `preferred_col`): horizontal moves reset the preference, vertical
|
|
31136
|
+
* moves reuse it, clamped to each row's width.
|
|
31053
31137
|
*/
|
|
31054
|
-
function
|
|
31055
|
-
const
|
|
31056
|
-
|
|
31057
|
-
|
|
31058
|
-
|
|
31059
|
-
const
|
|
31060
|
-
const
|
|
31061
|
-
|
|
31062
|
-
|
|
31063
|
-
|
|
31064
|
-
|
|
31065
|
-
|
|
31066
|
-
|
|
31067
|
-
|
|
31068
|
-
|
|
31069
|
-
const
|
|
31070
|
-
const
|
|
31071
|
-
|
|
31072
|
-
|
|
31073
|
-
|
|
31074
|
-
const index = leftKept.findIndex((entry) => entry.id === "context");
|
|
31075
|
-
if (index < 0) return;
|
|
31076
|
-
leftKept[index] = {
|
|
31077
|
-
group: { spans: contextGroupSpans(stats.lastPromptTokens, stats.contextWindow, contextWidth, contextReadout) },
|
|
31078
|
-
rank: RANK_CONTEXT,
|
|
31079
|
-
id: "context"
|
|
31080
|
-
};
|
|
31081
|
-
};
|
|
31082
|
-
const width = () => {
|
|
31083
|
-
const leftWidth = joinWidth(leftKept.map((entry) => spansWidth(entry.group.spans)), groupSeparator);
|
|
31084
|
-
const rightWidth = joinWidth(rightKept.map((entry) => visibleColumns(entry.span.text)), itemSeparator) + (hint ? visibleColumns(statusCycleHint()) : 0);
|
|
31085
|
-
return rightWidth > 0 ? leftWidth + LEFT_RIGHT_GAP + rightWidth : leftWidth;
|
|
31138
|
+
function moveCursorVertically(model, offset, preferredColumn, delta) {
|
|
31139
|
+
const target = caretSite(model, offset).row + delta;
|
|
31140
|
+
if (delta === 0) return offset;
|
|
31141
|
+
if (target < 0) return 0;
|
|
31142
|
+
if (target >= model.rows.length) return model.length;
|
|
31143
|
+
const row = model.rows[target];
|
|
31144
|
+
const wanted = Math.max(0, Math.min(preferredColumn, row.columns[row.columns.length - 1]));
|
|
31145
|
+
let best = 0;
|
|
31146
|
+
for (let index = 1; index < row.columns.length; index += 1) if (row.columns[index] <= wanted) best = index;
|
|
31147
|
+
else break;
|
|
31148
|
+
return row.offsets[best];
|
|
31149
|
+
}
|
|
31150
|
+
/** The start/end offsets of the logical line containing the cursor. */
|
|
31151
|
+
function lineBounds(value, offset) {
|
|
31152
|
+
const target = Math.max(0, Math.min(value.length, offset));
|
|
31153
|
+
const start = value.lastIndexOf("\n", Math.max(0, target - 1)) + 1;
|
|
31154
|
+
const end = value.indexOf("\n", target);
|
|
31155
|
+
return {
|
|
31156
|
+
start,
|
|
31157
|
+
end: end === -1 ? value.length : end
|
|
31086
31158
|
};
|
|
31087
|
-
|
|
31088
|
-
|
|
31089
|
-
|
|
31090
|
-
|
|
31091
|
-
|
|
31092
|
-
|
|
31093
|
-
|
|
31094
|
-
|
|
31095
|
-
|
|
31096
|
-
|
|
31097
|
-
|
|
31098
|
-
|
|
31099
|
-
|
|
31100
|
-
|
|
31101
|
-
|
|
31102
|
-
|
|
31103
|
-
|
|
31104
|
-
|
|
31105
|
-
const identityEntry = leftKept[0];
|
|
31106
|
-
if (identityEntry?.id === "identity") {
|
|
31107
|
-
const peeled = peelIdentityFact(identityEntry.group.spans);
|
|
31108
|
-
if (peeled !== void 0) {
|
|
31109
|
-
leftKept[0] = {
|
|
31110
|
-
...identityEntry,
|
|
31111
|
-
group: { spans: peeled }
|
|
31112
|
-
};
|
|
31113
|
-
continue;
|
|
31114
|
-
}
|
|
31115
|
-
}
|
|
31116
|
-
if (leftKept.some((entry) => entry.id === "context")) {
|
|
31117
|
-
leftKept.splice(leftKept.findIndex((entry) => entry.id === "context"), 1);
|
|
31159
|
+
}
|
|
31160
|
+
/** Codex WORD_SEPARATORS: punctuation runs are their own word pieces. */
|
|
31161
|
+
const WORD_SEPARATORS = /* @__PURE__ */ new Set("`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?");
|
|
31162
|
+
const HAN_GRAPHEME = /^\p{Script=Han}(?:\p{Mark}|\uFE0F)*$/u;
|
|
31163
|
+
const UNICODE_PUNCTUATION = /^\p{P}+$/u;
|
|
31164
|
+
function classifyGrapheme(text) {
|
|
31165
|
+
if (/^\s$/u.test(text)) return "space";
|
|
31166
|
+
return WORD_SEPARATORS.has(text) || UNICODE_PUNCTUATION.test(text) ? "punct" : "word";
|
|
31167
|
+
}
|
|
31168
|
+
/** Maximal same-class runs; Han graphemes deliberately stay one run each. */
|
|
31169
|
+
function pieceRuns(value) {
|
|
31170
|
+
const runs = [];
|
|
31171
|
+
let current;
|
|
31172
|
+
for (const span of splitGraphemes(value)) {
|
|
31173
|
+
const klass = span.text === "\n" ? "space" : classifyGrapheme(span.text);
|
|
31174
|
+
const atomic = klass === "word" && HAN_GRAPHEME.test(span.text);
|
|
31175
|
+
if (current !== void 0 && current.class === klass && !current.atomic && !atomic) {
|
|
31176
|
+
current.end = span.end;
|
|
31118
31177
|
continue;
|
|
31119
31178
|
}
|
|
31120
|
-
|
|
31121
|
-
|
|
31122
|
-
|
|
31123
|
-
|
|
31124
|
-
|
|
31125
|
-
if (rank < dropRank) {
|
|
31126
|
-
dropRank = rank;
|
|
31127
|
-
dropLeft = index;
|
|
31128
|
-
dropRight = -1;
|
|
31129
|
-
}
|
|
31130
|
-
}
|
|
31131
|
-
for (let index = 0; index < rightKept.length; index += 1) {
|
|
31132
|
-
const rank = rightKept[index].rank;
|
|
31133
|
-
if (rank < dropRank) {
|
|
31134
|
-
dropRank = rank;
|
|
31135
|
-
dropRight = index;
|
|
31136
|
-
dropLeft = -1;
|
|
31137
|
-
}
|
|
31138
|
-
}
|
|
31139
|
-
if (dropLeft < 0 && dropRight < 0) break;
|
|
31140
|
-
if (dropLeft >= 0) leftKept.splice(dropLeft, 1);
|
|
31141
|
-
else {
|
|
31142
|
-
rightKept.splice(dropRight, 1);
|
|
31143
|
-
if (dropRight === rightKept.length) hint = false;
|
|
31144
|
-
}
|
|
31145
|
-
}
|
|
31146
|
-
if (width() > budget) {
|
|
31147
|
-
rightKept.length = 0;
|
|
31148
|
-
hint = false;
|
|
31149
|
-
while (leftKept.length > 1) leftKept.pop();
|
|
31150
|
-
leftKept[0] = {
|
|
31151
|
-
group: { spans: [{
|
|
31152
|
-
text: truncateColumns(leftKept[0].group.spans.map((span) => span.text).join(""), budget),
|
|
31153
|
-
tone: "model"
|
|
31154
|
-
}] },
|
|
31155
|
-
rank: RANK_IDENTITY,
|
|
31156
|
-
id: "identity"
|
|
31179
|
+
current = {
|
|
31180
|
+
start: span.start,
|
|
31181
|
+
end: span.end,
|
|
31182
|
+
class: klass,
|
|
31183
|
+
atomic
|
|
31157
31184
|
};
|
|
31185
|
+
runs.push(current);
|
|
31158
31186
|
}
|
|
31159
|
-
|
|
31160
|
-
const row2Budget = Math.max(1, budget - 2);
|
|
31161
|
-
const row2Width = () => joinWidth(row2Kept.map((entry) => spansWidth(entry.group.spans)), groupSeparator);
|
|
31162
|
-
while (row2Width() > row2Budget && row2Kept.length > 0) {
|
|
31163
|
-
let dropIndex = 0;
|
|
31164
|
-
let dropRank = Number.POSITIVE_INFINITY;
|
|
31165
|
-
for (let index = 0; index < row2Kept.length; index += 1) if (row2Kept[index].rank < dropRank) {
|
|
31166
|
-
dropRank = row2Kept[index].rank;
|
|
31167
|
-
dropIndex = index;
|
|
31168
|
-
}
|
|
31169
|
-
row2Kept.splice(dropIndex, 1);
|
|
31170
|
-
}
|
|
31171
|
-
return {
|
|
31172
|
-
row1: {
|
|
31173
|
-
left: leftKept.map((entry) => entry.group),
|
|
31174
|
-
right: rightKept.map((entry) => entry.span),
|
|
31175
|
-
hint
|
|
31176
|
-
},
|
|
31177
|
-
row2: {
|
|
31178
|
-
left: row2Kept.map((entry) => entry.group),
|
|
31179
|
-
right: [],
|
|
31180
|
-
hint: false
|
|
31181
|
-
}
|
|
31182
|
-
};
|
|
31183
|
-
}
|
|
31184
|
-
//#endregion
|
|
31185
|
-
//#region src/render/usage.ts
|
|
31186
|
-
/** Prompt-side tokens the provider billed: uncached input plus both cache buckets. */
|
|
31187
|
-
function billedInputTokens(totals) {
|
|
31188
|
-
return totals.uncachedInputTokens + totals.cacheReadTokens + totals.cacheWriteTokens;
|
|
31189
|
-
}
|
|
31190
|
-
/** Prompt plus completion tokens over the whole log. */
|
|
31191
|
-
function usageTotalTokens(totals) {
|
|
31192
|
-
return billedInputTokens(totals) + totals.outputTokens;
|
|
31193
|
-
}
|
|
31194
|
-
/**
|
|
31195
|
-
* Cache-hit share of billed prompt-side input.
|
|
31196
|
-
* @param totals - cumulative provider-reported buckets.
|
|
31197
|
-
* @returns percent rounded to one decimal place, or null when nothing was billed.
|
|
31198
|
-
*/
|
|
31199
|
-
function usageCacheHitPercent(totals) {
|
|
31200
|
-
const billed = billedInputTokens(totals);
|
|
31201
|
-
return billed === 0 ? null : Math.round(totals.cacheReadTokens / billed * 1e3) / 10;
|
|
31187
|
+
return runs;
|
|
31202
31188
|
}
|
|
31203
31189
|
/**
|
|
31204
|
-
*
|
|
31205
|
-
*
|
|
31206
|
-
* trailing slice is dropped rather than guessed at.
|
|
31207
|
-
* @param events - the whole durable log, in log order.
|
|
31208
|
-
* @returns one slice per `turn/start`…`turn/end` span, oldest first.
|
|
31190
|
+
* Codex `beginning_of_previous_word`: skip whitespace left, then land on the
|
|
31191
|
+
* START of the trailing non-space piece (extending over separator pieces).
|
|
31209
31192
|
*/
|
|
31210
|
-
function
|
|
31211
|
-
const
|
|
31212
|
-
|
|
31213
|
-
|
|
31214
|
-
|
|
31215
|
-
|
|
31216
|
-
|
|
31217
|
-
|
|
31218
|
-
};
|
|
31219
|
-
continue;
|
|
31220
|
-
}
|
|
31221
|
-
if (open === void 0) continue;
|
|
31222
|
-
open.events.push(event);
|
|
31223
|
-
if (event.type === "turn/end") {
|
|
31224
|
-
slices.push(open);
|
|
31225
|
-
open = void 0;
|
|
31226
|
-
}
|
|
31193
|
+
function moveWordLeft(value, offset) {
|
|
31194
|
+
const cursor = clampCursor(value, offset);
|
|
31195
|
+
const runs = pieceRuns(value);
|
|
31196
|
+
let index = runs.findLastIndex((run) => run.start < cursor);
|
|
31197
|
+
if (index < 0) return 0;
|
|
31198
|
+
if (runs[index].class === "space") {
|
|
31199
|
+
index -= 1;
|
|
31200
|
+
if (index < 0) return 0;
|
|
31227
31201
|
}
|
|
31228
|
-
|
|
31202
|
+
let target = index;
|
|
31203
|
+
while (target > 0 && runs[target].class === "punct" && runs[target - 1].class === "punct") target -= 1;
|
|
31204
|
+
return runs[target].start;
|
|
31229
31205
|
}
|
|
31230
31206
|
/**
|
|
31231
|
-
*
|
|
31232
|
-
*
|
|
31233
|
-
* @param derive - the meter's per-turn fold, injected so this module stays pure.
|
|
31234
|
-
* @returns one row per provable turn, oldest first; turns whose attempts did
|
|
31235
|
-
* not all report usage are omitted rather than estimated, and so is a turn
|
|
31236
|
-
* that billed nothing at all — an empty row is noise in a usage table.
|
|
31207
|
+
* Codex `end_of_next_word`: skip whitespace right, then land on the END of
|
|
31208
|
+
* the leading non-space piece (extending over separator pieces).
|
|
31237
31209
|
*/
|
|
31238
|
-
function
|
|
31239
|
-
const
|
|
31240
|
-
|
|
31241
|
-
|
|
31242
|
-
|
|
31243
|
-
|
|
31244
|
-
|
|
31245
|
-
|
|
31246
|
-
model: turnModel(slice, usage)
|
|
31247
|
-
});
|
|
31210
|
+
function moveWordRight(value, offset) {
|
|
31211
|
+
const cursor = clampCursor(value, offset);
|
|
31212
|
+
const runs = pieceRuns(value);
|
|
31213
|
+
let index = runs.findIndex((run) => run.end > cursor);
|
|
31214
|
+
if (index >= runs.length) return value.length;
|
|
31215
|
+
if (runs[index].class === "space") {
|
|
31216
|
+
index += 1;
|
|
31217
|
+
if (index >= runs.length) return value.length;
|
|
31248
31218
|
}
|
|
31249
|
-
|
|
31219
|
+
let target = index;
|
|
31220
|
+
while (target < runs.length - 1 && runs[target].class === "punct" && runs[target + 1].class === "punct") target += 1;
|
|
31221
|
+
return runs[target].end;
|
|
31250
31222
|
}
|
|
31251
|
-
|
|
31252
|
-
|
|
31253
|
-
|
|
31254
|
-
|
|
31255
|
-
|
|
31256
|
-
|
|
31257
|
-
|
|
31258
|
-
|
|
31223
|
+
function replaceRange(value, cursor, start, end, killed) {
|
|
31224
|
+
const from = Math.min(start, end);
|
|
31225
|
+
const to = Math.max(start, end);
|
|
31226
|
+
if (from >= to) return {
|
|
31227
|
+
value,
|
|
31228
|
+
cursor,
|
|
31229
|
+
killed: void 0
|
|
31230
|
+
};
|
|
31231
|
+
const text = value.slice(from, to);
|
|
31232
|
+
return {
|
|
31233
|
+
value: value.slice(0, from) + value.slice(to),
|
|
31234
|
+
cursor: Math.max(0, cursor - Math.max(0, Math.min(cursor, to) - from)),
|
|
31235
|
+
killed: killed ? text : void 0
|
|
31236
|
+
};
|
|
31259
31237
|
}
|
|
31260
|
-
/**
|
|
31261
|
-
|
|
31262
|
-
|
|
31263
|
-
|
|
31264
|
-
|
|
31265
|
-
|
|
31266
|
-
|
|
31267
|
-
|
|
31268
|
-
|
|
31269
|
-
function modelTotals(turns) {
|
|
31270
|
-
const groups = /* @__PURE__ */ new Map();
|
|
31271
|
-
for (const row of turns) groups.set(row.model, [...groups.get(row.model) ?? [], row]);
|
|
31272
|
-
const merged = [];
|
|
31273
|
-
for (const [model, rows] of groups) {
|
|
31274
|
-
const partial = [];
|
|
31275
|
-
const sum = (key) => {
|
|
31276
|
-
const reported = rows.flatMap((row) => row.usage[key] === void 0 ? [] : [row.usage[key]]);
|
|
31277
|
-
if (reported.length === 0) return void 0;
|
|
31278
|
-
if (reported.length < rows.length) partial.push(key);
|
|
31279
|
-
return reported.reduce((total, value) => total + value, 0);
|
|
31280
|
-
};
|
|
31281
|
-
const cacheReadTokens = sum("cacheReadTokens");
|
|
31282
|
-
const cacheWriteTokens = sum("cacheWriteTokens");
|
|
31283
|
-
const reasoningTokens = sum("reasoningTokens");
|
|
31284
|
-
merged.push({
|
|
31285
|
-
model,
|
|
31286
|
-
turns: rows.length,
|
|
31287
|
-
uncachedInputTokens: rows.reduce((sum, row) => sum + row.usage.uncachedInputTokens, 0),
|
|
31288
|
-
outputTokens: rows.reduce((sum, row) => sum + row.usage.outputTokens, 0),
|
|
31289
|
-
totalTokens: rows.reduce((sum, row) => sum + row.usage.totalTokens, 0),
|
|
31290
|
-
cacheReadTokens,
|
|
31291
|
-
cacheWriteTokens,
|
|
31292
|
-
reasoningTokens,
|
|
31293
|
-
partial
|
|
31294
|
-
});
|
|
31295
|
-
}
|
|
31296
|
-
return merged.sort((left, right) => left.model === "" ? 1 : right.model === "" ? -1 : right.totalTokens - left.totalTokens);
|
|
31238
|
+
/** Delete the grapheme cluster before the cursor. */
|
|
31239
|
+
function deleteBackward(value, cursor) {
|
|
31240
|
+
if (cursor === 0) return {
|
|
31241
|
+
value,
|
|
31242
|
+
cursor,
|
|
31243
|
+
killed: void 0
|
|
31244
|
+
};
|
|
31245
|
+
const spans = splitGraphemes(value.slice(0, cursor));
|
|
31246
|
+
return replaceRange(value, cursor, spans.length === 0 ? 0 : spans[spans.length - 1].start, cursor, false);
|
|
31297
31247
|
}
|
|
31298
|
-
/**
|
|
31299
|
-
function
|
|
31300
|
-
|
|
31301
|
-
|
|
31302
|
-
|
|
31303
|
-
|
|
31304
|
-
|
|
31305
|
-
|
|
31306
|
-
|
|
31307
|
-
left -= columns;
|
|
31308
|
-
continue;
|
|
31309
|
-
}
|
|
31310
|
-
segments.push(lineSegment(truncateColumns(segment.text, left), segment.style));
|
|
31311
|
-
left = 0;
|
|
31312
|
-
}
|
|
31313
|
-
return { segments };
|
|
31248
|
+
/** Delete the grapheme cluster at the cursor. */
|
|
31249
|
+
function deleteForward(value, cursor) {
|
|
31250
|
+
if (cursor >= value.length) return {
|
|
31251
|
+
value,
|
|
31252
|
+
cursor,
|
|
31253
|
+
killed: void 0
|
|
31254
|
+
};
|
|
31255
|
+
const span = splitGraphemes(value).find((candidate) => candidate.start >= cursor);
|
|
31256
|
+
return replaceRange(value, cursor, cursor, span === void 0 ? value.length : span.end, false);
|
|
31314
31257
|
}
|
|
31315
|
-
/**
|
|
31316
|
-
function
|
|
31317
|
-
return
|
|
31258
|
+
/** Delete back to the start of the previous word (fills the kill buffer). */
|
|
31259
|
+
function deleteWordBackward(value, cursor) {
|
|
31260
|
+
return replaceRange(value, cursor, moveWordLeft(value, cursor), cursor, true);
|
|
31318
31261
|
}
|
|
31319
|
-
/**
|
|
31320
|
-
function
|
|
31321
|
-
return
|
|
31262
|
+
/** Delete forward to the end of the next word (fills the kill buffer). */
|
|
31263
|
+
function deleteWordForward(value, cursor) {
|
|
31264
|
+
return replaceRange(value, cursor, cursor, moveWordRight(value, cursor), true);
|
|
31322
31265
|
}
|
|
31323
|
-
/**
|
|
31324
|
-
|
|
31325
|
-
|
|
31326
|
-
|
|
31327
|
-
|
|
31328
|
-
|
|
31329
|
-
|
|
31330
|
-
|
|
31331
|
-
|
|
31332
|
-
|
|
31333
|
-
if (billed === 0) return void 0;
|
|
31334
|
-
return Math.round(row.cacheReadTokens / billed * 1e3) / 10;
|
|
31266
|
+
/** Ctrl+U: kill from the line start to the cursor; at BOL, kill the newline. */
|
|
31267
|
+
function killToLineStart(value, cursor) {
|
|
31268
|
+
const bounds = lineBounds(value, cursor);
|
|
31269
|
+
if (cursor > bounds.start) return replaceRange(value, cursor, bounds.start, cursor, true);
|
|
31270
|
+
if (bounds.start > 0) return replaceRange(value, cursor, bounds.start - 1, bounds.start, true);
|
|
31271
|
+
return {
|
|
31272
|
+
value,
|
|
31273
|
+
cursor,
|
|
31274
|
+
killed: void 0
|
|
31275
|
+
};
|
|
31335
31276
|
}
|
|
31336
|
-
/**
|
|
31337
|
-
|
|
31338
|
-
|
|
31339
|
-
|
|
31277
|
+
/** Ctrl+K: kill from the cursor to the line end; at EOL, kill the newline. */
|
|
31278
|
+
function killToLineEnd(value, cursor) {
|
|
31279
|
+
const bounds = lineBounds(value, cursor);
|
|
31280
|
+
if (cursor < bounds.end) return replaceRange(value, cursor, cursor, bounds.end, true);
|
|
31281
|
+
if (bounds.end < value.length) return replaceRange(value, cursor, bounds.end, bounds.end + 1, true);
|
|
31282
|
+
return {
|
|
31283
|
+
value,
|
|
31284
|
+
cursor,
|
|
31285
|
+
killed: void 0
|
|
31286
|
+
};
|
|
31340
31287
|
}
|
|
31341
|
-
|
|
31342
|
-
|
|
31343
|
-
|
|
31344
|
-
|
|
31345
|
-
|
|
31346
|
-
|
|
31347
|
-
|
|
31348
|
-
|
|
31349
|
-
|
|
31350
|
-
|
|
31351
|
-
|
|
31352
|
-
|
|
31353
|
-
|
|
31354
|
-
value: (row) => optionalTokens(row.cacheReadTokens, isPartial(row, "cacheReadTokens")),
|
|
31355
|
-
width: 9
|
|
31356
|
-
},
|
|
31357
|
-
{
|
|
31358
|
-
label: () => t("panel.usage.colCacheHit"),
|
|
31359
|
-
value: (row) => percent(hitShare(row), isPartial(row, "cacheReadTokens") || isPartial(row, "cacheWriteTokens")),
|
|
31360
|
-
width: 8
|
|
31361
|
-
},
|
|
31362
|
-
{
|
|
31363
|
-
label: () => t("panel.usage.colCacheWrite"),
|
|
31364
|
-
value: (row) => optionalTokens(row.cacheWriteTokens, isPartial(row, "cacheWriteTokens")),
|
|
31365
|
-
width: 9
|
|
31366
|
-
},
|
|
31367
|
-
{
|
|
31368
|
-
label: () => t("panel.usage.colOut"),
|
|
31369
|
-
value: (row) => formatTokens(row.outputTokens),
|
|
31370
|
-
width: 9
|
|
31371
|
-
},
|
|
31372
|
-
{
|
|
31373
|
-
label: () => t("panel.usage.colThink"),
|
|
31374
|
-
value: (row) => optionalTokens(row.reasoningTokens, isPartial(row, "reasoningTokens")),
|
|
31375
|
-
width: 9
|
|
31376
|
-
}
|
|
31377
|
-
];
|
|
31378
|
-
/** A percentage, or an em dash when the row cannot prove one; `+` for a floor. */
|
|
31379
|
-
function percent(value, partial = false) {
|
|
31380
|
-
return value === void 0 ? "—" : value + "%" + (partial ? "+" : "");
|
|
31288
|
+
/** Insert sanitized text at the cursor. */
|
|
31289
|
+
function insertText(value, cursor, text) {
|
|
31290
|
+
const safe = sanitizeDraftText(text);
|
|
31291
|
+
if (safe === "") return {
|
|
31292
|
+
value,
|
|
31293
|
+
cursor,
|
|
31294
|
+
killed: void 0
|
|
31295
|
+
};
|
|
31296
|
+
return {
|
|
31297
|
+
value: value.slice(0, cursor) + safe + value.slice(cursor),
|
|
31298
|
+
cursor: cursor + safe.length,
|
|
31299
|
+
killed: void 0
|
|
31300
|
+
};
|
|
31381
31301
|
}
|
|
31382
|
-
/**
|
|
31383
|
-
|
|
31384
|
-
|
|
31385
|
-
|
|
31386
|
-
|
|
31387
|
-
|
|
31388
|
-
return formatTokens(value) + (partial ? "+" : "");
|
|
31302
|
+
/** Ctrl+A: current logical line start, then the previous line start on repeat. */
|
|
31303
|
+
function moveToLineStart(value, cursor, crossOnRepeat) {
|
|
31304
|
+
const site = clampCursor(value, cursor);
|
|
31305
|
+
const bounds = lineBounds(value, site);
|
|
31306
|
+
if (!crossOnRepeat || site !== bounds.start || bounds.start === 0) return bounds.start;
|
|
31307
|
+
return lineBounds(value, bounds.start - 1).start;
|
|
31389
31308
|
}
|
|
31390
|
-
/**
|
|
31391
|
-
function
|
|
31392
|
-
|
|
31309
|
+
/** Ctrl+E: current logical line end, then the next line end on repeat. */
|
|
31310
|
+
function moveToLineEnd(value, cursor, crossOnRepeat) {
|
|
31311
|
+
const site = clampCursor(value, cursor);
|
|
31312
|
+
const bounds = lineBounds(value, site);
|
|
31313
|
+
if (!crossOnRepeat || site !== bounds.end || bounds.end >= value.length) return bounds.end;
|
|
31314
|
+
return lineBounds(value, bounds.end + 1).end;
|
|
31393
31315
|
}
|
|
31394
31316
|
/**
|
|
31395
|
-
*
|
|
31396
|
-
*
|
|
31397
|
-
*
|
|
31398
|
-
* recorded — nothing is inferred from the current selection.
|
|
31317
|
+
* Remap a captured range when all intervening edits are wholly before or
|
|
31318
|
+
* wholly after it. An edit overlapping either boundary invalidates the
|
|
31319
|
+
* anchor instead of guessing and inserting content at a surprising place.
|
|
31399
31320
|
*/
|
|
31400
|
-
function
|
|
31401
|
-
const
|
|
31402
|
-
|
|
31403
|
-
|
|
31404
|
-
|
|
31405
|
-
|
|
31406
|
-
|
|
31321
|
+
function remapStableRange(original, current, range) {
|
|
31322
|
+
const start = Math.max(0, Math.min(original.length, range.start));
|
|
31323
|
+
const end = Math.max(start, Math.min(original.length, range.end));
|
|
31324
|
+
if (original === current) return {
|
|
31325
|
+
start,
|
|
31326
|
+
end
|
|
31327
|
+
};
|
|
31328
|
+
let prefix = 0;
|
|
31329
|
+
const shared = Math.min(original.length, current.length);
|
|
31330
|
+
while (prefix < shared && original[prefix] === current[prefix]) prefix += 1;
|
|
31331
|
+
let suffix = 0;
|
|
31332
|
+
while (suffix < original.length - prefix && suffix < current.length - prefix && original[original.length - 1 - suffix] === current[current.length - 1 - suffix]) suffix += 1;
|
|
31333
|
+
const oldChangedEnd = original.length - suffix;
|
|
31334
|
+
if (prefix >= end) return {
|
|
31335
|
+
start,
|
|
31336
|
+
end
|
|
31337
|
+
};
|
|
31338
|
+
if (oldChangedEnd <= start) {
|
|
31339
|
+
const delta = current.length - original.length;
|
|
31340
|
+
return {
|
|
31341
|
+
start: start + delta,
|
|
31342
|
+
end: end + delta
|
|
31343
|
+
};
|
|
31407
31344
|
}
|
|
31408
|
-
if (counts.size === 0) return "";
|
|
31409
|
-
const most = Math.max(...counts.values());
|
|
31410
|
-
return [...counts.entries()].filter(([, count]) => count === most).map(([model]) => model).sort().join(" + ");
|
|
31411
31345
|
}
|
|
31412
|
-
/**
|
|
31413
|
-
function
|
|
31414
|
-
const
|
|
31415
|
-
|
|
31416
|
-
|
|
31346
|
+
/** Replace a current range while preserving a cursor moved after capture. */
|
|
31347
|
+
function replaceRangePreservingCursor(value, cursor, range, replacement) {
|
|
31348
|
+
const start = Math.max(0, Math.min(value.length, range.start));
|
|
31349
|
+
const end = Math.max(start, Math.min(value.length, range.end));
|
|
31350
|
+
const site = clampCursor(value, cursor);
|
|
31351
|
+
const nextCursor = site <= start ? site : site >= end ? site + replacement.length - (end - start) : start + replacement.length;
|
|
31352
|
+
return {
|
|
31353
|
+
value: value.slice(0, start) + replacement + value.slice(end),
|
|
31354
|
+
cursor: nextCursor,
|
|
31355
|
+
killed: void 0
|
|
31356
|
+
};
|
|
31417
31357
|
}
|
|
31418
31358
|
/**
|
|
31419
|
-
*
|
|
31420
|
-
*
|
|
31421
|
-
*
|
|
31422
|
-
* model name — the name is what the reader came for.
|
|
31423
|
-
* @param columns - the numeric columns, most important first.
|
|
31424
|
-
* @param rows - one entry per line, already in display order.
|
|
31425
|
-
* @param text - how to read the free-text cell of one entry.
|
|
31426
|
-
* @param width - the column budget.
|
|
31427
|
-
* @param textFirst - true puts the free-text cell before the numbers (the
|
|
31428
|
-
* model-group table); false appends it (the per-turn table).
|
|
31429
|
-
* @returns the header row plus one row per entry.
|
|
31359
|
+
* Composer editor row budget: the editor itself never grows past this many
|
|
31360
|
+
* physical rows; deeper drafts scroll internally to keep the caret visible.
|
|
31361
|
+
* Short terminals collapse toward one row so the live transcript keeps room.
|
|
31430
31362
|
*/
|
|
31431
|
-
function
|
|
31432
|
-
|
|
31433
|
-
const minText = 14;
|
|
31434
|
-
const kept = [];
|
|
31435
|
-
let used = 0;
|
|
31436
|
-
for (const column of columns) {
|
|
31437
|
-
if (used + column.width > budget - minText - 1) break;
|
|
31438
|
-
kept.push(column);
|
|
31439
|
-
used += column.width;
|
|
31440
|
-
}
|
|
31441
|
-
const textWidth = Math.min(24, Math.max(6, budget - used - 1));
|
|
31442
|
-
const numbers = (row) => kept.map((column) => padColumns(column.value(row), column.width)).join("");
|
|
31443
|
-
const header = kept.map((column) => padColumns(column.label(), column.width)).join("");
|
|
31444
|
-
const lines = [{ segments: [lineSegment(truncateColumns(" " + (textFirst ? padColumns(t("panel.usage.colModel"), textWidth) + " " + header : header + " " + t("panel.usage.colModel")), width), "dim")] }];
|
|
31445
|
-
for (const row of rows) {
|
|
31446
|
-
const label = truncateColumns(text(row), textWidth);
|
|
31447
|
-
lines.push({ segments: [lineSegment(" " + (textFirst ? padColumns(label, textWidth) + " " + numbers(row) : numbers(row) + " " + label), "plain")] });
|
|
31448
|
-
}
|
|
31449
|
-
return lines;
|
|
31363
|
+
function composerMaxRows(terminalRows) {
|
|
31364
|
+
return Math.max(1, Math.min(6, Math.floor((Math.max(1, terminalRows) - 10) / 3)));
|
|
31450
31365
|
}
|
|
31451
31366
|
/**
|
|
31452
|
-
*
|
|
31453
|
-
*
|
|
31454
|
-
*
|
|
31455
|
-
*
|
|
31367
|
+
* History navigation starts with Up on an empty draft, or continues from an
|
|
31368
|
+
* unchanged recalled entry whenever the caret sits on either text edge
|
|
31369
|
+
* (start or end) - moving the caret into the interior returns the keys to
|
|
31370
|
+
* ordinary editing until an edge is reached again.
|
|
31456
31371
|
*/
|
|
31457
|
-
function
|
|
31458
|
-
|
|
31459
|
-
|
|
31460
|
-
|
|
31461
|
-
const totals = view.totals;
|
|
31462
|
-
if (totals === void 0) lines.push(note(t("panel.usage.unavailable")));
|
|
31463
|
-
else {
|
|
31464
|
-
const label = 12;
|
|
31465
|
-
lines.push(pairLine(t("panel.usage.uncached"), formatTokens(totals.uncachedInputTokens), label));
|
|
31466
|
-
lines.push(pairLine(t("panel.usage.cacheWrite"), formatTokens(totals.cacheWriteTokens), label));
|
|
31467
|
-
lines.push(pairLine(t("panel.usage.cacheRead"), formatTokens(totals.cacheReadTokens), label));
|
|
31468
|
-
lines.push(pairLine(t("panel.usage.output"), formatTokens(totals.outputTokens), label));
|
|
31469
|
-
lines.push(pairLine(t("panel.usage.total"), formatTokens(usageTotalTokens(totals)), label, "bold"));
|
|
31470
|
-
const hit = usageCacheHitPercent(totals);
|
|
31471
|
-
if (hit !== null) lines.push(pairLine(t("panel.usage.cacheHit"), hit + "%", label));
|
|
31472
|
-
}
|
|
31473
|
-
lines.push({ segments: [] });
|
|
31474
|
-
const models = modelTotals(view.turns);
|
|
31475
|
-
lines.push(heading(models.length === 0 ? t("panel.usage.byModel") : `${t("panel.usage.byModel")} · ${models.length}`));
|
|
31476
|
-
if (models.length === 0) lines.push(note(t("panel.usage.noTurns")));
|
|
31477
|
-
else {
|
|
31478
|
-
lines.push(...table([{
|
|
31479
|
-
label: () => t("panel.usage.colTurns"),
|
|
31480
|
-
value: (row) => String(row.turns),
|
|
31481
|
-
width: 7
|
|
31482
|
-
}, ...BUCKET_COLUMNS.map((column) => ({
|
|
31483
|
-
...column,
|
|
31484
|
-
value: (row) => column.value(row)
|
|
31485
|
-
}))], models, (row) => modelLabel(row.model), width, true));
|
|
31486
|
-
if (models.some((row) => row.partial.length > 0)) lines.push(note(t("panel.usage.partialNote")));
|
|
31487
|
-
}
|
|
31488
|
-
lines.push({ segments: [] });
|
|
31489
|
-
lines.push(heading(`${t("panel.usage.turns")} · ${view.turns.length}`));
|
|
31490
|
-
if (view.turns.length === 0) lines.push(note(t("panel.usage.noTurns")));
|
|
31491
|
-
else {
|
|
31492
|
-
const withTurn = [{
|
|
31493
|
-
label: () => t("panel.usage.colTurn"),
|
|
31494
|
-
value: (row) => `#${row.turn}`,
|
|
31495
|
-
width: 7
|
|
31496
|
-
}, ...BUCKET_COLUMNS.map((column) => ({
|
|
31497
|
-
...column,
|
|
31498
|
-
value: (row) => column.value(row.usage)
|
|
31499
|
-
}))];
|
|
31500
|
-
lines.push(...table(withTurn, [...view.turns].reverse(), (row) => modelLabel(row.model), width, false));
|
|
31501
|
-
}
|
|
31502
|
-
return lines.map((line) => boundLine(line, width));
|
|
31372
|
+
function shouldRecallNavigate(value, cursor, lastRecalled, direction) {
|
|
31373
|
+
if (value === "") return direction < 0;
|
|
31374
|
+
if (lastRecalled !== value) return false;
|
|
31375
|
+
return cursor === 0 || cursor === value.length;
|
|
31503
31376
|
}
|
|
31504
31377
|
//#endregion
|
|
31505
|
-
//#region src/
|
|
31378
|
+
//#region src/keyboard.ts
|
|
31506
31379
|
/**
|
|
31507
|
-
*
|
|
31508
|
-
*
|
|
31509
|
-
* kill + yank, and the shell-recall boundary gate that keeps Up/Down usable
|
|
31510
|
-
* inside a multiline draft.
|
|
31511
|
-
*
|
|
31512
|
-
* The model is intentionally string-offset based (UTF-16 indices clamped to
|
|
31513
|
-
* grapheme boundaries) so the React state stays two primitives
|
|
31514
|
-
* (value, cursor) and every operation here stays pure and testable.
|
|
31380
|
+
* Keyboard enhancement protocol (Codex `keyboard_modes` parity) and the
|
|
31381
|
+
* kitty CSI-u normalization layer.
|
|
31515
31382
|
*
|
|
31516
|
-
*
|
|
31517
|
-
*
|
|
31383
|
+
* The TUI pushes the kitty keyboard protocol with DISAMBIGUATE_ESCAPE_CODES
|
|
31384
|
+
* and REPORT_ALTERNATE_KEYS (flags 1|4 = `\x1b[>5u`). Event types are
|
|
31385
|
+
* deliberately NOT requested: Ink 5's parser cannot decode the
|
|
31386
|
+
* `:event-type` suffix, and repeat/release reporting buys this surface
|
|
31387
|
+
* nothing.
|
|
31518
31388
|
*
|
|
31519
|
-
*
|
|
31520
|
-
|
|
31521
|
-
|
|
31522
|
-
|
|
31523
|
-
*
|
|
31524
|
-
*
|
|
31525
|
-
* (emoji) atomic so the cursor can never split one.
|
|
31389
|
+
* Ink 5 also cannot parse most CSI-u forms at all — they fall through its
|
|
31390
|
+
* regex as unnamed sequences and get INSERTED AS DRAFT TEXT. The composer's
|
|
31391
|
+
* stdin read patch therefore rewrites every CSI-u form it can decode back
|
|
31392
|
+
* to the legacy byte or canonical sequence the existing key handling
|
|
31393
|
+
* already understands, before Ink ever parses the chunk.
|
|
31394
|
+
* @module @deepseek-ai/dsh-code/keyboard
|
|
31526
31395
|
*/
|
|
31527
|
-
|
|
31528
|
-
|
|
31529
|
-
|
|
31530
|
-
|
|
31531
|
-
|
|
31532
|
-
|
|
31533
|
-
|
|
31534
|
-
|
|
31535
|
-
|
|
31536
|
-
|
|
31537
|
-
return spans;
|
|
31538
|
-
}
|
|
31539
|
-
let start = 0;
|
|
31540
|
-
for (const char of text) {
|
|
31541
|
-
spans.push({
|
|
31542
|
-
text: char,
|
|
31543
|
-
start,
|
|
31544
|
-
end: start + char.length,
|
|
31545
|
-
width: visibleColumns(char)
|
|
31546
|
-
});
|
|
31547
|
-
start += char.length;
|
|
31548
|
-
}
|
|
31549
|
-
return spans;
|
|
31396
|
+
/** Push keyboard enhancement (modifyOtherKeys off, kitty flags 1|4). */
|
|
31397
|
+
const KEYBOARD_ENHANCE_ENABLE = "\x1B[>4;0m\x1B[>5u";
|
|
31398
|
+
/** Pop the enhancement stack and reset modifyOtherKeys (exit path). */
|
|
31399
|
+
const KEYBOARD_ENHANCE_DISABLE = "\x1B[<u\x1B[>4;0m";
|
|
31400
|
+
const DSH_ENABLE_KEYBOARD_ENHANCEMENT = "DSH_ENABLE_KEYBOARD_ENHANCEMENT";
|
|
31401
|
+
function parseBooleanEnv(value) {
|
|
31402
|
+
if (value === void 0) return void 0;
|
|
31403
|
+
const normalized = value.trim().toLowerCase();
|
|
31404
|
+
if (normalized === "1" || normalized === "true" || normalized === "yes") return true;
|
|
31405
|
+
if (normalized === "0" || normalized === "false" || normalized === "no") return false;
|
|
31550
31406
|
}
|
|
31551
|
-
/**
|
|
31552
|
-
function
|
|
31553
|
-
|
|
31554
|
-
const span = spans[index];
|
|
31555
|
-
if (span.end <= offset) return span.end;
|
|
31556
|
-
if (span.start < offset) return span.start;
|
|
31557
|
-
}
|
|
31558
|
-
return 0;
|
|
31407
|
+
/** True when the process is running inside the VS Code integrated terminal. */
|
|
31408
|
+
function isVsCodeTerminalEnv(env = process.env) {
|
|
31409
|
+
return env.TERM_PROGRAM?.trim().toLowerCase() === "vscode" || env.VSCODE_INJECTION === "1";
|
|
31559
31410
|
}
|
|
31560
|
-
/**
|
|
31561
|
-
function
|
|
31562
|
-
if (
|
|
31563
|
-
const
|
|
31564
|
-
if (
|
|
31565
|
-
|
|
31566
|
-
const down = floorBoundary(spans, target);
|
|
31567
|
-
if (down === target) return target;
|
|
31568
|
-
const up = spans.find((span) => span.start >= target)?.start ?? value.length;
|
|
31569
|
-
return up - target < target - down ? up : down;
|
|
31411
|
+
/** Whether to push Kitty keyboard enhancement for the current terminal. */
|
|
31412
|
+
function shouldEnableKeyboardEnhancement(env = process.env) {
|
|
31413
|
+
if (parseBooleanEnv(env["DSH_DISABLE_KEYBOARD_ENHANCEMENT"]) === true) return false;
|
|
31414
|
+
const explicitEnable = parseBooleanEnv(env[DSH_ENABLE_KEYBOARD_ENHANCEMENT]);
|
|
31415
|
+
if (explicitEnable !== void 0) return explicitEnable;
|
|
31416
|
+
return !isVsCodeTerminalEnv(env);
|
|
31570
31417
|
}
|
|
31418
|
+
/** Enable bracketed paste reporting. */
|
|
31419
|
+
const BRACKETED_PASTE_ENABLE = "\x1B[?2004h";
|
|
31420
|
+
/** Disable bracketed paste reporting. */
|
|
31421
|
+
const BRACKETED_PASTE_DISABLE = "\x1B[?2004l";
|
|
31422
|
+
/** Enable terminal focus-in/focus-out reporting (xterm focus protocol). */
|
|
31423
|
+
const TERMINAL_FOCUS_REPORT_ENABLE = "\x1B[?1004h";
|
|
31424
|
+
/** Disable terminal focus-in/focus-out reporting. */
|
|
31425
|
+
const TERMINAL_FOCUS_REPORT_DISABLE = "\x1B[?1004l";
|
|
31571
31426
|
/**
|
|
31572
|
-
*
|
|
31573
|
-
*
|
|
31574
|
-
* lone trailing code unit behind.
|
|
31427
|
+
* Remove xterm focus reports from one input chunk and update the caller's
|
|
31428
|
+
* focus state. Focus reports are terminal protocol, not composer text.
|
|
31575
31429
|
*/
|
|
31576
|
-
function
|
|
31577
|
-
|
|
31578
|
-
|
|
31579
|
-
|
|
31430
|
+
function stripTerminalFocusEvents(chunk, onFocus) {
|
|
31431
|
+
return chunk.replace(/\x1b\[(I|O)/gu, (_whole, event) => {
|
|
31432
|
+
onFocus(event === "I");
|
|
31433
|
+
return "";
|
|
31434
|
+
});
|
|
31580
31435
|
}
|
|
31436
|
+
/** Bracketed paste markers as Ink delivers them (it strips the leading ESC). */
|
|
31437
|
+
const PASTE_START_MARKER = "[200~";
|
|
31438
|
+
const PASTE_END_MARKER = "[201~";
|
|
31581
31439
|
/**
|
|
31582
|
-
*
|
|
31583
|
-
*
|
|
31440
|
+
* How long an unterminated bracketed-paste block may hold buffered bytes
|
|
31441
|
+
* before the input splitter strips its start marker and releases them: a
|
|
31442
|
+
* terminal that loses the end marker must never take the whole keyboard
|
|
31443
|
+
* hostage (Esc/Ctrl+C included). Shared by the splitter and the composer's
|
|
31444
|
+
* lost-paste safety net so both use one window.
|
|
31584
31445
|
*/
|
|
31585
|
-
|
|
31586
|
-
if (delta === 0 || value === "") return clampCursor(value, offset);
|
|
31587
|
-
const spans = splitGraphemes(value);
|
|
31588
|
-
const boundaries = [0];
|
|
31589
|
-
for (const span of spans) boundaries.push(span.end);
|
|
31590
|
-
const current = boundaries.indexOf(clampCursor(value, offset));
|
|
31591
|
-
if (current === -1) return clampCursor(value, offset);
|
|
31592
|
-
return boundaries[Math.max(0, Math.min(boundaries.length - 1, current + delta))];
|
|
31593
|
-
}
|
|
31446
|
+
const PASTE_BRACKET_TIMEOUT_MS = 1e3;
|
|
31594
31447
|
/**
|
|
31595
|
-
*
|
|
31596
|
-
*
|
|
31597
|
-
*
|
|
31598
|
-
* draft is data, so a stray ESC (Windows Terminal file drops) disappears
|
|
31599
|
-
* instead of rendering as literal backslash-x-1-b text. Newlines survive.
|
|
31448
|
+
* Remove bracketed paste markers from one input chunk. Panel drafts accept raw
|
|
31449
|
+
* `input` text, where an unhandled paste would otherwise persist the literal
|
|
31450
|
+
* "[200~"/"[201~" markers Ink leaves after stripping the ESC byte.
|
|
31600
31451
|
*/
|
|
31601
|
-
function
|
|
31602
|
-
return text.replaceAll(
|
|
31452
|
+
function stripPasteMarkers(text) {
|
|
31453
|
+
return text.replaceAll(`\x1b${PASTE_START_MARKER}`, "").replaceAll(`\x1b${PASTE_END_MARKER}`, "").replaceAll(PASTE_START_MARKER, "").replaceAll(PASTE_END_MARKER, "");
|
|
31603
31454
|
}
|
|
31604
|
-
/**
|
|
31605
|
-
|
|
31606
|
-
|
|
31607
|
-
|
|
31608
|
-
|
|
31609
|
-
|
|
31610
|
-
|
|
31611
|
-
|
|
31612
|
-
|
|
31613
|
-
|
|
31614
|
-
|
|
31615
|
-
|
|
31616
|
-
|
|
31617
|
-
|
|
31618
|
-
|
|
31619
|
-
|
|
31620
|
-
|
|
31621
|
-
|
|
31622
|
-
|
|
31623
|
-
|
|
31624
|
-
|
|
31625
|
-
|
|
31626
|
-
|
|
31627
|
-
|
|
31628
|
-
|
|
31629
|
-
|
|
31630
|
-
offsets.length = 0;
|
|
31631
|
-
rowColumns.length = 0;
|
|
31632
|
-
rowCuts.length = 0;
|
|
31633
|
-
};
|
|
31634
|
-
for (const span of spans) {
|
|
31635
|
-
if (span.text === "\n") {
|
|
31636
|
-
flush(span.start);
|
|
31637
|
-
start = span.end;
|
|
31638
|
-
offsets.push(span.end);
|
|
31639
|
-
rowColumns.push(0);
|
|
31640
|
-
rowCuts.push(0);
|
|
31641
|
-
continue;
|
|
31642
|
-
}
|
|
31643
|
-
if (used > 0 && used + span.width > width) {
|
|
31644
|
-
flush(span.start);
|
|
31645
|
-
start = span.start;
|
|
31646
|
-
offsets.push(span.start);
|
|
31647
|
-
rowColumns.push(0);
|
|
31648
|
-
rowCuts.push(0);
|
|
31649
|
-
}
|
|
31650
|
-
text += span.text;
|
|
31651
|
-
used += span.width;
|
|
31652
|
-
offsets.push(span.end);
|
|
31653
|
-
rowColumns.push(used);
|
|
31654
|
-
rowCuts.push(text.length);
|
|
31455
|
+
/** Match one CSI-u sequence (code, optional ;modifiers, then :event or ;alternate). */
|
|
31456
|
+
const CSI_U_SOURCE = "\x1B\\[(\\d+)(?:;(\\d+))?(?:(:|;)(\\d+))?u";
|
|
31457
|
+
/** Kitty private-use keycodes for the numeric keypad and keypad Enter. */
|
|
31458
|
+
const KITTY_KEYPAD_CODES = {
|
|
31459
|
+
57399: "0",
|
|
31460
|
+
57400: "1",
|
|
31461
|
+
57401: "2",
|
|
31462
|
+
57402: "3",
|
|
31463
|
+
57403: "4",
|
|
31464
|
+
57404: "5",
|
|
31465
|
+
57405: "6",
|
|
31466
|
+
57406: "7",
|
|
31467
|
+
57407: "8",
|
|
31468
|
+
57408: "9",
|
|
31469
|
+
57414: "\r"
|
|
31470
|
+
};
|
|
31471
|
+
/** Legacy equivalent for one decoded CSI-u key, or undefined to pass through. */
|
|
31472
|
+
function legacyForKey(key) {
|
|
31473
|
+
const bits = Math.max(0, key.modifiers - 1);
|
|
31474
|
+
const shift = (bits & 1) !== 0;
|
|
31475
|
+
const alt = (bits & 2) !== 0;
|
|
31476
|
+
const ctrl = (bits & 4) !== 0;
|
|
31477
|
+
if (key.code === 13) {
|
|
31478
|
+
if (ctrl || shift) return "\n";
|
|
31479
|
+
if (alt) return "\x1B\r";
|
|
31480
|
+
return "\r";
|
|
31655
31481
|
}
|
|
31656
|
-
if (
|
|
31657
|
-
|
|
31658
|
-
|
|
31659
|
-
|
|
31660
|
-
|
|
31661
|
-
|
|
31662
|
-
|
|
31663
|
-
|
|
31482
|
+
if (key.code === 27) return "\x1B";
|
|
31483
|
+
if (key.code === 9) return shift ? "\x1B[Z" : " ";
|
|
31484
|
+
if (key.code === 127) return alt || ctrl ? "\x1B" : "";
|
|
31485
|
+
const keypad = KITTY_KEYPAD_CODES[key.code];
|
|
31486
|
+
if (keypad !== void 0) {
|
|
31487
|
+
if (alt) return "\x1B" + keypad;
|
|
31488
|
+
return keypad;
|
|
31489
|
+
}
|
|
31490
|
+
if (key.code >= 1 && key.code <= 6) {
|
|
31491
|
+
const mask = (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0);
|
|
31492
|
+
const mods = mask === 0 ? "" : `;${mask + 1}`;
|
|
31493
|
+
if (key.code === 1) return mods === "" ? "\x1B[H" : `\x1b[1${mods}H`;
|
|
31494
|
+
if (key.code === 4) return mods === "" ? "\x1B[F" : `\x1b[1${mods}F`;
|
|
31495
|
+
return `\x1b[${key.code}${mods}~`;
|
|
31496
|
+
}
|
|
31497
|
+
if (key.code >= 97 && key.code <= 122) {
|
|
31498
|
+
const letter = String.fromCodePoint(key.code);
|
|
31499
|
+
if (ctrl) return String.fromCodePoint(key.code - 96);
|
|
31500
|
+
if (alt) return "\x1B" + letter;
|
|
31501
|
+
if (shift) return String.fromCodePoint(key.alternate ?? key.code - 32);
|
|
31502
|
+
return letter;
|
|
31503
|
+
}
|
|
31504
|
+
if (key.code >= 65 && key.code <= 90) {
|
|
31505
|
+
if (ctrl) return String.fromCodePoint(key.code + 32 - 96);
|
|
31506
|
+
if (alt) return "\x1B" + String.fromCodePoint(key.code + 32);
|
|
31507
|
+
return String.fromCodePoint(key.code);
|
|
31508
|
+
}
|
|
31509
|
+
if (key.code >= 32 && key.code <= 126) {
|
|
31510
|
+
const base = key.alternate ?? key.code;
|
|
31511
|
+
if (ctrl && base - 96 >= 1 && base - 96 <= 26) return String.fromCodePoint(base - 96);
|
|
31512
|
+
if (alt) return "\x1B" + String.fromCodePoint(base);
|
|
31513
|
+
return String.fromCodePoint(base);
|
|
31514
|
+
}
|
|
31515
|
+
}
|
|
31516
|
+
/**
|
|
31517
|
+
* Rewrite every decodable kitty CSI-u sequence in one stdin chunk to the
|
|
31518
|
+
* legacy form the input layer already handles. Undecodable or non-key
|
|
31519
|
+
* sequences pass through untouched, so terminals without the protocol are
|
|
31520
|
+
* unaffected.
|
|
31521
|
+
*/
|
|
31522
|
+
function normalizeKeyboardChunk(chunk) {
|
|
31523
|
+
if (!chunk.includes("\x1B[") || !chunk.includes("u")) return chunk;
|
|
31524
|
+
const pattern = new RegExp(CSI_U_SOURCE, "g");
|
|
31525
|
+
return chunk.replace(pattern, (whole, code, mods, separator, third) => {
|
|
31526
|
+
return legacyForKey({
|
|
31527
|
+
code: Number.parseInt(code, 10),
|
|
31528
|
+
modifiers: mods === void 0 || mods === "" ? 1 : Math.max(1, Number.parseInt(mods, 10)),
|
|
31529
|
+
alternate: separator === ";" && third !== void 0 && third !== "" ? Number.parseInt(third, 10) : void 0
|
|
31530
|
+
}) ?? whole;
|
|
31664
31531
|
});
|
|
31665
|
-
return {
|
|
31666
|
-
rows,
|
|
31667
|
-
length: value.length
|
|
31668
|
-
};
|
|
31669
31532
|
}
|
|
31670
|
-
|
|
31671
|
-
|
|
31672
|
-
|
|
31673
|
-
|
|
31674
|
-
|
|
31675
|
-
|
|
31676
|
-
|
|
31533
|
+
const HOME_SEQUENCES = [
|
|
31534
|
+
"\x1B[H",
|
|
31535
|
+
"\x1B[1~",
|
|
31536
|
+
"\x1B[7~",
|
|
31537
|
+
"\x1BOH"
|
|
31538
|
+
];
|
|
31539
|
+
const END_SEQUENCES = [
|
|
31540
|
+
"\x1B[F",
|
|
31541
|
+
"\x1B[4~",
|
|
31542
|
+
"\x1B[8~",
|
|
31543
|
+
"\x1BOF"
|
|
31544
|
+
];
|
|
31545
|
+
/** Parse one CSI functional-key sequence at `offset`. */
|
|
31546
|
+
function functionalToken(chunk, offset) {
|
|
31547
|
+
const tail = chunk.slice(offset);
|
|
31548
|
+
for (const sequence of HOME_SEQUENCES) if (tail.startsWith(sequence)) return {
|
|
31549
|
+
token: { kind: "home" },
|
|
31550
|
+
length: sequence.length
|
|
31677
31551
|
};
|
|
31678
|
-
const
|
|
31679
|
-
|
|
31680
|
-
|
|
31681
|
-
caret: "",
|
|
31682
|
-
after: row.text,
|
|
31683
|
-
hasCaret: false
|
|
31552
|
+
for (const sequence of END_SEQUENCES) if (tail.startsWith(sequence)) return {
|
|
31553
|
+
token: { kind: "end" },
|
|
31554
|
+
length: sequence.length
|
|
31684
31555
|
};
|
|
31685
|
-
|
|
31686
|
-
|
|
31687
|
-
|
|
31688
|
-
|
|
31689
|
-
hasCaret: true
|
|
31556
|
+
const modifiedHome = /^\x1b\[1;(\d+)H/u.exec(tail);
|
|
31557
|
+
if (modifiedHome !== null) return {
|
|
31558
|
+
token: { kind: "home" },
|
|
31559
|
+
length: modifiedHome[0].length
|
|
31690
31560
|
};
|
|
31691
|
-
|
|
31692
|
-
|
|
31693
|
-
|
|
31694
|
-
|
|
31695
|
-
|
|
31696
|
-
|
|
31697
|
-
|
|
31698
|
-
|
|
31699
|
-
|
|
31700
|
-
|
|
31561
|
+
const modifiedEnd = /^\x1b\[1;(\d+)F/u.exec(tail);
|
|
31562
|
+
if (modifiedEnd !== null) return {
|
|
31563
|
+
token: { kind: "end" },
|
|
31564
|
+
length: modifiedEnd[0].length
|
|
31565
|
+
};
|
|
31566
|
+
const modifiedDelete = /^\x1b\[3(?:;(\d+))?~/u.exec(tail);
|
|
31567
|
+
if (modifiedDelete !== null) {
|
|
31568
|
+
const modifiers = Number.parseInt(modifiedDelete[1] ?? "1", 10) - 1;
|
|
31569
|
+
return {
|
|
31570
|
+
token: { kind: (modifiers & 2) !== 0 || (modifiers & 4) !== 0 ? "delete-word-forward" : "delete-forward" },
|
|
31571
|
+
length: modifiedDelete[0].length
|
|
31701
31572
|
};
|
|
31702
31573
|
}
|
|
31703
|
-
const last = model.rows[model.rows.length - 1];
|
|
31704
|
-
return {
|
|
31705
|
-
row: model.rows.length - 1,
|
|
31706
|
-
column: last === void 0 ? 0 : last.columns[last.columns.length - 1]
|
|
31707
|
-
};
|
|
31708
31574
|
}
|
|
31709
31575
|
/**
|
|
31710
|
-
*
|
|
31711
|
-
*
|
|
31712
|
-
*
|
|
31576
|
+
* Tokenize a stdin chunk containing at least one editor-only key. Ink calls
|
|
31577
|
+
* `useInput` once for a pasted/batched chunk, so preserving each action here
|
|
31578
|
+
* prevents repeated Backspace/Home/End/Delete presses from collapsing into
|
|
31579
|
+
* one blurred key event. Unknown escape sequences return undefined and stay
|
|
31580
|
+
* under Ink's ownership.
|
|
31713
31581
|
*/
|
|
31714
|
-
function
|
|
31715
|
-
const
|
|
31716
|
-
|
|
31717
|
-
|
|
31718
|
-
|
|
31719
|
-
|
|
31720
|
-
|
|
31721
|
-
|
|
31722
|
-
|
|
31723
|
-
|
|
31724
|
-
|
|
31725
|
-
}
|
|
31726
|
-
/** The start/end offsets of the logical line containing the cursor. */
|
|
31727
|
-
function lineBounds(value, offset) {
|
|
31728
|
-
const target = Math.max(0, Math.min(value.length, offset));
|
|
31729
|
-
const start = value.lastIndexOf("\n", Math.max(0, target - 1)) + 1;
|
|
31730
|
-
const end = value.indexOf("\n", target);
|
|
31731
|
-
return {
|
|
31732
|
-
start,
|
|
31733
|
-
end: end === -1 ? value.length : end
|
|
31582
|
+
function tokenizeRawEditorChunk(chunk) {
|
|
31583
|
+
const tokens = [];
|
|
31584
|
+
let text = "";
|
|
31585
|
+
let special = false;
|
|
31586
|
+
const flushText = () => {
|
|
31587
|
+
if (text === "") return;
|
|
31588
|
+
tokens.push({
|
|
31589
|
+
kind: "text",
|
|
31590
|
+
text
|
|
31591
|
+
});
|
|
31592
|
+
text = "";
|
|
31734
31593
|
};
|
|
31735
|
-
|
|
31736
|
-
|
|
31737
|
-
|
|
31738
|
-
|
|
31739
|
-
|
|
31740
|
-
|
|
31741
|
-
if (/^\s$/u.test(text)) return "space";
|
|
31742
|
-
return WORD_SEPARATORS.has(text) || UNICODE_PUNCTUATION.test(text) ? "punct" : "word";
|
|
31743
|
-
}
|
|
31744
|
-
/** Maximal same-class runs; Han graphemes deliberately stay one run each. */
|
|
31745
|
-
function pieceRuns(value) {
|
|
31746
|
-
const runs = [];
|
|
31747
|
-
let current;
|
|
31748
|
-
for (const span of splitGraphemes(value)) {
|
|
31749
|
-
const klass = span.text === "\n" ? "space" : classifyGrapheme(span.text);
|
|
31750
|
-
const atomic = klass === "word" && HAN_GRAPHEME.test(span.text);
|
|
31751
|
-
if (current !== void 0 && current.class === klass && !current.atomic && !atomic) {
|
|
31752
|
-
current.end = span.end;
|
|
31594
|
+
for (let offset = 0; offset < chunk.length;) {
|
|
31595
|
+
if (chunk.startsWith("\x1B", offset) || chunk.startsWith("\x1B\b", offset)) {
|
|
31596
|
+
flushText();
|
|
31597
|
+
tokens.push({ kind: "delete-word-backward" });
|
|
31598
|
+
special = true;
|
|
31599
|
+
offset += 2;
|
|
31753
31600
|
continue;
|
|
31754
31601
|
}
|
|
31755
|
-
|
|
31756
|
-
|
|
31757
|
-
|
|
31758
|
-
|
|
31759
|
-
|
|
31760
|
-
|
|
31761
|
-
|
|
31602
|
+
const functional = functionalToken(chunk, offset);
|
|
31603
|
+
if (functional !== void 0) {
|
|
31604
|
+
flushText();
|
|
31605
|
+
tokens.push(functional.token);
|
|
31606
|
+
special = true;
|
|
31607
|
+
offset += functional.length;
|
|
31608
|
+
continue;
|
|
31609
|
+
}
|
|
31610
|
+
const char = chunk[offset];
|
|
31611
|
+
if (char === "" || char === "\b") {
|
|
31612
|
+
flushText();
|
|
31613
|
+
tokens.push({ kind: "delete-backward" });
|
|
31614
|
+
special = true;
|
|
31615
|
+
offset += 1;
|
|
31616
|
+
continue;
|
|
31617
|
+
}
|
|
31618
|
+
if (char === "\x1B") return void 0;
|
|
31619
|
+
text += char;
|
|
31620
|
+
offset += 1;
|
|
31762
31621
|
}
|
|
31763
|
-
|
|
31622
|
+
flushText();
|
|
31623
|
+
return special ? tokens : void 0;
|
|
31764
31624
|
}
|
|
31625
|
+
//#endregion
|
|
31626
|
+
//#region src/render/status.ts
|
|
31765
31627
|
/**
|
|
31766
|
-
*
|
|
31767
|
-
*
|
|
31628
|
+
* Status-bar composition for the TUI footer. Codex/Claude-Code-style split
|
|
31629
|
+
* line: identity facts and session figures flow from the left, while the
|
|
31630
|
+
* permission badge (the Codex "autonomous selection" anchor, with its
|
|
31631
|
+
* shift+tab cycle hint) pins to the right edge. Every segment carries a tone
|
|
31632
|
+
* the footer maps to a theme color, and layoutStatusBar degrades the line
|
|
31633
|
+
* item by item so it always fits one physical row — truncation with an
|
|
31634
|
+
* ellipsis happens only after every lesser group has already dropped out.
|
|
31635
|
+
*
|
|
31636
|
+
* @module @deepseek-ai/dsh-tui/render/status
|
|
31768
31637
|
*/
|
|
31769
|
-
function moveWordLeft(value, offset) {
|
|
31770
|
-
const cursor = clampCursor(value, offset);
|
|
31771
|
-
const runs = pieceRuns(value);
|
|
31772
|
-
let index = runs.findLastIndex((run) => run.start < cursor);
|
|
31773
|
-
if (index < 0) return 0;
|
|
31774
|
-
if (runs[index].class === "space") {
|
|
31775
|
-
index -= 1;
|
|
31776
|
-
if (index < 0) return 0;
|
|
31777
|
-
}
|
|
31778
|
-
let target = index;
|
|
31779
|
-
while (target > 0 && runs[target].class === "punct" && runs[target - 1].class === "punct") target -= 1;
|
|
31780
|
-
return runs[target].start;
|
|
31781
|
-
}
|
|
31782
31638
|
/**
|
|
31783
|
-
*
|
|
31784
|
-
*
|
|
31639
|
+
* Compact duration: 45.2s under a minute, 2m42s from there on.
|
|
31640
|
+
* @param ms - duration in milliseconds.
|
|
31641
|
+
* @returns display string.
|
|
31785
31642
|
*/
|
|
31786
|
-
function
|
|
31787
|
-
const
|
|
31788
|
-
|
|
31789
|
-
|
|
31790
|
-
|
|
31791
|
-
|
|
31792
|
-
|
|
31793
|
-
|
|
31794
|
-
}
|
|
31795
|
-
let target = index;
|
|
31796
|
-
while (target < runs.length - 1 && runs[target].class === "punct" && runs[target + 1].class === "punct") target += 1;
|
|
31797
|
-
return runs[target].end;
|
|
31798
|
-
}
|
|
31799
|
-
function replaceRange(value, cursor, start, end, killed) {
|
|
31800
|
-
const from = Math.min(start, end);
|
|
31801
|
-
const to = Math.max(start, end);
|
|
31802
|
-
if (from >= to) return {
|
|
31803
|
-
value,
|
|
31804
|
-
cursor,
|
|
31805
|
-
killed: void 0
|
|
31806
|
-
};
|
|
31807
|
-
const text = value.slice(from, to);
|
|
31808
|
-
return {
|
|
31809
|
-
value: value.slice(0, from) + value.slice(to),
|
|
31810
|
-
cursor: Math.max(0, cursor - Math.max(0, Math.min(cursor, to) - from)),
|
|
31811
|
-
killed: killed ? text : void 0
|
|
31812
|
-
};
|
|
31813
|
-
}
|
|
31814
|
-
/** Delete the grapheme cluster before the cursor. */
|
|
31815
|
-
function deleteBackward(value, cursor) {
|
|
31816
|
-
if (cursor === 0) return {
|
|
31817
|
-
value,
|
|
31818
|
-
cursor,
|
|
31819
|
-
killed: void 0
|
|
31820
|
-
};
|
|
31821
|
-
const spans = splitGraphemes(value.slice(0, cursor));
|
|
31822
|
-
return replaceRange(value, cursor, spans.length === 0 ? 0 : spans[spans.length - 1].start, cursor, false);
|
|
31823
|
-
}
|
|
31824
|
-
/** Delete the grapheme cluster at the cursor. */
|
|
31825
|
-
function deleteForward(value, cursor) {
|
|
31826
|
-
if (cursor >= value.length) return {
|
|
31827
|
-
value,
|
|
31828
|
-
cursor,
|
|
31829
|
-
killed: void 0
|
|
31830
|
-
};
|
|
31831
|
-
const span = splitGraphemes(value).find((candidate) => candidate.start >= cursor);
|
|
31832
|
-
return replaceRange(value, cursor, cursor, span === void 0 ? value.length : span.end, false);
|
|
31833
|
-
}
|
|
31834
|
-
/** Delete back to the start of the previous word (fills the kill buffer). */
|
|
31835
|
-
function deleteWordBackward(value, cursor) {
|
|
31836
|
-
return replaceRange(value, cursor, moveWordLeft(value, cursor), cursor, true);
|
|
31837
|
-
}
|
|
31838
|
-
/** Delete forward to the end of the next word (fills the kill buffer). */
|
|
31839
|
-
function deleteWordForward(value, cursor) {
|
|
31840
|
-
return replaceRange(value, cursor, cursor, moveWordRight(value, cursor), true);
|
|
31841
|
-
}
|
|
31842
|
-
/** Ctrl+U: kill from the line start to the cursor; at BOL, kill the newline. */
|
|
31843
|
-
function killToLineStart(value, cursor) {
|
|
31844
|
-
const bounds = lineBounds(value, cursor);
|
|
31845
|
-
if (cursor > bounds.start) return replaceRange(value, cursor, bounds.start, cursor, true);
|
|
31846
|
-
if (bounds.start > 0) return replaceRange(value, cursor, bounds.start - 1, bounds.start, true);
|
|
31847
|
-
return {
|
|
31848
|
-
value,
|
|
31849
|
-
cursor,
|
|
31850
|
-
killed: void 0
|
|
31851
|
-
};
|
|
31852
|
-
}
|
|
31853
|
-
/** Ctrl+K: kill from the cursor to the line end; at EOL, kill the newline. */
|
|
31854
|
-
function killToLineEnd(value, cursor) {
|
|
31855
|
-
const bounds = lineBounds(value, cursor);
|
|
31856
|
-
if (cursor < bounds.end) return replaceRange(value, cursor, cursor, bounds.end, true);
|
|
31857
|
-
if (bounds.end < value.length) return replaceRange(value, cursor, bounds.end, bounds.end + 1, true);
|
|
31858
|
-
return {
|
|
31859
|
-
value,
|
|
31860
|
-
cursor,
|
|
31861
|
-
killed: void 0
|
|
31862
|
-
};
|
|
31863
|
-
}
|
|
31864
|
-
/** Insert sanitized text at the cursor. */
|
|
31865
|
-
function insertText(value, cursor, text) {
|
|
31866
|
-
const safe = sanitizeDraftText(text);
|
|
31867
|
-
if (safe === "") return {
|
|
31868
|
-
value,
|
|
31869
|
-
cursor,
|
|
31870
|
-
killed: void 0
|
|
31871
|
-
};
|
|
31872
|
-
return {
|
|
31873
|
-
value: value.slice(0, cursor) + safe + value.slice(cursor),
|
|
31874
|
-
cursor: cursor + safe.length,
|
|
31875
|
-
killed: void 0
|
|
31876
|
-
};
|
|
31877
|
-
}
|
|
31878
|
-
/** Ctrl+A: current logical line start, then the previous line start on repeat. */
|
|
31879
|
-
function moveToLineStart(value, cursor, crossOnRepeat) {
|
|
31880
|
-
const site = clampCursor(value, cursor);
|
|
31881
|
-
const bounds = lineBounds(value, site);
|
|
31882
|
-
if (!crossOnRepeat || site !== bounds.start || bounds.start === 0) return bounds.start;
|
|
31883
|
-
return lineBounds(value, bounds.start - 1).start;
|
|
31884
|
-
}
|
|
31885
|
-
/** Ctrl+E: current logical line end, then the next line end on repeat. */
|
|
31886
|
-
function moveToLineEnd(value, cursor, crossOnRepeat) {
|
|
31887
|
-
const site = clampCursor(value, cursor);
|
|
31888
|
-
const bounds = lineBounds(value, site);
|
|
31889
|
-
if (!crossOnRepeat || site !== bounds.end || bounds.end >= value.length) return bounds.end;
|
|
31890
|
-
return lineBounds(value, bounds.end + 1).end;
|
|
31643
|
+
function formatDuration(ms) {
|
|
31644
|
+
const s = ms / 1e3;
|
|
31645
|
+
if (s < 60) return String(Math.round(s * 10) / 10) + "s";
|
|
31646
|
+
const whole = Math.round(s);
|
|
31647
|
+
const hours = Math.floor(whole / 3600);
|
|
31648
|
+
const minutes = Math.floor(whole % 3600 / 60);
|
|
31649
|
+
const seconds = whole % 60;
|
|
31650
|
+
if (hours > 0) return `${hours}h${minutes}m${seconds === 0 ? "" : `${seconds}s`}`;
|
|
31651
|
+
return `${minutes}m${seconds}s`;
|
|
31891
31652
|
}
|
|
31892
31653
|
/**
|
|
31893
|
-
*
|
|
31894
|
-
*
|
|
31895
|
-
*
|
|
31654
|
+
* Compact decode rate: one decimal under a hundred, whole below a thousand,
|
|
31655
|
+
* then thousands (15.3 / 124 / 1.2K).
|
|
31656
|
+
* @param n - tokens per second.
|
|
31657
|
+
* @returns display string.
|
|
31896
31658
|
*/
|
|
31897
|
-
function
|
|
31898
|
-
|
|
31899
|
-
|
|
31900
|
-
|
|
31901
|
-
start,
|
|
31902
|
-
end
|
|
31903
|
-
};
|
|
31904
|
-
let prefix = 0;
|
|
31905
|
-
const shared = Math.min(original.length, current.length);
|
|
31906
|
-
while (prefix < shared && original[prefix] === current[prefix]) prefix += 1;
|
|
31907
|
-
let suffix = 0;
|
|
31908
|
-
while (suffix < original.length - prefix && suffix < current.length - prefix && original[original.length - 1 - suffix] === current[current.length - 1 - suffix]) suffix += 1;
|
|
31909
|
-
const oldChangedEnd = original.length - suffix;
|
|
31910
|
-
if (prefix >= end) return {
|
|
31911
|
-
start,
|
|
31912
|
-
end
|
|
31913
|
-
};
|
|
31914
|
-
if (oldChangedEnd <= start) {
|
|
31915
|
-
const delta = current.length - original.length;
|
|
31916
|
-
return {
|
|
31917
|
-
start: start + delta,
|
|
31918
|
-
end: end + delta
|
|
31919
|
-
};
|
|
31920
|
-
}
|
|
31921
|
-
}
|
|
31922
|
-
/** Replace a current range while preserving a cursor moved after capture. */
|
|
31923
|
-
function replaceRangePreservingCursor(value, cursor, range, replacement) {
|
|
31924
|
-
const start = Math.max(0, Math.min(value.length, range.start));
|
|
31925
|
-
const end = Math.max(start, Math.min(value.length, range.end));
|
|
31926
|
-
const site = clampCursor(value, cursor);
|
|
31927
|
-
const nextCursor = site <= start ? site : site >= end ? site + replacement.length - (end - start) : start + replacement.length;
|
|
31928
|
-
return {
|
|
31929
|
-
value: value.slice(0, start) + replacement + value.slice(end),
|
|
31930
|
-
cursor: nextCursor,
|
|
31931
|
-
killed: void 0
|
|
31932
|
-
};
|
|
31659
|
+
function formatRate(n) {
|
|
31660
|
+
if (n < 100) return String(Math.round(n * 10) / 10);
|
|
31661
|
+
if (n < 1e3) return String(Math.round(n));
|
|
31662
|
+
return String(Math.round(n / 100) / 10) + "K";
|
|
31933
31663
|
}
|
|
31934
31664
|
/**
|
|
31935
|
-
*
|
|
31936
|
-
*
|
|
31937
|
-
*
|
|
31665
|
+
* Cache-hit share of billed prompt-side input. The denominator is the same
|
|
31666
|
+
* billed total the /usage panel shows (uncached input plus both cache
|
|
31667
|
+
* buckets), so the two readouts can never disagree.
|
|
31668
|
+
* @param usage - cumulative token totals.
|
|
31669
|
+
* @returns percent rounded to one decimal place, or null when no input was billed.
|
|
31938
31670
|
*/
|
|
31939
|
-
function
|
|
31940
|
-
|
|
31671
|
+
function cacheHitPercent(usage) {
|
|
31672
|
+
const billed = billedInputTokens(usage);
|
|
31673
|
+
return billed === 0 ? null : Math.round(usage.cacheReadTokens / billed * 1e3) / 10;
|
|
31674
|
+
}
|
|
31675
|
+
/** Separator between trailing state spans. */
|
|
31676
|
+
const STATUS_ITEM_SEPARATOR = " · ";
|
|
31677
|
+
/** Localized mode-cycle hint used by the live layout. */
|
|
31678
|
+
function statusCycleHint() {
|
|
31679
|
+
return ` ${t("status.cycleHint")}`;
|
|
31941
31680
|
}
|
|
31681
|
+
/** Occupancy at which the usage readout flips from brand blue to amber. */
|
|
31682
|
+
const CONTEXT_WARN_PERCENT = 90;
|
|
31942
31683
|
/**
|
|
31943
|
-
*
|
|
31944
|
-
*
|
|
31945
|
-
*
|
|
31946
|
-
* ordinary editing until an edge is reached again.
|
|
31947
|
-
*/
|
|
31948
|
-
function shouldRecallNavigate(value, cursor, lastRecalled, direction) {
|
|
31949
|
-
if (value === "") return direction < 0;
|
|
31950
|
-
if (lastRecalled !== value) return false;
|
|
31951
|
-
return cursor === 0 || cursor === value.length;
|
|
31952
|
-
}
|
|
31953
|
-
//#endregion
|
|
31954
|
-
//#region src/keyboard.ts
|
|
31955
|
-
/**
|
|
31956
|
-
* Keyboard enhancement protocol (Codex `keyboard_modes` parity) and the
|
|
31957
|
-
* kitty CSI-u normalization layer.
|
|
31958
|
-
*
|
|
31959
|
-
* The TUI pushes the kitty keyboard protocol with DISAMBIGUATE_ESCAPE_CODES
|
|
31960
|
-
* and REPORT_ALTERNATE_KEYS (flags 1|4 = `\x1b[>5u`). Event types are
|
|
31961
|
-
* deliberately NOT requested: Ink 5's parser cannot decode the
|
|
31962
|
-
* `:event-type` suffix, and repeat/release reporting buys this surface
|
|
31963
|
-
* nothing.
|
|
31964
|
-
*
|
|
31965
|
-
* Ink 5 also cannot parse most CSI-u forms at all — they fall through its
|
|
31966
|
-
* regex as unnamed sequences and get INSERTED AS DRAFT TEXT. The composer's
|
|
31967
|
-
* stdin read patch therefore rewrites every CSI-u form it can decode back
|
|
31968
|
-
* to the legacy byte or canonical sequence the existing key handling
|
|
31969
|
-
* already understands, before Ink ever parses the chunk.
|
|
31970
|
-
* @module @deepseek-ai/dsh-code/keyboard
|
|
31684
|
+
* Narrowest bar width the drop ladder keeps before dropping the whole
|
|
31685
|
+
* context group: the bar shrinks to this floor first (the absolute readout
|
|
31686
|
+
* survives), and only past it does the readout degrade and the group go.
|
|
31971
31687
|
*/
|
|
31972
|
-
|
|
31973
|
-
const KEYBOARD_ENHANCE_ENABLE = "\x1B[>4;0m\x1B[>5u";
|
|
31974
|
-
/** Pop the enhancement stack and reset modifyOtherKeys (exit path). */
|
|
31975
|
-
const KEYBOARD_ENHANCE_DISABLE = "\x1B[<u\x1B[>4;0m";
|
|
31976
|
-
const DSH_ENABLE_KEYBOARD_ENHANCEMENT = "DSH_ENABLE_KEYBOARD_ENHANCEMENT";
|
|
31977
|
-
function parseBooleanEnv(value) {
|
|
31978
|
-
if (value === void 0) return void 0;
|
|
31979
|
-
const normalized = value.trim().toLowerCase();
|
|
31980
|
-
if (normalized === "1" || normalized === "true" || normalized === "yes") return true;
|
|
31981
|
-
if (normalized === "0" || normalized === "false" || normalized === "no") return false;
|
|
31982
|
-
}
|
|
31983
|
-
/** True when the process is running inside the VS Code integrated terminal. */
|
|
31984
|
-
function isVsCodeTerminalEnv(env = process.env) {
|
|
31985
|
-
return env.TERM_PROGRAM?.trim().toLowerCase() === "vscode" || env.VSCODE_INJECTION === "1";
|
|
31986
|
-
}
|
|
31987
|
-
/** Whether to push Kitty keyboard enhancement for the current terminal. */
|
|
31988
|
-
function shouldEnableKeyboardEnhancement(env = process.env) {
|
|
31989
|
-
if (parseBooleanEnv(env["DSH_DISABLE_KEYBOARD_ENHANCEMENT"]) === true) return false;
|
|
31990
|
-
const explicitEnable = parseBooleanEnv(env[DSH_ENABLE_KEYBOARD_ENHANCEMENT]);
|
|
31991
|
-
if (explicitEnable !== void 0) return explicitEnable;
|
|
31992
|
-
return !isVsCodeTerminalEnv(env);
|
|
31993
|
-
}
|
|
31994
|
-
/** Enable bracketed paste reporting. */
|
|
31995
|
-
const BRACKETED_PASTE_ENABLE = "\x1B[?2004h";
|
|
31996
|
-
/** Disable bracketed paste reporting. */
|
|
31997
|
-
const BRACKETED_PASTE_DISABLE = "\x1B[?2004l";
|
|
31998
|
-
/** Enable terminal focus-in/focus-out reporting (xterm focus protocol). */
|
|
31999
|
-
const TERMINAL_FOCUS_REPORT_ENABLE = "\x1B[?1004h";
|
|
32000
|
-
/** Disable terminal focus-in/focus-out reporting. */
|
|
32001
|
-
const TERMINAL_FOCUS_REPORT_DISABLE = "\x1B[?1004l";
|
|
31688
|
+
const CONTEXT_MIN_WIDTH = 5;
|
|
32002
31689
|
/**
|
|
32003
|
-
*
|
|
32004
|
-
*
|
|
31690
|
+
* Render context occupancy as ONE stepless proportional bar: a solid
|
|
31691
|
+
* DeepSeek-blue fill run tracking the occupancy and a dim dotted free
|
|
31692
|
+
* track for the rest. Nothing else lives inside the bar — the usage
|
|
31693
|
+
* readout rides outside it (see contextGroupSpans) — so the geometry
|
|
31694
|
+
* always reads as the true remaining share. A given occupancy always
|
|
31695
|
+
* renders the identical bar.
|
|
31696
|
+
* @param usedTokens - reported used tokens.
|
|
31697
|
+
* @param contextWindow - route capacity.
|
|
31698
|
+
* @param width - total bar columns.
|
|
31699
|
+
* @returns tone-split spans for the footer to paint.
|
|
32005
31700
|
*/
|
|
32006
|
-
function
|
|
32007
|
-
|
|
32008
|
-
|
|
32009
|
-
|
|
31701
|
+
function contextBar(usedTokens, contextWindow, width) {
|
|
31702
|
+
if (width <= 0 || contextWindow <= 0) return [];
|
|
31703
|
+
const fill = Math.min(width, Math.max(0, Math.round(Math.max(0, usedTokens) / contextWindow * width)));
|
|
31704
|
+
const spans = [];
|
|
31705
|
+
if (fill > 0) spans.push({
|
|
31706
|
+
text: "█".repeat(fill),
|
|
31707
|
+
tone: "ctxFill"
|
|
32010
31708
|
});
|
|
32011
|
-
|
|
32012
|
-
|
|
32013
|
-
|
|
32014
|
-
|
|
32015
|
-
|
|
32016
|
-
|
|
32017
|
-
* before the input splitter strips its start marker and releases them: a
|
|
32018
|
-
* terminal that loses the end marker must never take the whole keyboard
|
|
32019
|
-
* hostage (Esc/Ctrl+C included). Shared by the splitter and the composer's
|
|
32020
|
-
* lost-paste safety net so both use one window.
|
|
32021
|
-
*/
|
|
32022
|
-
const PASTE_BRACKET_TIMEOUT_MS = 1e3;
|
|
32023
|
-
/**
|
|
32024
|
-
* Remove bracketed paste markers from one input chunk. Panel drafts accept raw
|
|
32025
|
-
* `input` text, where an unhandled paste would otherwise persist the literal
|
|
32026
|
-
* "[200~"/"[201~" markers Ink leaves after stripping the ESC byte.
|
|
32027
|
-
*/
|
|
32028
|
-
function stripPasteMarkers(text) {
|
|
32029
|
-
return text.replaceAll(`\x1b${PASTE_START_MARKER}`, "").replaceAll(`\x1b${PASTE_END_MARKER}`, "").replaceAll(PASTE_START_MARKER, "").replaceAll(PASTE_END_MARKER, "");
|
|
32030
|
-
}
|
|
32031
|
-
/** Match one CSI-u sequence (code, optional ;modifiers, then :event or ;alternate). */
|
|
32032
|
-
const CSI_U_SOURCE = "\x1B\\[(\\d+)(?:;(\\d+))?(?:(:|;)(\\d+))?u";
|
|
32033
|
-
/** Kitty private-use keycodes for the numeric keypad and keypad Enter. */
|
|
32034
|
-
const KITTY_KEYPAD_CODES = {
|
|
32035
|
-
57399: "0",
|
|
32036
|
-
57400: "1",
|
|
32037
|
-
57401: "2",
|
|
32038
|
-
57402: "3",
|
|
32039
|
-
57403: "4",
|
|
32040
|
-
57404: "5",
|
|
32041
|
-
57405: "6",
|
|
32042
|
-
57406: "7",
|
|
32043
|
-
57407: "8",
|
|
32044
|
-
57408: "9",
|
|
32045
|
-
57414: "\r"
|
|
32046
|
-
};
|
|
32047
|
-
/** Legacy equivalent for one decoded CSI-u key, or undefined to pass through. */
|
|
32048
|
-
function legacyForKey(key) {
|
|
32049
|
-
const bits = Math.max(0, key.modifiers - 1);
|
|
32050
|
-
const shift = (bits & 1) !== 0;
|
|
32051
|
-
const alt = (bits & 2) !== 0;
|
|
32052
|
-
const ctrl = (bits & 4) !== 0;
|
|
32053
|
-
if (key.code === 13) {
|
|
32054
|
-
if (ctrl || shift) return "\n";
|
|
32055
|
-
if (alt) return "\x1B\r";
|
|
32056
|
-
return "\r";
|
|
32057
|
-
}
|
|
32058
|
-
if (key.code === 27) return "\x1B";
|
|
32059
|
-
if (key.code === 9) return shift ? "\x1B[Z" : " ";
|
|
32060
|
-
if (key.code === 127) return alt || ctrl ? "\x1B" : "";
|
|
32061
|
-
const keypad = KITTY_KEYPAD_CODES[key.code];
|
|
32062
|
-
if (keypad !== void 0) {
|
|
32063
|
-
if (alt) return "\x1B" + keypad;
|
|
32064
|
-
return keypad;
|
|
32065
|
-
}
|
|
32066
|
-
if (key.code >= 1 && key.code <= 6) {
|
|
32067
|
-
const mask = (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0);
|
|
32068
|
-
const mods = mask === 0 ? "" : `;${mask + 1}`;
|
|
32069
|
-
if (key.code === 1) return mods === "" ? "\x1B[H" : `\x1b[1${mods}H`;
|
|
32070
|
-
if (key.code === 4) return mods === "" ? "\x1B[F" : `\x1b[1${mods}F`;
|
|
32071
|
-
return `\x1b[${key.code}${mods}~`;
|
|
32072
|
-
}
|
|
32073
|
-
if (key.code >= 97 && key.code <= 122) {
|
|
32074
|
-
const letter = String.fromCodePoint(key.code);
|
|
32075
|
-
if (ctrl) return String.fromCodePoint(key.code - 96);
|
|
32076
|
-
if (alt) return "\x1B" + letter;
|
|
32077
|
-
if (shift) return String.fromCodePoint(key.alternate ?? key.code - 32);
|
|
32078
|
-
return letter;
|
|
32079
|
-
}
|
|
32080
|
-
if (key.code >= 65 && key.code <= 90) {
|
|
32081
|
-
if (ctrl) return String.fromCodePoint(key.code + 32 - 96);
|
|
32082
|
-
if (alt) return "\x1B" + String.fromCodePoint(key.code + 32);
|
|
32083
|
-
return String.fromCodePoint(key.code);
|
|
32084
|
-
}
|
|
32085
|
-
if (key.code >= 32 && key.code <= 126) {
|
|
32086
|
-
const base = key.alternate ?? key.code;
|
|
32087
|
-
if (ctrl && base - 96 >= 1 && base - 96 <= 26) return String.fromCodePoint(base - 96);
|
|
32088
|
-
if (alt) return "\x1B" + String.fromCodePoint(base);
|
|
32089
|
-
return String.fromCodePoint(base);
|
|
32090
|
-
}
|
|
31709
|
+
const free = width - fill;
|
|
31710
|
+
if (free > 0) spans.push({
|
|
31711
|
+
text: "░".repeat(free),
|
|
31712
|
+
tone: "label"
|
|
31713
|
+
});
|
|
31714
|
+
return spans;
|
|
32091
31715
|
}
|
|
32092
31716
|
/**
|
|
32093
|
-
*
|
|
32094
|
-
*
|
|
32095
|
-
*
|
|
32096
|
-
*
|
|
31717
|
+
* Compose the context group: the proportional bar plus the usage readout
|
|
31718
|
+
* OUTSIDE the bar, so the dotted track keeps its proportional meaning no
|
|
31719
|
+
* matter how wide the readout is. `full` reads `12.3K/1.0M 25%`; `percent`
|
|
31720
|
+
* drops the absolute pair; `none` is the bare bar. The readout turns amber
|
|
31721
|
+
* once occupancy reaches the warning threshold.
|
|
32097
31722
|
*/
|
|
32098
|
-
function
|
|
32099
|
-
|
|
32100
|
-
|
|
32101
|
-
|
|
32102
|
-
|
|
32103
|
-
|
|
32104
|
-
|
|
32105
|
-
|
|
32106
|
-
|
|
31723
|
+
function contextGroupSpans(usedTokens, contextWindow, barWidth, readout) {
|
|
31724
|
+
const spans = [{
|
|
31725
|
+
text: t("status.label.context") + " ",
|
|
31726
|
+
tone: "label"
|
|
31727
|
+
}];
|
|
31728
|
+
spans.push(...contextBar(usedTokens, contextWindow, barWidth));
|
|
31729
|
+
if (readout === "none" || barWidth <= 0 || contextWindow <= 0) return spans;
|
|
31730
|
+
const used = Math.max(0, usedTokens);
|
|
31731
|
+
const percent = Math.round(used / contextWindow * 100);
|
|
31732
|
+
const text = readout === "full" ? `${formatTokens(used)}/${formatTokens(contextWindow)} ${percent}%` : `${percent}%`;
|
|
31733
|
+
spans.push({
|
|
31734
|
+
text: " ",
|
|
31735
|
+
tone: "label"
|
|
31736
|
+
}, {
|
|
31737
|
+
text,
|
|
31738
|
+
tone: percent >= CONTEXT_WARN_PERCENT ? "warn" : "value"
|
|
32107
31739
|
});
|
|
31740
|
+
return spans;
|
|
32108
31741
|
}
|
|
32109
|
-
|
|
32110
|
-
|
|
32111
|
-
|
|
32112
|
-
|
|
32113
|
-
|
|
32114
|
-
|
|
32115
|
-
|
|
32116
|
-
|
|
32117
|
-
|
|
32118
|
-
|
|
32119
|
-
|
|
32120
|
-
|
|
32121
|
-
|
|
32122
|
-
|
|
32123
|
-
|
|
32124
|
-
|
|
32125
|
-
|
|
32126
|
-
|
|
32127
|
-
|
|
32128
|
-
|
|
32129
|
-
|
|
32130
|
-
|
|
32131
|
-
|
|
32132
|
-
|
|
32133
|
-
|
|
32134
|
-
|
|
32135
|
-
|
|
32136
|
-
|
|
32137
|
-
|
|
32138
|
-
|
|
32139
|
-
|
|
32140
|
-
|
|
32141
|
-
|
|
32142
|
-
|
|
32143
|
-
|
|
32144
|
-
|
|
32145
|
-
|
|
32146
|
-
|
|
32147
|
-
|
|
32148
|
-
|
|
31742
|
+
/** The full item catalog in canonical order (the /statusline default). */
|
|
31743
|
+
const STATUS_ITEMS = [
|
|
31744
|
+
{
|
|
31745
|
+
id: "model",
|
|
31746
|
+
label: "model",
|
|
31747
|
+
description: "provider/model serving this session",
|
|
31748
|
+
side: "left"
|
|
31749
|
+
},
|
|
31750
|
+
{
|
|
31751
|
+
id: "cwd",
|
|
31752
|
+
label: "cwd",
|
|
31753
|
+
description: "working-directory basename",
|
|
31754
|
+
side: "left"
|
|
31755
|
+
},
|
|
31756
|
+
{
|
|
31757
|
+
id: "mode",
|
|
31758
|
+
label: "mode",
|
|
31759
|
+
description: "agent preset composing the session",
|
|
31760
|
+
side: "left"
|
|
31761
|
+
},
|
|
31762
|
+
{
|
|
31763
|
+
id: "branch",
|
|
31764
|
+
label: "branch",
|
|
31765
|
+
description: "git branch inside a repository",
|
|
31766
|
+
side: "left"
|
|
31767
|
+
},
|
|
31768
|
+
{
|
|
31769
|
+
id: "context",
|
|
31770
|
+
label: "context",
|
|
31771
|
+
description: "context-window occupancy meter",
|
|
31772
|
+
side: "left"
|
|
31773
|
+
},
|
|
31774
|
+
{
|
|
31775
|
+
id: "permission",
|
|
31776
|
+
label: "permission",
|
|
31777
|
+
description: "permission preset badge with cycle hint",
|
|
31778
|
+
side: "right"
|
|
31779
|
+
},
|
|
31780
|
+
{
|
|
31781
|
+
id: "plan",
|
|
31782
|
+
label: "plan",
|
|
31783
|
+
description: "plan-mode state mark",
|
|
31784
|
+
side: "left"
|
|
31785
|
+
},
|
|
31786
|
+
{
|
|
31787
|
+
id: "turns",
|
|
31788
|
+
label: "turns",
|
|
31789
|
+
description: "turn and step counters",
|
|
31790
|
+
side: "left"
|
|
31791
|
+
},
|
|
31792
|
+
{
|
|
31793
|
+
id: "durations",
|
|
31794
|
+
label: "durations",
|
|
31795
|
+
description: "llm/ttft/decode/tool wall time",
|
|
31796
|
+
side: "left"
|
|
31797
|
+
},
|
|
31798
|
+
{
|
|
31799
|
+
id: "cache",
|
|
31800
|
+
label: "cache",
|
|
31801
|
+
description: "cache-hit share of billed input",
|
|
31802
|
+
side: "left"
|
|
31803
|
+
},
|
|
31804
|
+
{
|
|
31805
|
+
id: "tokens",
|
|
31806
|
+
label: "tokens",
|
|
31807
|
+
description: "cumulative input/output tokens",
|
|
31808
|
+
side: "left"
|
|
31809
|
+
},
|
|
31810
|
+
{
|
|
31811
|
+
id: "title",
|
|
31812
|
+
label: "title",
|
|
31813
|
+
description: "session title or short id",
|
|
31814
|
+
side: "left"
|
|
31815
|
+
},
|
|
31816
|
+
{
|
|
31817
|
+
id: "goal",
|
|
31818
|
+
label: "goal",
|
|
31819
|
+
description: "live goal phase and round progress",
|
|
31820
|
+
side: "left"
|
|
31821
|
+
},
|
|
31822
|
+
{
|
|
31823
|
+
id: "sandbox",
|
|
31824
|
+
label: "sandbox",
|
|
31825
|
+
description: "divergent sandbox-mode override",
|
|
31826
|
+
side: "left"
|
|
31827
|
+
}
|
|
31828
|
+
];
|
|
31829
|
+
const STATUSLINE_ITEM_KEYS = {
|
|
31830
|
+
model: {
|
|
31831
|
+
label: "statusline.item.model",
|
|
31832
|
+
description: "statusline.item.model.desc"
|
|
31833
|
+
},
|
|
31834
|
+
cwd: {
|
|
31835
|
+
label: "statusline.item.cwd",
|
|
31836
|
+
description: "statusline.item.cwd.desc"
|
|
31837
|
+
},
|
|
31838
|
+
mode: {
|
|
31839
|
+
label: "statusline.item.mode",
|
|
31840
|
+
description: "statusline.item.mode.desc"
|
|
31841
|
+
},
|
|
31842
|
+
branch: {
|
|
31843
|
+
label: "statusline.item.branch",
|
|
31844
|
+
description: "statusline.item.branch.desc"
|
|
31845
|
+
},
|
|
31846
|
+
context: {
|
|
31847
|
+
label: "statusline.item.context",
|
|
31848
|
+
description: "statusline.item.context.desc"
|
|
31849
|
+
},
|
|
31850
|
+
permission: {
|
|
31851
|
+
label: "statusline.item.permission",
|
|
31852
|
+
description: "statusline.item.permission.desc"
|
|
31853
|
+
},
|
|
31854
|
+
plan: {
|
|
31855
|
+
label: "statusline.item.plan",
|
|
31856
|
+
description: "statusline.item.plan.desc"
|
|
31857
|
+
},
|
|
31858
|
+
turns: {
|
|
31859
|
+
label: "statusline.item.turns",
|
|
31860
|
+
description: "statusline.item.turns.desc"
|
|
31861
|
+
},
|
|
31862
|
+
durations: {
|
|
31863
|
+
label: "statusline.item.durations",
|
|
31864
|
+
description: "statusline.item.durations.desc"
|
|
31865
|
+
},
|
|
31866
|
+
cache: {
|
|
31867
|
+
label: "statusline.item.cache",
|
|
31868
|
+
description: "statusline.item.cache.desc"
|
|
31869
|
+
},
|
|
31870
|
+
tokens: {
|
|
31871
|
+
label: "statusline.item.tokens",
|
|
31872
|
+
description: "statusline.item.tokens.desc"
|
|
31873
|
+
},
|
|
31874
|
+
title: {
|
|
31875
|
+
label: "statusline.item.title",
|
|
31876
|
+
description: "statusline.item.title.desc"
|
|
31877
|
+
},
|
|
31878
|
+
goal: {
|
|
31879
|
+
label: "statusline.item.goal",
|
|
31880
|
+
description: "statusline.item.goal.desc"
|
|
31881
|
+
},
|
|
31882
|
+
sandbox: {
|
|
31883
|
+
label: "statusline.item.sandbox",
|
|
31884
|
+
description: "statusline.item.sandbox.desc"
|
|
31885
|
+
}
|
|
31886
|
+
};
|
|
31887
|
+
/** Picker rows with labels/descriptions in the active interface language. */
|
|
31888
|
+
function localizedStatusItems() {
|
|
31889
|
+
return STATUS_ITEMS.map((item) => {
|
|
31890
|
+
const keys = STATUSLINE_ITEM_KEYS[item.id];
|
|
31891
|
+
return {
|
|
31892
|
+
...item,
|
|
31893
|
+
label: t(keys.label),
|
|
31894
|
+
description: t(keys.description)
|
|
31895
|
+
};
|
|
31896
|
+
});
|
|
31897
|
+
}
|
|
31898
|
+
/**
|
|
31899
|
+
* Default order: the whole catalog (matches the pre-customization bar).
|
|
31900
|
+
* The busy dot is not an item — it always leads the identity cluster.
|
|
31901
|
+
*/
|
|
31902
|
+
const DEFAULT_STATUSLINE_ITEMS = STATUS_ITEMS.map((item) => item.id);
|
|
31903
|
+
/**
|
|
31904
|
+
* Parse a persisted statusline item list. The stored value is the ordered
|
|
31905
|
+
* set of ENABLED items (the Codex /statusline contract): unknown ids and
|
|
31906
|
+
* duplicates drop out, and a non-array value (missing or corrupt file)
|
|
31907
|
+
* falls back to the full default set. An explicitly empty array is valid —
|
|
31908
|
+
* the bar degrades to its busy dot alone.
|
|
31909
|
+
* @param value - the raw parsed JSON value (expected string[]).
|
|
31910
|
+
* @returns the normalized ordered item list.
|
|
31911
|
+
*/
|
|
31912
|
+
function parseStatuslineItems(value) {
|
|
31913
|
+
if (!Array.isArray(value)) return [...DEFAULT_STATUSLINE_ITEMS];
|
|
31914
|
+
const known = new Set(STATUS_ITEMS.map((item) => item.id));
|
|
31915
|
+
const kept = [];
|
|
31916
|
+
for (const entry of value) if (typeof entry === "string" && known.has(entry) && !kept.includes(entry)) kept.push(entry);
|
|
31917
|
+
return kept;
|
|
31918
|
+
}
|
|
31919
|
+
/** Minimum blank gap kept between the leading and trailing sides. */
|
|
31920
|
+
const LEFT_RIGHT_GAP = 2;
|
|
31921
|
+
/** Column held back so Ink/yoga measurement drift can never force a wrap. */
|
|
31922
|
+
const WIDTH_SAFETY = 1;
|
|
31923
|
+
/** Column budget for the session title before it ellipsizes. */
|
|
31924
|
+
const TITLE_BUDGET = 48;
|
|
31925
|
+
/**
|
|
31926
|
+
* Primary-row drop ranks: identity never drops; permission outranks context.
|
|
31927
|
+
* The drop ladder peels hint and trailing identity facts before removing
|
|
31928
|
+
* the context meter, so occupancy stays visible on a typical 120-column
|
|
31929
|
+
* terminal. Secondary-row groups reuse the remaining ranks independently.
|
|
31930
|
+
*/
|
|
31931
|
+
const RANK_TITLE = 10;
|
|
31932
|
+
const RANK_TOKENS = 50;
|
|
31933
|
+
const RANK_COUNTS = 90;
|
|
31934
|
+
const RANK_CONTEXT = 90;
|
|
31935
|
+
const RANK_SANDBOX = 92;
|
|
31936
|
+
const RANK_GOAL = 95;
|
|
31937
|
+
const RANK_BADGE = 100;
|
|
31938
|
+
const RANK_IDENTITY = Number.POSITIVE_INFINITY;
|
|
31939
|
+
/** Row 2 drop ranks: title and durations go first; state and counts survive longest. */
|
|
31940
|
+
const RANK2_DURATIONS = 40;
|
|
31941
|
+
const RANK2_CACHE = 50;
|
|
31942
|
+
const RANK2_PLAN = 70;
|
|
31943
|
+
/**
|
|
31944
|
+
* Traffic-light tone for a permission preset: read-only stays success green,
|
|
31945
|
+
* full access reads error red, and every workspace-scoped middle ground
|
|
31946
|
+
* (including unknown presets) reads warning amber.
|
|
31947
|
+
* @param permission - active permission preset label.
|
|
31948
|
+
* @returns tone for the badge span.
|
|
31949
|
+
*/
|
|
31950
|
+
function permissionTone(permission) {
|
|
31951
|
+
const label = permission.toLowerCase();
|
|
31952
|
+
if (label.includes("read")) return "success";
|
|
31953
|
+
if (label.includes("danger") || label.includes("full")) return "error";
|
|
31954
|
+
return "warn";
|
|
31955
|
+
}
|
|
31956
|
+
/** Display-safe external text: one row, controls escaped. */
|
|
31957
|
+
function safe(text) {
|
|
31958
|
+
return singleLineText(text);
|
|
31959
|
+
}
|
|
31960
|
+
/** Dim junction separator span inside a cluster. */
|
|
31961
|
+
function sep() {
|
|
31962
|
+
return {
|
|
31963
|
+
text: " · ",
|
|
31964
|
+
tone: "label"
|
|
31965
|
+
};
|
|
31966
|
+
}
|
|
31967
|
+
/** True for the dim ` · ` that joins identity facts. */
|
|
31968
|
+
function isIdentitySep(span) {
|
|
31969
|
+
return span.text === " · " && span.tone === "label";
|
|
31970
|
+
}
|
|
31971
|
+
/**
|
|
31972
|
+
* Drop the trailing identity fact (branch, then mode, then cwd) so the
|
|
31973
|
+
* context meter can keep a column. Returns undefined once only the busy
|
|
31974
|
+
* dot and model remain.
|
|
31975
|
+
*/
|
|
31976
|
+
function peelIdentityFact(spans) {
|
|
31977
|
+
if (spans.length < 3) return void 0;
|
|
31978
|
+
const last = spans[spans.length - 1];
|
|
31979
|
+
const before = spans[spans.length - 2];
|
|
31980
|
+
const modeSep = spans[spans.length - 3];
|
|
31981
|
+
if (last?.tone === "accent" && modeSep !== void 0 && isIdentitySep(modeSep) && before?.tone === "label") return spans.slice(0, -3);
|
|
31982
|
+
if ((last?.tone === "branch" || last?.tone === "path") && before !== void 0 && isIdentitySep(before)) return spans.slice(0, -2);
|
|
31983
|
+
}
|
|
31984
|
+
/** Total visible columns of a span list (separators ride inside the spans). */
|
|
31985
|
+
function spansWidth(spans) {
|
|
31986
|
+
let width = 0;
|
|
31987
|
+
for (const span of spans) width += visibleColumns(span.text);
|
|
31988
|
+
return width;
|
|
31989
|
+
}
|
|
31990
|
+
/** Join widths of parts with one fixed separator between neighbors. */
|
|
31991
|
+
function joinWidth(parts, separator) {
|
|
31992
|
+
if (parts.length === 0) return 0;
|
|
31993
|
+
let width = 0;
|
|
31994
|
+
for (const part of parts) width += part;
|
|
31995
|
+
return width + separator * (parts.length - 1);
|
|
31996
|
+
}
|
|
31997
|
+
/** Build every candidate group/span with its drop rank and item id. */
|
|
31998
|
+
function buildCandidates(facts, stats, busy, enabled, contextWidth) {
|
|
31999
|
+
const identity = [{
|
|
32000
|
+
text: busy ? "● " : "○ ",
|
|
32001
|
+
tone: busy ? "live" : "meta"
|
|
32002
|
+
}];
|
|
32003
|
+
const push = (span) => {
|
|
32004
|
+
if (identity.length > 1) identity.push(sep());
|
|
32005
|
+
identity.push(span);
|
|
32006
|
+
};
|
|
32007
|
+
const model = safe(facts.model);
|
|
32008
|
+
if (model !== "" && enabled.has("model")) {
|
|
32009
|
+
const effort = safe(stats.reasoningEffort);
|
|
32010
|
+
push({
|
|
32011
|
+
text: effort === "" ? model : `${model}@${effort}`,
|
|
32012
|
+
tone: "model"
|
|
32013
|
+
});
|
|
32014
|
+
}
|
|
32015
|
+
const cwd = safe(facts.cwd);
|
|
32016
|
+
if (cwd !== "" && enabled.has("cwd")) push({
|
|
32017
|
+
text: cwd,
|
|
32018
|
+
tone: "path"
|
|
32019
|
+
});
|
|
32020
|
+
const mode = safe(facts.mode ?? "");
|
|
32021
|
+
if (mode !== "" && enabled.has("mode")) {
|
|
32022
|
+
push({
|
|
32023
|
+
text: t("status.label.mode") + " ",
|
|
32024
|
+
tone: "label"
|
|
32025
|
+
});
|
|
32026
|
+
identity.push({
|
|
32027
|
+
text: mode,
|
|
32028
|
+
tone: "accent"
|
|
32029
|
+
});
|
|
32030
|
+
}
|
|
32031
|
+
const branch = safe(facts.branch);
|
|
32032
|
+
if (branch !== "" && enabled.has("branch")) push({
|
|
32033
|
+
text: "⑂ " + branch,
|
|
32034
|
+
tone: "branch"
|
|
32035
|
+
});
|
|
32036
|
+
const left = [{
|
|
32037
|
+
group: { spans: identity },
|
|
32038
|
+
rank: RANK_IDENTITY,
|
|
32039
|
+
id: "identity"
|
|
32040
|
+
}];
|
|
32041
|
+
const right = [];
|
|
32042
|
+
const row2 = [];
|
|
32043
|
+
if (stats.turns > 0 || stats.steps > 0) {
|
|
32044
|
+
if (enabled.has("turns")) {
|
|
32045
|
+
const counts = [];
|
|
32046
|
+
const pair = (label, value) => {
|
|
32047
|
+
if (counts.length > 0) counts.push(sep());
|
|
32048
|
+
counts.push({
|
|
32049
|
+
text: label + " ",
|
|
32050
|
+
tone: "label"
|
|
32051
|
+
}, {
|
|
32052
|
+
text: value,
|
|
32053
|
+
tone: "value"
|
|
32054
|
+
});
|
|
32055
|
+
};
|
|
32056
|
+
pair(t("status.label.turns"), String(stats.turns));
|
|
32057
|
+
pair(t("status.label.steps"), String(stats.steps));
|
|
32058
|
+
row2.push({
|
|
32059
|
+
group: { spans: counts },
|
|
32060
|
+
rank: RANK_COUNTS,
|
|
32061
|
+
id: "turns"
|
|
32062
|
+
});
|
|
32063
|
+
}
|
|
32064
|
+
if (enabled.has("durations")) {
|
|
32065
|
+
const durations = [];
|
|
32066
|
+
const pair = (label, value) => {
|
|
32067
|
+
if (durations.length > 0) durations.push(sep());
|
|
32068
|
+
durations.push({
|
|
32069
|
+
text: label + " ",
|
|
32070
|
+
tone: "label"
|
|
32071
|
+
}, {
|
|
32072
|
+
text: value,
|
|
32073
|
+
tone: "value"
|
|
32074
|
+
});
|
|
32075
|
+
};
|
|
32076
|
+
if (stats.llmMs > 0) pair(t("status.label.modelTime"), formatDuration(stats.llmMs));
|
|
32077
|
+
if (stats.ttftSteps > 0) pair(t("status.label.latency"), formatDuration(stats.ttftMs / stats.ttftSteps));
|
|
32078
|
+
if (stats.decodeMs > 0 && stats.decodeTokens > 0) {
|
|
32079
|
+
if (durations.length > 0) durations.push(sep());
|
|
32080
|
+
durations.push({
|
|
32081
|
+
text: formatRate(stats.decodeTokens / (stats.decodeMs / 1e3)),
|
|
32082
|
+
tone: "value"
|
|
32083
|
+
}, {
|
|
32084
|
+
text: t("status.label.tokensPerSec"),
|
|
32085
|
+
tone: "label"
|
|
32086
|
+
});
|
|
32087
|
+
}
|
|
32088
|
+
if (stats.toolMs > 0) pair(t("status.label.tool"), formatDuration(stats.toolMs));
|
|
32089
|
+
if (durations.length > 0) row2.push({
|
|
32090
|
+
group: { spans: durations },
|
|
32091
|
+
rank: RANK2_DURATIONS,
|
|
32092
|
+
id: "durations"
|
|
32093
|
+
});
|
|
32094
|
+
}
|
|
32095
|
+
}
|
|
32096
|
+
const cacheHit = cacheHitPercent(stats.usage);
|
|
32097
|
+
if (cacheHit !== null && enabled.has("cache")) {
|
|
32098
|
+
const spans = [{
|
|
32099
|
+
text: t("status.label.cache") + " ",
|
|
32100
|
+
tone: "label"
|
|
32101
|
+
}];
|
|
32102
|
+
if (stats.usage.cacheReadTokens > 0) spans.push({
|
|
32103
|
+
text: formatTokens(stats.usage.cacheReadTokens),
|
|
32104
|
+
tone: "value"
|
|
32105
|
+
}, sep());
|
|
32106
|
+
spans.push({
|
|
32107
|
+
text: cacheHit + "%",
|
|
32108
|
+
tone: "value"
|
|
32109
|
+
});
|
|
32110
|
+
row2.push({
|
|
32111
|
+
group: { spans },
|
|
32112
|
+
rank: RANK2_CACHE,
|
|
32113
|
+
id: "cache"
|
|
32114
|
+
});
|
|
32115
|
+
}
|
|
32116
|
+
if (stats.contextWindow > 0 && stats.lastPromptTokens > 0 && enabled.has("context")) left.push({
|
|
32117
|
+
group: { spans: contextGroupSpans(stats.lastPromptTokens, stats.contextWindow, contextWidth, "full") },
|
|
32118
|
+
rank: RANK_CONTEXT,
|
|
32119
|
+
id: "context"
|
|
32120
|
+
});
|
|
32121
|
+
if ((stats.usage.uncachedInputTokens > 0 || stats.usage.outputTokens > 0) && enabled.has("tokens")) {
|
|
32122
|
+
const tokens = [];
|
|
32123
|
+
const pair = (label, value) => {
|
|
32124
|
+
if (tokens.length > 0) tokens.push(sep());
|
|
32125
|
+
tokens.push({
|
|
32126
|
+
text: label + " ",
|
|
32127
|
+
tone: "label"
|
|
32128
|
+
}, {
|
|
32129
|
+
text: value,
|
|
32130
|
+
tone: "value"
|
|
32131
|
+
});
|
|
32132
|
+
};
|
|
32133
|
+
pair(t("status.label.in"), formatTokens(stats.usage.uncachedInputTokens));
|
|
32134
|
+
pair(t("status.label.out"), formatTokens(stats.usage.outputTokens));
|
|
32135
|
+
row2.push({
|
|
32136
|
+
group: { spans: tokens },
|
|
32137
|
+
rank: RANK_TOKENS,
|
|
32138
|
+
id: "tokens"
|
|
32139
|
+
});
|
|
32140
|
+
}
|
|
32141
|
+
const label = truncateColumns(safe(facts.title !== void 0 && facts.title !== "" ? facts.title : facts.sessionId), TITLE_BUDGET);
|
|
32142
|
+
if (label !== "" && enabled.has("title")) row2.push({
|
|
32143
|
+
group: { spans: [{
|
|
32144
|
+
text: label,
|
|
32145
|
+
tone: "meta"
|
|
32146
|
+
}] },
|
|
32147
|
+
rank: RANK_TITLE,
|
|
32148
|
+
id: "title"
|
|
32149
|
+
});
|
|
32150
|
+
if (facts.goal !== void 0 && enabled.has("goal")) row2.push({
|
|
32151
|
+
group: { spans: [{
|
|
32152
|
+
text: facts.goal.phase === "active" ? t("status.goal.round", {
|
|
32153
|
+
current: facts.goal.rounds,
|
|
32154
|
+
max: facts.goal.max
|
|
32155
|
+
}) : t("status.goal.phase", { phase: safe(facts.goal.phase) }),
|
|
32156
|
+
tone: "accent"
|
|
32157
|
+
}] },
|
|
32158
|
+
rank: RANK_GOAL,
|
|
32159
|
+
id: "goal"
|
|
32160
|
+
});
|
|
32161
|
+
const sandbox = safe(facts.sandbox ?? "");
|
|
32162
|
+
if (sandbox !== "" && sandbox.toLowerCase() !== facts.permission.toLowerCase() && enabled.has("sandbox")) row2.push({
|
|
32163
|
+
group: { spans: [{
|
|
32164
|
+
text: t("status.label.sandbox") + " " + sandbox,
|
|
32165
|
+
tone: "warn"
|
|
32166
|
+
}] },
|
|
32167
|
+
rank: RANK_SANDBOX,
|
|
32168
|
+
id: "sandbox"
|
|
32169
|
+
});
|
|
32170
|
+
const permission = safe(facts.permission);
|
|
32171
|
+
let badge = -1;
|
|
32172
|
+
const planStation = facts.plan && permissionTone(permission) === "success";
|
|
32173
|
+
if (permission !== "" && enabled.has("permission")) {
|
|
32174
|
+
right.push({
|
|
32175
|
+
span: planStation ? {
|
|
32176
|
+
text: t("status.plan.on"),
|
|
32177
|
+
tone: "plan"
|
|
32178
|
+
} : {
|
|
32179
|
+
text: permission,
|
|
32180
|
+
tone: permissionTone(permission)
|
|
32181
|
+
},
|
|
32182
|
+
rank: RANK_BADGE,
|
|
32183
|
+
id: "permission"
|
|
32184
|
+
});
|
|
32185
|
+
badge = right.length - 1;
|
|
32149
32186
|
}
|
|
32187
|
+
if (facts.plan && enabled.has("plan")) row2.push({
|
|
32188
|
+
group: { spans: [{
|
|
32189
|
+
text: t("status.plan.mark"),
|
|
32190
|
+
tone: "accent"
|
|
32191
|
+
}] },
|
|
32192
|
+
rank: RANK2_PLAN,
|
|
32193
|
+
id: "plan"
|
|
32194
|
+
});
|
|
32195
|
+
return {
|
|
32196
|
+
left,
|
|
32197
|
+
right,
|
|
32198
|
+
badge,
|
|
32199
|
+
row2
|
|
32200
|
+
};
|
|
32150
32201
|
}
|
|
32151
32202
|
/**
|
|
32152
|
-
*
|
|
32153
|
-
*
|
|
32154
|
-
*
|
|
32155
|
-
*
|
|
32156
|
-
*
|
|
32203
|
+
* Compose the two-row footer layout under a column budget. Row 1 keeps model,
|
|
32204
|
+
* cwd, mode, branch, context, then the right-pinned permission badge and cycle
|
|
32205
|
+
* hint. It shrinks the context bar, drops the hint, and peels trailing
|
|
32206
|
+
* identity facts before removing the context group or the permission badge.
|
|
32207
|
+
* Row 2 fits all secondary figures and state within its own budget.
|
|
32208
|
+
* @param facts - identity facts resolved by the runner.
|
|
32209
|
+
* @param stats - session figures folded from the durable log.
|
|
32210
|
+
* @param columns - usable columns for each row (before their left padding).
|
|
32211
|
+
* @param options - 'busy' hides the cycle hint while a turn runs (Codex
|
|
32212
|
+
* keeps mode hints idle-only); 'items' is the ordered enabled-item config
|
|
32213
|
+
* from /statusline (defaults to the full catalog). Display order follows the
|
|
32214
|
+
* config per side while the drop ladder keeps its fixed ranks.
|
|
32215
|
+
* @returns the two rows to render; row1.left is never empty.
|
|
32157
32216
|
*/
|
|
32158
|
-
function
|
|
32159
|
-
const
|
|
32160
|
-
|
|
32161
|
-
|
|
32162
|
-
const
|
|
32163
|
-
|
|
32164
|
-
|
|
32165
|
-
|
|
32166
|
-
|
|
32167
|
-
|
|
32168
|
-
|
|
32217
|
+
function layoutStatusBar(facts, stats, columns, options = {}) {
|
|
32218
|
+
const busy = options.busy === true;
|
|
32219
|
+
const items = options.items ?? DEFAULT_STATUSLINE_ITEMS;
|
|
32220
|
+
const enabled = new Set(items);
|
|
32221
|
+
const budget = Math.max(1, Math.floor(columns) - WIDTH_SAFETY);
|
|
32222
|
+
const maxContextWidth = Math.max(CONTEXT_MIN_WIDTH, Math.min(budget, Math.floor(options.contextWidth ?? 24)));
|
|
32223
|
+
const { left, right, badge, row2 } = buildCandidates(facts, stats, busy, enabled, maxContextWidth);
|
|
32224
|
+
const groupSeparator = visibleColumns(" | ");
|
|
32225
|
+
const itemSeparator = visibleColumns(STATUS_ITEM_SEPARATOR);
|
|
32226
|
+
const position = new Map(items.map((id, index) => [id, index]));
|
|
32227
|
+
const byPosition = (a, b) => (position.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (position.get(b.id) ?? Number.MAX_SAFE_INTEGER);
|
|
32228
|
+
const orderedLeft = [left[0], ...left.slice(1).sort(byPosition)];
|
|
32229
|
+
const orderedRight = right.slice().sort(byPosition);
|
|
32230
|
+
const orderedRow2 = row2.slice().sort(byPosition);
|
|
32231
|
+
let hint = badge >= 0 && !busy;
|
|
32232
|
+
const leftKept = [...orderedLeft];
|
|
32233
|
+
const rightKept = [...orderedRight];
|
|
32234
|
+
let contextReadout = "full";
|
|
32235
|
+
let contextWidth = maxContextWidth;
|
|
32236
|
+
const rebuildContext = () => {
|
|
32237
|
+
const index = leftKept.findIndex((entry) => entry.id === "context");
|
|
32238
|
+
if (index < 0) return;
|
|
32239
|
+
leftKept[index] = {
|
|
32240
|
+
group: { spans: contextGroupSpans(stats.lastPromptTokens, stats.contextWindow, contextWidth, contextReadout) },
|
|
32241
|
+
rank: RANK_CONTEXT,
|
|
32242
|
+
id: "context"
|
|
32243
|
+
};
|
|
32169
32244
|
};
|
|
32170
|
-
|
|
32171
|
-
|
|
32172
|
-
|
|
32173
|
-
|
|
32174
|
-
|
|
32175
|
-
|
|
32176
|
-
|
|
32245
|
+
const width = () => {
|
|
32246
|
+
const leftWidth = joinWidth(leftKept.map((entry) => spansWidth(entry.group.spans)), groupSeparator);
|
|
32247
|
+
const rightWidth = joinWidth(rightKept.map((entry) => visibleColumns(entry.span.text)), itemSeparator) + (hint ? visibleColumns(statusCycleHint()) : 0);
|
|
32248
|
+
return rightWidth > 0 ? leftWidth + LEFT_RIGHT_GAP + rightWidth : leftWidth;
|
|
32249
|
+
};
|
|
32250
|
+
while (width() > budget) {
|
|
32251
|
+
if (leftKept.some((entry) => entry.id === "context")) {
|
|
32252
|
+
if (contextWidth > CONTEXT_MIN_WIDTH) {
|
|
32253
|
+
const overflow = width() - budget;
|
|
32254
|
+
contextWidth = Math.max(CONTEXT_MIN_WIDTH, contextWidth - overflow);
|
|
32255
|
+
rebuildContext();
|
|
32256
|
+
continue;
|
|
32257
|
+
}
|
|
32258
|
+
if (contextReadout === "full") {
|
|
32259
|
+
contextReadout = "percent";
|
|
32260
|
+
rebuildContext();
|
|
32261
|
+
continue;
|
|
32262
|
+
}
|
|
32177
32263
|
}
|
|
32178
|
-
|
|
32179
|
-
|
|
32180
|
-
flushText();
|
|
32181
|
-
tokens.push(functional.token);
|
|
32182
|
-
special = true;
|
|
32183
|
-
offset += functional.length;
|
|
32264
|
+
if (hint) {
|
|
32265
|
+
hint = false;
|
|
32184
32266
|
continue;
|
|
32185
32267
|
}
|
|
32186
|
-
const
|
|
32187
|
-
if (
|
|
32188
|
-
|
|
32189
|
-
|
|
32190
|
-
|
|
32191
|
-
|
|
32268
|
+
const identityEntry = leftKept[0];
|
|
32269
|
+
if (identityEntry?.id === "identity") {
|
|
32270
|
+
const peeled = peelIdentityFact(identityEntry.group.spans);
|
|
32271
|
+
if (peeled !== void 0) {
|
|
32272
|
+
leftKept[0] = {
|
|
32273
|
+
...identityEntry,
|
|
32274
|
+
group: { spans: peeled }
|
|
32275
|
+
};
|
|
32276
|
+
continue;
|
|
32277
|
+
}
|
|
32278
|
+
}
|
|
32279
|
+
if (leftKept.some((entry) => entry.id === "context")) {
|
|
32280
|
+
leftKept.splice(leftKept.findIndex((entry) => entry.id === "context"), 1);
|
|
32192
32281
|
continue;
|
|
32193
32282
|
}
|
|
32194
|
-
|
|
32195
|
-
|
|
32196
|
-
|
|
32283
|
+
let dropLeft = -1;
|
|
32284
|
+
let dropRight = -1;
|
|
32285
|
+
let dropRank = Number.POSITIVE_INFINITY;
|
|
32286
|
+
for (let index = 0; index < leftKept.length; index += 1) {
|
|
32287
|
+
const rank = leftKept[index].rank;
|
|
32288
|
+
if (rank < dropRank) {
|
|
32289
|
+
dropRank = rank;
|
|
32290
|
+
dropLeft = index;
|
|
32291
|
+
dropRight = -1;
|
|
32292
|
+
}
|
|
32293
|
+
}
|
|
32294
|
+
for (let index = 0; index < rightKept.length; index += 1) {
|
|
32295
|
+
const rank = rightKept[index].rank;
|
|
32296
|
+
if (rank < dropRank) {
|
|
32297
|
+
dropRank = rank;
|
|
32298
|
+
dropRight = index;
|
|
32299
|
+
dropLeft = -1;
|
|
32300
|
+
}
|
|
32301
|
+
}
|
|
32302
|
+
if (dropLeft < 0 && dropRight < 0) break;
|
|
32303
|
+
if (dropLeft >= 0) leftKept.splice(dropLeft, 1);
|
|
32304
|
+
else {
|
|
32305
|
+
rightKept.splice(dropRight, 1);
|
|
32306
|
+
if (dropRight === rightKept.length) hint = false;
|
|
32307
|
+
}
|
|
32197
32308
|
}
|
|
32198
|
-
|
|
32199
|
-
|
|
32309
|
+
if (width() > budget) {
|
|
32310
|
+
rightKept.length = 0;
|
|
32311
|
+
hint = false;
|
|
32312
|
+
while (leftKept.length > 1) leftKept.pop();
|
|
32313
|
+
leftKept[0] = {
|
|
32314
|
+
group: { spans: [{
|
|
32315
|
+
text: truncateColumns(leftKept[0].group.spans.map((span) => span.text).join(""), budget),
|
|
32316
|
+
tone: "model"
|
|
32317
|
+
}] },
|
|
32318
|
+
rank: RANK_IDENTITY,
|
|
32319
|
+
id: "identity"
|
|
32320
|
+
};
|
|
32321
|
+
}
|
|
32322
|
+
const row2Kept = [...orderedRow2];
|
|
32323
|
+
const row2Budget = Math.max(1, budget - 2);
|
|
32324
|
+
const row2Width = () => joinWidth(row2Kept.map((entry) => spansWidth(entry.group.spans)), groupSeparator);
|
|
32325
|
+
while (row2Width() > row2Budget && row2Kept.length > 0) {
|
|
32326
|
+
let dropIndex = 0;
|
|
32327
|
+
let dropRank = Number.POSITIVE_INFINITY;
|
|
32328
|
+
for (let index = 0; index < row2Kept.length; index += 1) if (row2Kept[index].rank < dropRank) {
|
|
32329
|
+
dropRank = row2Kept[index].rank;
|
|
32330
|
+
dropIndex = index;
|
|
32331
|
+
}
|
|
32332
|
+
row2Kept.splice(dropIndex, 1);
|
|
32333
|
+
}
|
|
32334
|
+
return {
|
|
32335
|
+
row1: {
|
|
32336
|
+
left: leftKept.map((entry) => entry.group),
|
|
32337
|
+
right: rightKept.map((entry) => entry.span),
|
|
32338
|
+
hint
|
|
32339
|
+
},
|
|
32340
|
+
row2: {
|
|
32341
|
+
left: row2Kept.map((entry) => entry.group),
|
|
32342
|
+
right: [],
|
|
32343
|
+
hint: false
|
|
32344
|
+
}
|
|
32345
|
+
};
|
|
32200
32346
|
}
|
|
32201
32347
|
//#endregion
|
|
32202
32348
|
//#region src/ui/query-editor.ts
|
|
@@ -35301,9 +35447,9 @@ function deepseekWaveHues(tier) {
|
|
|
35301
35447
|
palette.brandMid
|
|
35302
35448
|
];
|
|
35303
35449
|
}
|
|
35304
|
-
/**
|
|
35450
|
+
/** Functional busy chase; decorative color flow still follows the preference. */
|
|
35305
35451
|
function BusyChase({ animated = true }) {
|
|
35306
|
-
const tick = useFrames(125,
|
|
35452
|
+
const tick = useFrames(125, true);
|
|
35307
35453
|
const flow = themeFlow();
|
|
35308
35454
|
const marker = flow !== void 0 && animated ? flowColor(tick * 125 + flow.phaseMs, flow.anchors) : getPalette().brandBright;
|
|
35309
35455
|
return (0, import_react.createElement)(Text, { color: inkColor(marker) }, busyChaseFrame(tick) + " ");
|
|
@@ -35636,7 +35782,7 @@ function Composer({ active, frozen, frozenHint, busy, descriptors, skills, dispa
|
|
|
35636
35782
|
const [preparingImages, setPreparingImages] = (0, import_react.useState)(false);
|
|
35637
35783
|
const prepareAbortRef = (0, import_react.useRef)(void 0);
|
|
35638
35784
|
const prepareEpochRef = (0, import_react.useRef)(0);
|
|
35639
|
-
const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages
|
|
35785
|
+
const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages);
|
|
35640
35786
|
(0, import_react.useEffect)(() => () => {
|
|
35641
35787
|
prepareEpochRef.current += 1;
|
|
35642
35788
|
prepareAbortRef.current?.abort();
|
|
@@ -38315,6 +38461,37 @@ function QuestionBar({ store, snapshot, locked }) {
|
|
|
38315
38461
|
}
|
|
38316
38462
|
/** The /model panel: a scrolling list over the advisory model directory. */
|
|
38317
38463
|
//#endregion
|
|
38464
|
+
//#region src/render/transcript-viewport.ts
|
|
38465
|
+
/**
|
|
38466
|
+
* Advance the monotonic physical-row flush cursor.
|
|
38467
|
+
*
|
|
38468
|
+
* Ordinary renders never pull a row back out of native scrollback. A source-
|
|
38469
|
+
* backed replay (epoch change) flushes the complete durable window exactly
|
|
38470
|
+
* once after the caller clears/remounts Static; subsequent durable rows start
|
|
38471
|
+
* a fresh retained tail. Session/reset shrink starts from the new source.
|
|
38472
|
+
*/
|
|
38473
|
+
function advanceTranscriptViewport(previous, input) {
|
|
38474
|
+
const total = Math.max(0, Math.floor(input.totalRows));
|
|
38475
|
+
const retained = Math.max(0, Math.floor(input.retainedRows));
|
|
38476
|
+
const desiredStatic = Math.max(0, total - retained);
|
|
38477
|
+
const staticRows = previous.sessionKey !== input.sessionKey ? desiredStatic : previous.epoch !== input.epoch ? total : total < previous.flushedRows ? desiredStatic : Math.max(previous.flushedRows, desiredStatic);
|
|
38478
|
+
return {
|
|
38479
|
+
cursor: {
|
|
38480
|
+
sessionKey: input.sessionKey,
|
|
38481
|
+
epoch: input.epoch,
|
|
38482
|
+
flushedRows: staticRows
|
|
38483
|
+
},
|
|
38484
|
+
staticRows,
|
|
38485
|
+
liveRows: total - staticRows
|
|
38486
|
+
};
|
|
38487
|
+
}
|
|
38488
|
+
/** Real history rows that fit beside the currently painted stream/tool rows. */
|
|
38489
|
+
function visibleTranscriptRows(historyRows, occupiedRows, capacity) {
|
|
38490
|
+
const history = Math.max(0, Math.floor(historyRows));
|
|
38491
|
+
const available = Math.max(0, Math.floor(capacity) - Math.max(0, Math.floor(occupiedRows)));
|
|
38492
|
+
return Math.min(history, available);
|
|
38493
|
+
}
|
|
38494
|
+
//#endregion
|
|
38318
38495
|
//#region src/app.ts
|
|
38319
38496
|
/**
|
|
38320
38497
|
* The Ink terminal app: whale-and-wordmark header in DeepSeek blue, the live
|
|
@@ -38363,26 +38540,23 @@ const TRIM_REFLOW_CLEAR = "\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[H";
|
|
|
38363
38540
|
const SYNCHRONIZED_UPDATE_BEGIN = "\x1B[?2026h";
|
|
38364
38541
|
/** Release the held frame after Ink has replayed the source-backed Static rows. */
|
|
38365
38542
|
const SYNCHRONIZED_UPDATE_END = "\x1B[?2026l";
|
|
38366
|
-
/**
|
|
38367
|
-
|
|
38368
|
-
|
|
38369
|
-
*/
|
|
38370
|
-
function Caret({ animated = true }) {
|
|
38371
|
-
const tick = useFrames(530, animated);
|
|
38543
|
+
/** Functional streaming caret: activity remains visible in reduced motion. */
|
|
38544
|
+
function Caret() {
|
|
38545
|
+
const tick = useFrames(530, true);
|
|
38372
38546
|
return (0, import_react.createElement)(Text, null, caretVisible(tick) ? "▍" : " ");
|
|
38373
38547
|
}
|
|
38374
38548
|
/** One resettable input-caret phase shared by the entire composer. */
|
|
38375
|
-
function ShimmerLine({ text,
|
|
38376
|
-
const tick = useFrames(33,
|
|
38549
|
+
function ShimmerLine({ text, themeFlowAnimated = true }) {
|
|
38550
|
+
const tick = useFrames(33, true);
|
|
38377
38551
|
const palette = getPalette();
|
|
38378
38552
|
const flow = themeFlow();
|
|
38379
|
-
const highlight = flow !== void 0 &&
|
|
38553
|
+
const highlight = flow !== void 0 && themeFlowAnimated ? flowColor(tick * 33 + flow.phaseMs, flow.anchors) : palette.brandBright;
|
|
38380
38554
|
const graphemes = splitGraphemes(text);
|
|
38381
38555
|
return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...graphemes.map((grapheme, index) => {
|
|
38382
38556
|
const sparkle = grapheme.text === "✻";
|
|
38383
38557
|
return (0, import_react.createElement)(Text, {
|
|
38384
38558
|
key: `${grapheme.start}-${grapheme.end}`,
|
|
38385
|
-
color: inkColor(
|
|
38559
|
+
color: inkColor(sparkle ? deepDivingSparkColor(tick, palette.brandDeep, highlight) : deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, highlight)),
|
|
38386
38560
|
bold: sparkle || void 0
|
|
38387
38561
|
}, grapheme.text);
|
|
38388
38562
|
}));
|
|
@@ -38393,12 +38567,13 @@ function ShimmerLine({ text, animated = true }) {
|
|
|
38393
38567
|
* only once the turn has clearly been running (15s) — anchored to `turn/start`
|
|
38394
38568
|
* so a resumed mid-turn keeps the real time.
|
|
38395
38569
|
*/
|
|
38396
|
-
function DeepDivingLine({ since,
|
|
38570
|
+
function DeepDivingLine({ since, themeFlowAnimated = true }) {
|
|
38571
|
+
useFrames(1e3, true);
|
|
38397
38572
|
const elapsed = since === 0 ? 0 : Date.now() - since;
|
|
38398
38573
|
const text = elapsed >= 15e3 ? `✻ Deep diving... ${runClock(elapsed)}` : "✻ Deep diving...";
|
|
38399
38574
|
return (0, import_react.createElement)(ShimmerLine, {
|
|
38400
38575
|
text,
|
|
38401
|
-
|
|
38576
|
+
themeFlowAnimated
|
|
38402
38577
|
});
|
|
38403
38578
|
}
|
|
38404
38579
|
/**
|
|
@@ -38419,11 +38594,25 @@ function streamTailBodyColumns(rowColumns, prefix, continuationPrefix = prefix)
|
|
|
38419
38594
|
const prefixColumns = Math.max(visibleColumns(prefix), visibleColumns(continuationPrefix));
|
|
38420
38595
|
return Math.max(1, width - prefixColumns);
|
|
38421
38596
|
}
|
|
38422
|
-
|
|
38597
|
+
/** Shared physical layout for streaming render and viewport row accounting. */
|
|
38598
|
+
function streamTailLayout(text, columns, maxRows, prefix, continuationPrefix) {
|
|
38423
38599
|
const safeRows = Math.max(1, maxRows);
|
|
38424
38600
|
const contentColumns = streamTailBodyColumns(columns, prefix, continuationPrefix);
|
|
38425
38601
|
const initial = displayTail(text, contentColumns, safeRows);
|
|
38426
38602
|
const tail = initial.truncated && safeRows > 1 ? displayTail(text, contentColumns, safeRows - 1) : initial;
|
|
38603
|
+
return {
|
|
38604
|
+
text: tail.text,
|
|
38605
|
+
truncated: tail.truncated,
|
|
38606
|
+
rows: tail.text.split("\n").length + (tail.truncated && safeRows > 1 ? 1 : 0)
|
|
38607
|
+
};
|
|
38608
|
+
}
|
|
38609
|
+
function streamTailPhysicalRows(text, columns, maxRows, prefix = "", continuationPrefix = prefix) {
|
|
38610
|
+
if (text === "" || maxRows <= 0) return 0;
|
|
38611
|
+
return streamTailLayout(text, columns, maxRows, prefix, continuationPrefix).rows;
|
|
38612
|
+
}
|
|
38613
|
+
function StreamTail({ text, dim, maxRows, prefix = "", continuationPrefix = prefix, children, columns }) {
|
|
38614
|
+
const safeRows = Math.max(1, maxRows);
|
|
38615
|
+
const tail = streamTailLayout(text, columns, safeRows, prefix, continuationPrefix);
|
|
38427
38616
|
const rows = tail.text.split("\n");
|
|
38428
38617
|
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, {
|
|
38429
38618
|
key: index,
|
|
@@ -39003,7 +39192,10 @@ function StatusLine({ facts, stats, busy, columns, items, onRows, animated }) {
|
|
|
39003
39192
|
}, (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...leftParts), rightParts.length > 0 ? (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...rightParts) : void 0);
|
|
39004
39193
|
};
|
|
39005
39194
|
const row2Present = layout.row2.left.length > 0;
|
|
39006
|
-
return (0, import_react.createElement)(Box, {
|
|
39195
|
+
return (0, import_react.createElement)(Box, {
|
|
39196
|
+
flexDirection: "column",
|
|
39197
|
+
width: columns
|
|
39198
|
+
}, renderRow(layout.row1, "s1"), row2Present ? renderRow(layout.row2, "s2", 2) : void 0);
|
|
39007
39199
|
}
|
|
39008
39200
|
/**
|
|
39009
39201
|
* One fixed-height local feedback row. Errors remain visible while a slash
|
|
@@ -39265,21 +39457,32 @@ function StaticTranscript({ items }) {
|
|
|
39265
39457
|
});
|
|
39266
39458
|
}
|
|
39267
39459
|
const MemoStaticTranscript = (0, import_react.memo)(StaticTranscript);
|
|
39268
|
-
/** Build one settled
|
|
39460
|
+
/** Build one settled entry as stable physical rows shared by Static and viewport. */
|
|
39269
39461
|
function buildSettledRow(entry, index, showReasoning, columns) {
|
|
39270
|
-
const
|
|
39271
|
-
|
|
39462
|
+
const physical = settledEntryLines(entry, Math.max(10, columns - 2), showReasoning).map((line, row) => ({
|
|
39463
|
+
line,
|
|
39464
|
+
element: (0, import_react.createElement)(Box, { key: `entry-${index}-row-${row}` }, (0, import_react.createElement)(StyledRows, { lines: [line] }))
|
|
39465
|
+
}));
|
|
39466
|
+
if (entry.kind === "user" && !entry.notice) {
|
|
39467
|
+
const blank = { segments: [] };
|
|
39468
|
+
physical.unshift({
|
|
39469
|
+
line: blank,
|
|
39470
|
+
element: (0, import_react.createElement)(Box, {
|
|
39471
|
+
key: `prompt-before-${index}`,
|
|
39472
|
+
paddingX: 1
|
|
39473
|
+
}, (0, import_react.createElement)(Text, null, " "))
|
|
39474
|
+
});
|
|
39475
|
+
physical.push({
|
|
39476
|
+
line: blank,
|
|
39477
|
+
element: (0, import_react.createElement)(Box, {
|
|
39478
|
+
key: `prompt-after-${index}`,
|
|
39479
|
+
paddingX: 1
|
|
39480
|
+
}, (0, import_react.createElement)(Text, null, " "))
|
|
39481
|
+
});
|
|
39482
|
+
}
|
|
39272
39483
|
return {
|
|
39273
|
-
|
|
39274
|
-
|
|
39275
|
-
key: `prompt-before-${index}`,
|
|
39276
|
-
paddingX: 1
|
|
39277
|
-
}, (0, import_react.createElement)(Text, null, " ")) : void 0,
|
|
39278
|
-
after: roomyPrompt ? (0, import_react.createElement)(Box, {
|
|
39279
|
-
key: `prompt-after-${index}`,
|
|
39280
|
-
paddingX: 1
|
|
39281
|
-
}, (0, import_react.createElement)(Text, null, " ")) : void 0,
|
|
39282
|
-
rows: lines.length + (roomyPrompt ? 2 : 0)
|
|
39484
|
+
physical,
|
|
39485
|
+
rows: physical.length
|
|
39283
39486
|
};
|
|
39284
39487
|
}
|
|
39285
39488
|
/** The dim hint row placed under the header once the window has dropped entries. */
|
|
@@ -39305,8 +39508,9 @@ function settledTrimHint(droppedEntries, columns) {
|
|
|
39305
39508
|
*
|
|
39306
39509
|
* RENDERED-HISTORY CAP: the window holds at most `rowCap` physical rows of
|
|
39307
39510
|
* settled transcript (header and hint reserved on top). The cap exists only
|
|
39308
|
-
* here — the event log, the store projection, /export,
|
|
39309
|
-
*
|
|
39511
|
+
* here — the event log, the store projection, /export, and /resume keep the
|
|
39512
|
+
* full history; Ctrl+O keeps its inspectable subset. Ink 5's <Static> is a
|
|
39513
|
+
* consumption counter
|
|
39310
39514
|
* (items.slice(index) keyed on length): deleting head items mid-stream while
|
|
39311
39515
|
* appending tail items can permanently swallow new rows, so the append branch
|
|
39312
39516
|
* NEVER drops the head — it only accounts rows and flags `needsTrim` once the
|
|
@@ -39328,7 +39532,7 @@ function settledTrimHint(droppedEntries, columns) {
|
|
|
39328
39532
|
function computeSettledRows(previous, entries, settled, showReasoning, resumed, epoch, columns = 80, rowCap = SETTLED_ROW_CAP) {
|
|
39329
39533
|
if (previous === void 0 || previous.epoch !== epoch || previous.resumed !== resumed || settled < previous.entries.length) {
|
|
39330
39534
|
const records = /* @__PURE__ */ new Map();
|
|
39331
|
-
const
|
|
39535
|
+
const physical = [];
|
|
39332
39536
|
let windowRows = 0;
|
|
39333
39537
|
let droppedEntries = 0;
|
|
39334
39538
|
let index = settled - 1;
|
|
@@ -39342,19 +39546,18 @@ function computeSettledRows(previous, entries, settled, showReasoning, resumed,
|
|
|
39342
39546
|
}
|
|
39343
39547
|
records.set(entry, record);
|
|
39344
39548
|
windowRows += record.rows;
|
|
39345
|
-
|
|
39346
|
-
window.unshift(record.box);
|
|
39347
|
-
if (record.before !== void 0) window.unshift(record.before);
|
|
39549
|
+
physical.unshift(...record.physical);
|
|
39348
39550
|
}
|
|
39349
39551
|
const header = (0, import_react.createElement)(Header, {
|
|
39350
39552
|
key: "header",
|
|
39351
39553
|
resumed
|
|
39352
39554
|
});
|
|
39555
|
+
const rowElements = physical.map((row) => row.element);
|
|
39353
39556
|
const flat = droppedEntries > 0 ? [
|
|
39354
39557
|
header,
|
|
39355
39558
|
settledTrimHint(droppedEntries, columns),
|
|
39356
|
-
...
|
|
39357
|
-
] : [header, ...
|
|
39559
|
+
...rowElements
|
|
39560
|
+
] : [header, ...rowElements];
|
|
39358
39561
|
return {
|
|
39359
39562
|
cache: {
|
|
39360
39563
|
entries: entries.slice(droppedEntries, settled),
|
|
@@ -39365,6 +39568,7 @@ function computeSettledRows(previous, entries, settled, showReasoning, resumed,
|
|
|
39365
39568
|
epoch,
|
|
39366
39569
|
columns,
|
|
39367
39570
|
flat,
|
|
39571
|
+
physical,
|
|
39368
39572
|
droppedEntries,
|
|
39369
39573
|
totalRows: windowRows,
|
|
39370
39574
|
needsTrim: false
|
|
@@ -39393,9 +39597,7 @@ function computeSettledRows(previous, entries, settled, showReasoning, resumed,
|
|
|
39393
39597
|
records.set(entry, record);
|
|
39394
39598
|
suffix.push(entry);
|
|
39395
39599
|
deltaRows += record.rows;
|
|
39396
|
-
|
|
39397
|
-
added.push(record.box);
|
|
39398
|
-
if (record.after !== void 0) added.push(record.after);
|
|
39600
|
+
added.push(...record.physical);
|
|
39399
39601
|
}
|
|
39400
39602
|
const totalRows = previous.totalRows + deltaRows;
|
|
39401
39603
|
const needsTrim = rowCap > 0 && totalRows > rowCap + Math.floor(rowCap / 4);
|
|
@@ -39408,7 +39610,8 @@ function computeSettledRows(previous, entries, settled, showReasoning, resumed,
|
|
|
39408
39610
|
showReasoning,
|
|
39409
39611
|
epoch: previous.epoch,
|
|
39410
39612
|
columns: previous.columns,
|
|
39411
|
-
flat: previous.flat.concat(added),
|
|
39613
|
+
flat: previous.flat.concat(added.map((row) => row.element)),
|
|
39614
|
+
physical: previous.physical.concat(added),
|
|
39412
39615
|
droppedEntries: previous.droppedEntries,
|
|
39413
39616
|
totalRows,
|
|
39414
39617
|
needsTrim
|
|
@@ -39797,6 +40000,7 @@ function App(props) {
|
|
|
39797
40000
|
panelSurfacesRef.current = panelSurfaces;
|
|
39798
40001
|
const openPanel = panelSurfaces.find((surface) => surface.open);
|
|
39799
40002
|
const inspectorVisible = verboseOpen && !approvalPending && !questionPending;
|
|
40003
|
+
const verboseEntries = (0, import_react.useMemo)(() => verboseOpen ? inspectableTranscriptEntries(view.entries) : [], [verboseOpen, view.entries]);
|
|
39800
40004
|
const modalVisible = openPanel !== void 0 || inspectorVisible || approvalPending || questionPending;
|
|
39801
40005
|
const inputActive = deleteConfirmId !== void 0 ? !approvalPending && !questionPending : !modalVisible;
|
|
39802
40006
|
const transcriptVisible = !modalVisible;
|
|
@@ -39813,10 +40017,15 @@ function App(props) {
|
|
|
39813
40017
|
}));
|
|
39814
40018
|
const terminalSizeRef = (0, import_react.useRef)(terminalSize);
|
|
39815
40019
|
const settledRowsCache = (0, import_react.useRef)(void 0);
|
|
39816
|
-
const
|
|
40020
|
+
const viewportFlushRef = (0, import_react.useRef)({
|
|
40021
|
+
sessionKey: props.sessionKey,
|
|
40022
|
+
epoch: refreshEpoch,
|
|
40023
|
+
rows: 0
|
|
40024
|
+
});
|
|
40025
|
+
const settledRowsResult = (0, import_react.useMemo)(() => {
|
|
39817
40026
|
const result = computeSettledRows(settledRowsCache.current, view.entries, settled, showReasoning, props.resumed, refreshEpoch, terminalSize.columns);
|
|
39818
40027
|
settledRowsCache.current = result.cache;
|
|
39819
|
-
return result
|
|
40028
|
+
return result;
|
|
39820
40029
|
}, [
|
|
39821
40030
|
view.entries,
|
|
39822
40031
|
settled,
|
|
@@ -39891,17 +40100,54 @@ function App(props) {
|
|
|
39891
40100
|
const deepDivingVisible = busy && !streamingActive;
|
|
39892
40101
|
const tabTitle = view.title === "" ? DEFAULT_TERMINAL_TITLE : view.title;
|
|
39893
40102
|
useTerminalTitle(tabTitle);
|
|
39894
|
-
const
|
|
40103
|
+
const maximumTranscriptRows = liveRegionBudget({
|
|
40104
|
+
terminalRows,
|
|
40105
|
+
composerRows: 1,
|
|
40106
|
+
statusBarRows: 1,
|
|
40107
|
+
menuRows: 0,
|
|
40108
|
+
gutterRows: composerGutterRows,
|
|
40109
|
+
notice: false,
|
|
40110
|
+
todo: false,
|
|
40111
|
+
agents: false
|
|
40112
|
+
});
|
|
40113
|
+
const settledPhysical = settledRowsResult.cache.physical;
|
|
40114
|
+
const viewportStep = advanceTranscriptViewport({
|
|
40115
|
+
sessionKey: viewportFlushRef.current.sessionKey,
|
|
40116
|
+
epoch: viewportFlushRef.current.epoch,
|
|
40117
|
+
flushedRows: viewportFlushRef.current.rows
|
|
40118
|
+
}, {
|
|
40119
|
+
sessionKey: props.sessionKey,
|
|
40120
|
+
epoch: refreshEpoch,
|
|
40121
|
+
totalRows: settledPhysical.length,
|
|
40122
|
+
retainedRows: maximumTranscriptRows
|
|
40123
|
+
});
|
|
40124
|
+
const flushedRows = viewportStep.staticRows;
|
|
40125
|
+
viewportFlushRef.current = {
|
|
40126
|
+
sessionKey: viewportStep.cursor.sessionKey,
|
|
40127
|
+
epoch: viewportStep.cursor.epoch,
|
|
40128
|
+
rows: viewportStep.cursor.flushedRows
|
|
40129
|
+
};
|
|
40130
|
+
const staticPrefixRows = settledRowsResult.cache.flat.length - settledPhysical.length;
|
|
40131
|
+
const settledRows = settledRowsResult.cache.flat.slice(0, staticPrefixRows + flushedRows);
|
|
40132
|
+
const retainedSettledLines = settledPhysical.slice(flushedRows).map((row) => row.line);
|
|
40133
|
+
const mutableLiveLines = (0, import_react.useMemo)(() => view.entries.slice(settled).flatMap((entry) => transcriptEntryLines(entry, Math.max(1, terminalColumns - 2), showReasoning)), [
|
|
39895
40134
|
view.entries,
|
|
39896
40135
|
settled,
|
|
39897
40136
|
terminalColumns,
|
|
39898
40137
|
showReasoning
|
|
39899
40138
|
]);
|
|
39900
|
-
const
|
|
39901
|
-
const
|
|
39902
|
-
const
|
|
39903
|
-
const
|
|
39904
|
-
const
|
|
40139
|
+
const allLiveLines = retainedSettledLines.concat(mutableLiveLines);
|
|
40140
|
+
const provisionalLiveBudget = busy || streamingActive ? Math.max(1, Math.floor(dynamicRows / 3)) : Math.max(0, dynamicRows - (deepDivingVisible ? 1 : 0));
|
|
40141
|
+
const provisionalLiveRows = Math.min(allLiveLines.length, provisionalLiveBudget);
|
|
40142
|
+
const provisionalStreamRows = Math.max(1, dynamicRows - provisionalLiveRows);
|
|
40143
|
+
const provisionalReasoningRows = view.streamingReasoning === "" ? 0 : view.streaming === "" ? provisionalStreamRows : provisionalStreamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(provisionalStreamRows / 3)) : 1;
|
|
40144
|
+
const provisionalAnswerRows = view.streaming === "" ? 0 : Math.max(1, provisionalStreamRows - provisionalReasoningRows);
|
|
40145
|
+
const streamColumns = Math.max(1, terminalColumns - 2);
|
|
40146
|
+
const reasoningRows = view.streamingReasoning === "" || provisionalReasoningRows === 0 ? 0 : showReasoning ? streamTailPhysicalRows(view.streamingReasoning, streamColumns, provisionalReasoningRows, "✻ ", " ") : 1;
|
|
40147
|
+
const answerRows = view.streaming === "" || provisionalAnswerRows === 0 ? 0 : streamTailPhysicalRows(view.streaming, streamColumns, provisionalAnswerRows, " ");
|
|
40148
|
+
const nonHistoryRows = reasoningRows + answerRows + (deepDivingVisible ? 1 : 0);
|
|
40149
|
+
const visibleHistoryRows = visibleTranscriptRows(allLiveLines.length, nonHistoryRows, dynamicRows);
|
|
40150
|
+
const visibleLiveLines = visibleHistoryRows === 0 ? [] : allLiveLines.slice(-visibleHistoryRows);
|
|
39905
40151
|
const liveAudit = clampLiveAllocation({
|
|
39906
40152
|
live: visibleLiveLines.length,
|
|
39907
40153
|
reasoning: reasoningRows,
|
|
@@ -39914,6 +40160,11 @@ function App(props) {
|
|
|
39914
40160
|
const auditedLiveLines = liveAudit.allocation.live === visibleLiveLines.length ? visibleLiveLines : visibleLiveLines.slice(-liveAudit.allocation.live);
|
|
39915
40161
|
const auditedReasoningRows = liveAudit.allocation.reasoning;
|
|
39916
40162
|
const auditedAnswerRows = liveAudit.allocation.answer;
|
|
40163
|
+
const transcriptViewportFilled = allLiveLines.length + nonHistoryRows >= dynamicRows;
|
|
40164
|
+
const modalViewportFilled = allLiveLines.length >= dynamicRows;
|
|
40165
|
+
const anchoredSurfaceRows = dynamicRows + (transcriptVisible && view.todos.length > 0 ? 1 : 0) + (transcriptVisible && agentRows.length > 0 ? 1 : 0);
|
|
40166
|
+
const surfaceAnchored = transcriptVisible ? transcriptViewportFilled : modalViewportFilled;
|
|
40167
|
+
const frozenModalLines = surfaceAnchored && !transcriptVisible ? allLiveLines.slice(-dynamicRows) : [];
|
|
39917
40168
|
const keyboardOwner = approvalPending ? "the approval prompt" : questionPending ? "the question" : openPanel?.hint ?? (inspectorVisible ? "history details" : void 0);
|
|
39918
40169
|
useStableInput((input, key) => {
|
|
39919
40170
|
if (!(key.ctrl && input === "c")) return;
|
|
@@ -39957,10 +40208,15 @@ function App(props) {
|
|
|
39957
40208
|
const attachedView = (0, import_react.useSyncExternalStore)(subscribeAttached, readAttached);
|
|
39958
40209
|
const attachedSettled = (0, import_react.useMemo)(() => settledEntryCount(attachedView.entries), [attachedView.entries]);
|
|
39959
40210
|
const attachedRowsCache = (0, import_react.useRef)(void 0);
|
|
39960
|
-
const
|
|
40211
|
+
const attachedViewportFlushRef = (0, import_react.useRef)({
|
|
40212
|
+
sessionKey: "none",
|
|
40213
|
+
epoch: refreshEpoch,
|
|
40214
|
+
rows: 0
|
|
40215
|
+
});
|
|
40216
|
+
const attachedSettledRowsResult = (0, import_react.useMemo)(() => {
|
|
39961
40217
|
const result = computeSettledRows(attachedRowsCache.current, attachedView.entries, attachedSettled, showReasoning, true, refreshEpoch, terminalSize.columns);
|
|
39962
40218
|
attachedRowsCache.current = result.cache;
|
|
39963
|
-
return result
|
|
40219
|
+
return result;
|
|
39964
40220
|
}, [
|
|
39965
40221
|
attachedView.entries,
|
|
39966
40222
|
attachedSettled,
|
|
@@ -39968,6 +40224,26 @@ function App(props) {
|
|
|
39968
40224
|
refreshEpoch,
|
|
39969
40225
|
terminalSize.columns
|
|
39970
40226
|
]);
|
|
40227
|
+
const attachDynamicRows = Math.max(1, terminalRows - 3 - 1 - 1 - 2);
|
|
40228
|
+
const attachedPhysical = attachedSettledRowsResult.cache.physical;
|
|
40229
|
+
const attachedViewportStep = advanceTranscriptViewport({
|
|
40230
|
+
sessionKey: attachedViewportFlushRef.current.sessionKey,
|
|
40231
|
+
epoch: attachedViewportFlushRef.current.epoch,
|
|
40232
|
+
flushedRows: attachedViewportFlushRef.current.rows
|
|
40233
|
+
}, {
|
|
40234
|
+
sessionKey: attachment?.id ?? "none",
|
|
40235
|
+
epoch: refreshEpoch,
|
|
40236
|
+
totalRows: attachedPhysical.length,
|
|
40237
|
+
retainedRows: attachDynamicRows
|
|
40238
|
+
});
|
|
40239
|
+
attachedViewportFlushRef.current = {
|
|
40240
|
+
sessionKey: attachedViewportStep.cursor.sessionKey,
|
|
40241
|
+
epoch: attachedViewportStep.cursor.epoch,
|
|
40242
|
+
rows: attachedViewportStep.cursor.flushedRows
|
|
40243
|
+
};
|
|
40244
|
+
const attachedStaticPrefixRows = attachedSettledRowsResult.cache.flat.length - attachedPhysical.length;
|
|
40245
|
+
const attachedSettledRows = attachedSettledRowsResult.cache.flat.slice(0, attachedStaticPrefixRows + attachedViewportStep.staticRows);
|
|
40246
|
+
const attachedRetainedLines = attachedPhysical.slice(attachedViewportStep.staticRows).map((row) => row.line);
|
|
39971
40247
|
/** Detach and hand the keyboard back to the /agents list. */
|
|
39972
40248
|
const detach = (0, import_react.useCallback)(() => {
|
|
39973
40249
|
setAttachment((current) => {
|
|
@@ -40342,25 +40618,33 @@ function App(props) {
|
|
|
40342
40618
|
});
|
|
40343
40619
|
}
|
|
40344
40620
|
if (attachment !== void 0) {
|
|
40345
|
-
const attachDynamicRows = Math.max(1, terminalRows - 3 - 1 - 1 - 2);
|
|
40346
40621
|
const attachBusy = attachedView.busySince !== void 0;
|
|
40347
40622
|
const attachStreaming = attachedView.streaming !== "" || attachedView.streamingReasoning !== "";
|
|
40348
|
-
const
|
|
40349
|
-
const
|
|
40350
|
-
const
|
|
40351
|
-
const attachStreamRows = Math.max(1, attachDynamicRows -
|
|
40352
|
-
const
|
|
40353
|
-
const
|
|
40623
|
+
const attachMutableLines = attachedView.entries.slice(attachedSettled).flatMap((entry) => transcriptEntryLines(entry, Math.max(1, terminalColumns - 2), showReasoning));
|
|
40624
|
+
const attachAllLiveLines = attachedRetainedLines.concat(attachMutableLines);
|
|
40625
|
+
const attachProvisionalLiveRows = Math.min(attachAllLiveLines.length, attachBusy || attachStreaming ? Math.max(1, Math.floor(attachDynamicRows / 3)) : attachDynamicRows);
|
|
40626
|
+
const attachStreamRows = Math.max(1, attachDynamicRows - attachProvisionalLiveRows);
|
|
40627
|
+
const attachReasoningLimit = attachedView.streamingReasoning === "" ? 0 : attachedView.streaming === "" ? attachStreamRows : attachStreamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(attachStreamRows / 3)) : 1;
|
|
40628
|
+
const attachAnswerLimit = attachedView.streaming === "" ? 0 : Math.max(1, attachStreamRows - attachReasoningLimit);
|
|
40629
|
+
const attachReasoningRows = attachedView.streamingReasoning === "" || attachReasoningLimit === 0 ? 0 : showReasoning ? streamTailPhysicalRows(attachedView.streamingReasoning, Math.max(1, terminalColumns - 2), attachReasoningLimit, "✻ ", " ") : 1;
|
|
40630
|
+
const attachAnswerRows = attachedView.streaming === "" || attachAnswerLimit === 0 ? 0 : streamTailPhysicalRows(attachedView.streaming, Math.max(1, terminalColumns - 2), attachAnswerLimit, " ");
|
|
40631
|
+
const attachNonHistoryRows = attachReasoningRows + attachAnswerRows + (attachBusy && !attachStreaming ? 1 : 0);
|
|
40632
|
+
const attachVisibleHistoryRows = visibleTranscriptRows(attachAllLiveLines.length, attachNonHistoryRows, attachDynamicRows);
|
|
40633
|
+
const attachVisibleLive = attachVisibleHistoryRows === 0 ? [] : attachAllLiveLines.slice(-attachVisibleHistoryRows);
|
|
40354
40634
|
const attachAudit = clampLiveAllocation({
|
|
40355
40635
|
live: attachVisibleLive.length,
|
|
40356
40636
|
reasoning: attachReasoningRows,
|
|
40357
40637
|
answer: attachAnswerRows
|
|
40358
40638
|
}, attachDynamicRows);
|
|
40639
|
+
const attachViewportFilled = attachAllLiveLines.length + attachNonHistoryRows >= attachDynamicRows;
|
|
40359
40640
|
const attachTurns = attachedView.entries.filter((entry) => entry.kind === "turn-marker").length;
|
|
40360
40641
|
return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(MemoStaticTranscript, {
|
|
40361
40642
|
key: refreshEpoch,
|
|
40362
40643
|
items: attachedSettledRows
|
|
40363
|
-
}), (0, import_react.createElement)(Box,
|
|
40644
|
+
}), (0, import_react.createElement)(Box, attachViewportFilled ? {
|
|
40645
|
+
flexDirection: "column",
|
|
40646
|
+
height: attachDynamicRows
|
|
40647
|
+
} : { flexDirection: "column" }, attachAudit.allocation.live === attachVisibleLive.length && attachVisibleLive.length > 0 ? (0, import_react.createElement)(StyledRows, { lines: attachVisibleLive }) : void 0, attachedView.streamingReasoning !== "" && attachAudit.allocation.reasoning > 0 ? (0, import_react.createElement)(StreamTail, {
|
|
40364
40648
|
text: showReasoning ? attachedView.streamingReasoning : "Thinking…",
|
|
40365
40649
|
prefix: "✻ ",
|
|
40366
40650
|
continuationPrefix: " ",
|
|
@@ -40373,9 +40657,9 @@ function App(props) {
|
|
|
40373
40657
|
maxRows: attachAudit.allocation.answer,
|
|
40374
40658
|
prefix: " ",
|
|
40375
40659
|
columns: Math.max(1, terminalColumns - 2)
|
|
40376
|
-
}, attachBusy ? (0, import_react.createElement)(Caret
|
|
40660
|
+
}, attachBusy ? (0, import_react.createElement)(Caret) : void 0) : void 0, attachBusy && attachedView.streaming === "" && attachedView.streamingReasoning === "" ? (0, import_react.createElement)(DeepDivingLine, {
|
|
40377
40661
|
since: attachedView.busySince,
|
|
40378
|
-
|
|
40662
|
+
themeFlowAnimated: animations
|
|
40379
40663
|
}) : void 0), (0, import_react.createElement)(Box, {
|
|
40380
40664
|
borderStyle: "round",
|
|
40381
40665
|
borderColor: inkColor(getPalette().brandMid),
|
|
@@ -40396,7 +40680,17 @@ function App(props) {
|
|
|
40396
40680
|
return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(MemoStaticTranscript, {
|
|
40397
40681
|
key: refreshEpoch,
|
|
40398
40682
|
items: attachment === void 0 ? settledRows : attachedSettledRows
|
|
40399
|
-
}),
|
|
40683
|
+
}), (0, import_react.createElement)(Box, surfaceAnchored ? {
|
|
40684
|
+
flexDirection: "column",
|
|
40685
|
+
height: anchoredSurfaceRows
|
|
40686
|
+
} : { flexDirection: "column" }, frozenModalLines.length === 0 ? void 0 : (0, import_react.createElement)(Box, {
|
|
40687
|
+
flexDirection: "column",
|
|
40688
|
+
height: 0,
|
|
40689
|
+
flexGrow: 1,
|
|
40690
|
+
flexShrink: 1,
|
|
40691
|
+
overflow: "hidden",
|
|
40692
|
+
justifyContent: "flex-end"
|
|
40693
|
+
}, (0, import_react.createElement)(StyledRows, { lines: frozenModalLines })), transcriptVisible ? (0, import_react.createElement)(Box, { flexDirection: "column" }, auditedLiveLines.length === 0 ? void 0 : (0, import_react.createElement)(StyledRows, { lines: auditedLiveLines }), view.streamingReasoning !== "" && auditedReasoningRows > 0 ? showReasoning ? (0, import_react.createElement)(StreamTail, {
|
|
40400
40694
|
text: view.streamingReasoning,
|
|
40401
40695
|
prefix: "✻ ",
|
|
40402
40696
|
continuationPrefix: " ",
|
|
@@ -40405,7 +40699,7 @@ function App(props) {
|
|
|
40405
40699
|
columns: Math.max(1, terminalColumns - 2)
|
|
40406
40700
|
}) : view.streaming === "" ? (0, import_react.createElement)(ShimmerLine, {
|
|
40407
40701
|
text: "✻ Thinking… (Ctrl/Alt+R to expand)",
|
|
40408
|
-
|
|
40702
|
+
themeFlowAnimated: animations
|
|
40409
40703
|
}) : (0, import_react.createElement)(StreamTail, {
|
|
40410
40704
|
text: "Thinking… (Ctrl/Alt+R to expand)",
|
|
40411
40705
|
prefix: "✻ ",
|
|
@@ -40419,9 +40713,9 @@ function App(props) {
|
|
|
40419
40713
|
maxRows: auditedAnswerRows,
|
|
40420
40714
|
prefix: " ",
|
|
40421
40715
|
columns: Math.max(1, terminalColumns - 2)
|
|
40422
|
-
}, busy ? (0, import_react.createElement)(Caret
|
|
40716
|
+
}, busy ? (0, import_react.createElement)(Caret) : void 0) : void 0, deepDivingVisible ? (0, import_react.createElement)(DeepDivingLine, {
|
|
40423
40717
|
since: view.busySince,
|
|
40424
|
-
|
|
40718
|
+
themeFlowAnimated: animations
|
|
40425
40719
|
}) : void 0) : void 0, transcriptVisible ? (0, import_react.createElement)(TodoPanel, { todos: view.todos }) : void 0, transcriptVisible ? (0, import_react.createElement)(AgentsLine, {
|
|
40426
40720
|
rows: agentRows,
|
|
40427
40721
|
total: props.subagents.getTotalSeen()
|
|
@@ -40469,7 +40763,7 @@ function App(props) {
|
|
|
40469
40763
|
},
|
|
40470
40764
|
close: () => setReviewPickerOpen(false)
|
|
40471
40765
|
}) : void 0, verboseOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(MemoVerbosePanel, {
|
|
40472
|
-
entries:
|
|
40766
|
+
entries: verboseEntries,
|
|
40473
40767
|
onClose: closeInspector,
|
|
40474
40768
|
columns: terminalColumns,
|
|
40475
40769
|
rows: terminalRows
|
|
@@ -40606,7 +40900,7 @@ function App(props) {
|
|
|
40606
40900
|
setSubagentOpen(false);
|
|
40607
40901
|
},
|
|
40608
40902
|
close: () => setSubagentOpen(false)
|
|
40609
|
-
}) : void 0, notice === void 0 ? void 0 : (0, import_react.createElement)(NoticeLine, {
|
|
40903
|
+
}) : void 0), notice === void 0 ? void 0 : (0, import_react.createElement)(NoticeLine, {
|
|
40610
40904
|
text: notice.text,
|
|
40611
40905
|
tone: notice.tone,
|
|
40612
40906
|
columns: terminalColumns
|
|
@@ -40790,7 +41084,7 @@ function App(props) {
|
|
|
40790
41084
|
stats: view.stats,
|
|
40791
41085
|
busy,
|
|
40792
41086
|
animated: animations,
|
|
40793
|
-
columns: terminalColumns,
|
|
41087
|
+
columns: Math.max(1, terminalColumns - 1),
|
|
40794
41088
|
items: statuslineItems,
|
|
40795
41089
|
onRows: handleStatusRows
|
|
40796
41090
|
})));
|
|
@@ -43053,18 +43347,29 @@ function createInputHistory(path, onFailure) {
|
|
|
43053
43347
|
flush: () => chain
|
|
43054
43348
|
};
|
|
43055
43349
|
}
|
|
43056
|
-
|
|
43057
|
-
|
|
43058
|
-
|
|
43059
|
-
*
|
|
43060
|
-
*
|
|
43061
|
-
* "until removed externally" — so removal is planned, layout-checked, and
|
|
43062
|
-
* lease-guarded here before any file is touched. Every read goes through the
|
|
43063
|
-
* in-process session-query engine, so this module stays free of `app.ts` and
|
|
43064
|
-
* of the Ink tree.
|
|
43350
|
+
/**
|
|
43351
|
+
* Map a collection with a fixed worker count while preserving input order.
|
|
43352
|
+
* The picker may own thousands of persisted sessions; an unbounded
|
|
43353
|
+
* `Promise.all` turns those into one filesystem burst and can exhaust handles
|
|
43354
|
+
* on Windows or remote filesystems.
|
|
43065
43355
|
*
|
|
43066
|
-
* @
|
|
43356
|
+
* @internal Exported for the deterministic concurrency regression.
|
|
43067
43357
|
*/
|
|
43358
|
+
async function mapConcurrent(items, concurrency, mapper) {
|
|
43359
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) throw new RangeError("concurrency must be a positive safe integer");
|
|
43360
|
+
const results = new Array(items.length);
|
|
43361
|
+
let next = 0;
|
|
43362
|
+
const worker = async () => {
|
|
43363
|
+
for (;;) {
|
|
43364
|
+
const index = next;
|
|
43365
|
+
if (index >= items.length) return;
|
|
43366
|
+
next += 1;
|
|
43367
|
+
results[index] = await mapper(items[index], index);
|
|
43368
|
+
}
|
|
43369
|
+
};
|
|
43370
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
|
|
43371
|
+
return results;
|
|
43372
|
+
}
|
|
43068
43373
|
/**
|
|
43069
43374
|
* Bind the session IO to one runner's services.
|
|
43070
43375
|
* @param services - the query engine, persistence, and the live-session probe.
|
|
@@ -43077,15 +43382,23 @@ function createSessionIo(services) {
|
|
|
43077
43382
|
const records = await sessionQuery.listSessions(signal);
|
|
43078
43383
|
const root = jsonlSessionRoot(persistence);
|
|
43079
43384
|
const updated = /* @__PURE__ */ new Map();
|
|
43080
|
-
if (root !== void 0)
|
|
43081
|
-
|
|
43082
|
-
|
|
43083
|
-
|
|
43084
|
-
|
|
43085
|
-
|
|
43086
|
-
|
|
43087
|
-
|
|
43088
|
-
|
|
43385
|
+
if (root !== void 0) {
|
|
43386
|
+
const mtimes = await mapConcurrent(records, 16, async (record) => {
|
|
43387
|
+
signal?.throwIfAborted();
|
|
43388
|
+
try {
|
|
43389
|
+
const dir = sessionDirectoryFor(root, record.header.cwd, record.header.id);
|
|
43390
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
43391
|
+
const stats = await Promise.all(entries.filter((entry) => entry.isFile() && isSessionArtifactName(entry.name)).map((entry) => stat(join(dir, entry.name))));
|
|
43392
|
+
signal?.throwIfAborted();
|
|
43393
|
+
const newest = Math.max(...stats.map((info) => info.mtimeMs));
|
|
43394
|
+
return Number.isFinite(newest) ? [record.header.id, newest] : void 0;
|
|
43395
|
+
} catch {
|
|
43396
|
+
signal?.throwIfAborted();
|
|
43397
|
+
return;
|
|
43398
|
+
}
|
|
43399
|
+
});
|
|
43400
|
+
for (const pair of mtimes) if (pair !== void 0) updated.set(...pair);
|
|
43401
|
+
}
|
|
43089
43402
|
const projected = projectSessionRows(records, {
|
|
43090
43403
|
...options,
|
|
43091
43404
|
query: ""
|