dsh-code 1.2.0 → 1.3.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/lib/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-runtime-CMFfr-1z.mjs";
2
- import { _ as parseRainbowArgument, a as dim, b as rerollRainbow, c as inkColor, d as parseThemeName, f as promptRowTokens, g as themeFlow, h as surfaceAccent, i as diffBackground, l as isPrismatic, m as setTheme, n as THEMES, o as getPalette, p as rowBackground, s as getTheme, t as FLOW_ANCHORS, u as isRainbow, v as rainbowRoll, x as chalk, y as rainbowSeedLabel } from "./theme-7u5Qo3dF.mjs";
2
+ import { _ as parseRainbowArgument, a as dim, b as rerollRainbow, c as inkColor, d as parseThemeName, f as promptRowTokens, g as themeFlow, h as surfaceAccent, i as diffBackground, l as isPrismatic, m as setTheme, n as THEMES, o as getPalette, p as rowBackground, s as getTheme, t as FLOW_ANCHORS, u as isRainbow, v as rainbowRoll, x as chalk, y as rainbowSeedLabel } from "./theme-B3orFUYz.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import * as fs from "node:fs";
@@ -12,6 +12,7 @@ import { installModelSelection } from "@deepseek-ai/dsh-agent";
12
12
  import { MessageId, ReasoningEffortId, assistantStreamFirstTokenTime, boundContextSummary, createUserMessage, isTokenDelta, normalizeApiKey } from "@deepseek-ai/dsh-llm";
13
13
  import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset } from "@deepseek-ai/dsh-session";
14
14
  import { deriveTurnTokenUsage } from "@deepseek-ai/dsh-token-meter/client";
15
+ import { SessionAlreadyOwnedError } from "@deepseek-ai/dsh-session-persistence";
15
16
  import { PassThrough, Stream } from "node:stream";
16
17
  import process$1, { cwd, env } from "node:process";
17
18
  import { EventEmitter } from "node:events";
@@ -24607,7 +24608,7 @@ function stringWidth(text) {
24607
24608
  function formatTokens(n) {
24608
24609
  const scaled = (v) => v >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10);
24609
24610
  if (n < 1e3) return String(n);
24610
- if (n < 1e6) return scaled(n / 1e3) + "K";
24611
+ if (n < 999500) return scaled(n / 1e3) + "K";
24611
24612
  return scaled(n / 1e6) + "M";
24612
24613
  }
24613
24614
  /** C0 controls except tab (0x09) and newline (0x0a), plus DEL and C1. */
@@ -24779,7 +24780,37 @@ const en = {
24779
24780
  "search.compact.searching": "searching…",
24780
24781
  "search.compact.error": "error: {message}",
24781
24782
  "search.footer": "↑↓ move · enter search/resume · esc close",
24783
+ "header.hint": "/help · Esc interrupt · Ctrl+C quit",
24784
+ "header.hintResumed": "resumed · /help · Esc interrupt",
24782
24785
  "statusline.title": "/statusline · items apply to the live status line below",
24786
+ "statusline.item.model": "model",
24787
+ "statusline.item.model.desc": "provider/model serving this session",
24788
+ "statusline.item.cwd": "cwd",
24789
+ "statusline.item.cwd.desc": "working-directory basename",
24790
+ "statusline.item.mode": "mode",
24791
+ "statusline.item.mode.desc": "agent preset composing the session",
24792
+ "statusline.item.branch": "branch",
24793
+ "statusline.item.branch.desc": "git branch inside a repository",
24794
+ "statusline.item.context": "context",
24795
+ "statusline.item.context.desc": "context-window occupancy meter",
24796
+ "statusline.item.permission": "permission",
24797
+ "statusline.item.permission.desc": "permission preset badge with cycle hint",
24798
+ "statusline.item.plan": "plan",
24799
+ "statusline.item.plan.desc": "plan-mode state mark",
24800
+ "statusline.item.turns": "turns",
24801
+ "statusline.item.turns.desc": "turn and step counters",
24802
+ "statusline.item.durations": "durations",
24803
+ "statusline.item.durations.desc": "llm/ttft/decode/tool wall time",
24804
+ "statusline.item.cache": "cache",
24805
+ "statusline.item.cache.desc": "cache-hit share of billed input",
24806
+ "statusline.item.tokens": "tokens",
24807
+ "statusline.item.tokens.desc": "cumulative input/output tokens",
24808
+ "statusline.item.title": "title",
24809
+ "statusline.item.title.desc": "session title or short id",
24810
+ "statusline.item.goal": "goal",
24811
+ "statusline.item.goal.desc": "live goal phase and round progress",
24812
+ "statusline.item.sandbox": "sandbox",
24813
+ "statusline.item.sandbox.desc": "divergent sandbox-mode override",
24783
24814
  "schedule.title": "/schedule · {count} active reminder",
24784
24815
  "schedule.titlePlural": "/schedule · {count} active reminders",
24785
24816
  "schedule.empty": "no active reminders — the model creates them with schedule_create",
@@ -24925,10 +24956,16 @@ const en = {
24925
24956
  "status.label.out": "out",
24926
24957
  "status.label.mode": "/mode",
24927
24958
  "status.label.context": "context",
24959
+ "status.label.sandbox": "sandbox",
24960
+ "status.plan.on": "plan on",
24961
+ "status.plan.mark": "⧉ plan",
24962
+ "status.goal.round": "◎ round {current}/{max}",
24963
+ "status.goal.phase": "◎ {phase}",
24928
24964
  "frozen.keysGoTo": "keys go to {owner} · esc {action}",
24929
24965
  "frozen.action.rejects": "rejects",
24930
24966
  "frozen.action.cancels": "cancels",
24931
24967
  "frozen.action.closes": "closes",
24968
+ "frozen.action.waits": "waits",
24932
24969
  "time.justNow": "now",
24933
24970
  "time.minutesAgo": "{n}m ago",
24934
24971
  "time.hoursAgo": "{n}h ago",
@@ -24940,6 +24977,7 @@ const en = {
24940
24977
  "notice.themeSaveFailed": "theme save failed: {message}",
24941
24978
  "notice.languageSaveFailed": "language save failed: {message}",
24942
24979
  "notice.alreadyActive": "that session is already active",
24980
+ "notice.permissionPresetsUnmounted": "permission presets are not mounted in this composition",
24943
24981
  "notice.queueCancelled": "queued message cancelled",
24944
24982
  "notice.queueActionFailed": "queue action failed: {message}",
24945
24983
  "notice.queueUnavailable": "queued message is no longer pending",
@@ -24954,6 +24992,8 @@ const en = {
24954
24992
  "notice.usage.animation": "usage: /animation [on|off]",
24955
24993
  "notice.usage.language": "usage: /language [en|zh]",
24956
24994
  "notice.usage.rainbow": "usage: /rainbow [seed]",
24995
+ "notice.usage.bareCommand": "usage: /{name}",
24996
+ "notice.switchInProgress": "switching sessions — send again once the new session is up, or /resume cancel",
24957
24997
  "notice.imageCancelled": "image submission cancelled",
24958
24998
  "notice.themeConfigUnreadable": "theme config unreadable, using dark: {message}",
24959
24999
  "notice.languageConfigUnreadable": "language config unreadable, using english: {message}",
@@ -24984,6 +25024,13 @@ const en = {
24984
25024
  "notice.resumeFailed": "resume failed: {message}",
24985
25025
  "notice.forkFailed": "fork failed: {message}",
24986
25026
  "notice.sessionSwitchFailed": "session switch failed: {message}",
25027
+ "notice.sessionCreated": "created {id} · mode {mode}",
25028
+ "notice.sessionResumed": "resumed {id} · mode {mode}",
25029
+ "notice.sessionSwitchedDirty": "switched to {id}, but {detail}",
25030
+ "notice.flushFailed": "previous session flush failed: {message}",
25031
+ "notice.agentReleaseFailed": "previous agent release failed: {message}",
25032
+ "notice.copyEmpty": "nothing to copy yet",
25033
+ "notice.copied": "copied latest response",
24987
25034
  "notice.switchQueued": "will switch to {label} when the current turn finishes · /resume cancel to abort",
24988
25035
  "notice.reviewStarted": "review started under read-only permissions",
24989
25036
  "notice.reviewFailed": "review failed: {message}",
@@ -25035,7 +25082,8 @@ const en = {
25035
25082
  "panel.footer.chooseSelect": "↑↓ choose · enter select · r refresh · esc close",
25036
25083
  "panel.footer.inspectDetails": "↑↓ inspect · enter details · r refresh · esc close",
25037
25084
  "panel.footer.inspectRefresh": "↑↓ inspect · r refresh · esc close",
25038
- "panel.footer.resume": "tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · d delete · enter resume",
25085
+ "panel.footer.resume": "tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · enter resume",
25086
+ "panel.footer.delete": "tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · enter delete · esc close",
25039
25087
  "panel.footer.transcript": "lines {from}-{to}/{total} · ↑↓/pg/g/G · t/esc close",
25040
25088
  "panel.footer.history": "↑↓ move · g/G ends · enter fill · esc close",
25041
25089
  "panel.footer.review": "↑↓ move · enter select · esc back · q close",
@@ -25047,6 +25095,7 @@ const en = {
25047
25095
  "panel.jobs.title": "/jobs · background tasks · {count}",
25048
25096
  "panel.resume.deleteTitle": "permanently delete {target}? this cannot be undone · subagent threads go too",
25049
25097
  "panel.resume.title": "/resume{mode}{search} · {toolbar}",
25098
+ "panel.delete.title": "/delete{search} · {toolbar}",
25050
25099
  "panel.noActiveReminders": "no active reminders — the model schedules via schedule_create",
25051
25100
  "panel.notMounted": "not mounted",
25052
25101
  "panel.broken": "broken: {message}",
@@ -25222,7 +25271,37 @@ const zh = {
25222
25271
  "search.compact.searching": "搜索中…",
25223
25272
  "search.compact.error": "错误:{message}",
25224
25273
  "search.footer": "↑↓ 移动 · 回车 搜索/恢复 · esc 关闭",
25274
+ "header.hint": "/help · Esc 中断 · Ctrl+C 退出",
25275
+ "header.hintResumed": "已恢复 · /help · Esc 中断",
25225
25276
  "statusline.title": "/statusline · 选项实时应用到下方状态栏",
25277
+ "statusline.item.model": "模型",
25278
+ "statusline.item.model.desc": "本会话使用的 provider/model",
25279
+ "statusline.item.cwd": "目录",
25280
+ "statusline.item.cwd.desc": "工作目录基名",
25281
+ "statusline.item.mode": "模式",
25282
+ "statusline.item.mode.desc": "当前 agent 预设",
25283
+ "statusline.item.branch": "分支",
25284
+ "statusline.item.branch.desc": "仓库内 git 分支",
25285
+ "statusline.item.context": "上下文",
25286
+ "statusline.item.context.desc": "上下文窗口占用",
25287
+ "statusline.item.permission": "权限",
25288
+ "statusline.item.permission.desc": "权限预设徽章与循环提示",
25289
+ "statusline.item.plan": "计划",
25290
+ "statusline.item.plan.desc": "计划模式标记",
25291
+ "statusline.item.turns": "回合",
25292
+ "statusline.item.turns.desc": "回合与步骤计数",
25293
+ "statusline.item.durations": "耗时",
25294
+ "statusline.item.durations.desc": "模型/首字/解码/工具墙钟",
25295
+ "statusline.item.cache": "缓存",
25296
+ "statusline.item.cache.desc": "计费输入的缓存命中占比",
25297
+ "statusline.item.tokens": "用量",
25298
+ "statusline.item.tokens.desc": "累计输入/输出 token",
25299
+ "statusline.item.title": "标题",
25300
+ "statusline.item.title.desc": "会话标题或短 id",
25301
+ "statusline.item.goal": "目标",
25302
+ "statusline.item.goal.desc": "进行中的目标阶段与轮次",
25303
+ "statusline.item.sandbox": "沙箱",
25304
+ "statusline.item.sandbox.desc": "与预设不同的沙箱覆盖",
25226
25305
  "schedule.title": "/schedule · {count} 条活动提醒",
25227
25306
  "schedule.titlePlural": "/schedule · {count} 条活动提醒",
25228
25307
  "schedule.empty": "暂无活动提醒——模型通过 schedule_create 创建",
@@ -25368,10 +25447,16 @@ const zh = {
25368
25447
  "status.label.out": "出",
25369
25448
  "status.label.mode": "/mode",
25370
25449
  "status.label.context": "上下文",
25450
+ "status.label.sandbox": "沙箱",
25451
+ "status.plan.on": "计划开",
25452
+ "status.plan.mark": "⧉ 计划",
25453
+ "status.goal.round": "◎ 第 {current}/{max} 轮",
25454
+ "status.goal.phase": "◎ {phase}",
25371
25455
  "frozen.keysGoTo": "按键交给{owner} · esc {action}",
25372
25456
  "frozen.action.rejects": "拒绝",
25373
25457
  "frozen.action.cancels": "取消",
25374
25458
  "frozen.action.closes": "关闭",
25459
+ "frozen.action.waits": "等待",
25375
25460
  "time.justNow": "刚刚",
25376
25461
  "time.minutesAgo": "{n} 分钟前",
25377
25462
  "time.hoursAgo": "{n} 小时前",
@@ -25383,6 +25468,7 @@ const zh = {
25383
25468
  "notice.themeSaveFailed": "主题保存失败:{message}",
25384
25469
  "notice.languageSaveFailed": "语言保存失败:{message}",
25385
25470
  "notice.alreadyActive": "该会话已是当前会话",
25471
+ "notice.permissionPresetsUnmounted": "当前组合未挂载权限预设",
25386
25472
  "notice.queueCancelled": "已取消排队消息",
25387
25473
  "notice.queueActionFailed": "队列操作失败:{message}",
25388
25474
  "notice.queueUnavailable": "该消息已不在队列中",
@@ -25397,6 +25483,8 @@ const zh = {
25397
25483
  "notice.usage.animation": "用法:/animation [on|off]",
25398
25484
  "notice.usage.language": "用法:/language [en|zh]",
25399
25485
  "notice.usage.rainbow": "用法:/rainbow [seed]",
25486
+ "notice.usage.bareCommand": "用法:/{name}",
25487
+ "notice.switchInProgress": "正在切换会话 —— 等新会话就绪后再发送,或用 /resume cancel 取消",
25400
25488
  "notice.imageCancelled": "图片提交已取消",
25401
25489
  "notice.themeConfigUnreadable": "主题配置无法读取,使用暗色:{message}",
25402
25490
  "notice.languageConfigUnreadable": "语言配置无法读取,使用英文:{message}",
@@ -25427,6 +25515,13 @@ const zh = {
25427
25515
  "notice.resumeFailed": "恢复失败:{message}",
25428
25516
  "notice.forkFailed": "分叉失败:{message}",
25429
25517
  "notice.sessionSwitchFailed": "会话切换失败:{message}",
25518
+ "notice.sessionCreated": "已创建 {id} · 模式 {mode}",
25519
+ "notice.sessionResumed": "已恢复 {id} · 模式 {mode}",
25520
+ "notice.sessionSwitchedDirty": "已切换到 {id},但 {detail}",
25521
+ "notice.flushFailed": "上一会话刷新失败:{message}",
25522
+ "notice.agentReleaseFailed": "上一代理释放失败:{message}",
25523
+ "notice.copyEmpty": "还没有可复制的回复",
25524
+ "notice.copied": "已复制最近一条回复",
25430
25525
  "notice.switchQueued": "当前回合结束后将切换到 {label} · 可用 /resume cancel 取消",
25431
25526
  "notice.reviewStarted": "审查已在只读权限下开始",
25432
25527
  "notice.reviewFailed": "审查失败:{message}",
@@ -25478,7 +25573,8 @@ const zh = {
25478
25573
  "panel.footer.chooseSelect": "↑↓ 移动 · 回车 选择 · r 刷新 · esc 关闭",
25479
25574
  "panel.footer.inspectDetails": "↑↓ 查看 · 回车 详情 · r 刷新 · esc 关闭",
25480
25575
  "panel.footer.inspectRefresh": "↑↓ 查看 · r 刷新 · esc 关闭",
25481
- "panel.footer.resume": "tab/←→ 筛选 · ↑↓/pg 翻页 · ctrl+f 搜索 · e 详情 · t 转录 · d 删除 · 回车恢复",
25576
+ "panel.footer.resume": "tab/←→ 筛选 · ↑↓/pg 翻页 · ctrl+f 搜索 · e 详情 · t 转录 · 回车恢复",
25577
+ "panel.footer.delete": "tab/←→ 筛选 · ↑↓/pg 翻页 · ctrl+f 搜索 · e 详情 · t 转录 · 回车删除 · esc 关闭",
25482
25578
  "panel.footer.transcript": "行 {from}-{to}/{total} · ↑↓/pg/g/G · t/esc 关闭",
25483
25579
  "panel.footer.history": "↑↓ 移动 · g/G 首尾 · 回车填入 · esc 关闭",
25484
25580
  "panel.footer.review": "↑↓ 移动 · 回车选择 · esc 返回 · q 关闭",
@@ -25490,6 +25586,7 @@ const zh = {
25490
25586
  "panel.jobs.title": "/jobs · 后台任务 · {count}",
25491
25587
  "panel.resume.deleteTitle": "永久删除 {target}?此操作无法撤销,子代理线程也会删除",
25492
25588
  "panel.resume.title": "/resume{mode}{search} · {toolbar}",
25589
+ "panel.delete.title": "/delete{search} · {toolbar}",
25493
25590
  "panel.noActiveReminders": "暂无活动提醒——模型通过 schedule_create 创建",
25494
25591
  "panel.notMounted": "未加载",
25495
25592
  "panel.broken": "异常:{message}",
@@ -25771,6 +25868,64 @@ function LanguagePanel({ current, select, close }) {
25771
25868
  function clipUpdateLines(lines) {
25772
25869
  return lines.length <= 800 ? lines : lines.slice(lines.length - 800);
25773
25870
  }
25871
+ let updateApplyJob;
25872
+ const updateApplyRunningListeners = /* @__PURE__ */ new Set();
25873
+ function emitUpdateApplyRunning(running) {
25874
+ for (const listener of updateApplyRunningListeners) listener(running);
25875
+ }
25876
+ /** True while an apply child is alive, even if the /update panel is closed. */
25877
+ function isUpdateApplyRunning() {
25878
+ return updateApplyJob !== void 0;
25879
+ }
25880
+ /** Subscribe to apply-running changes (frozen-hint "esc waits"). */
25881
+ function subscribeUpdateApplyRunning(listener) {
25882
+ updateApplyRunningListeners.add(listener);
25883
+ listener(updateApplyJob !== void 0);
25884
+ return () => {
25885
+ updateApplyRunningListeners.delete(listener);
25886
+ };
25887
+ }
25888
+ /** The live apply job, if any: lines already streamed plus the shared promise. */
25889
+ function currentUpdateApply() {
25890
+ return updateApplyJob;
25891
+ }
25892
+ /**
25893
+ * Run `apply` once. A second call while the child is alive reuses the same
25894
+ * promise and replays buffered lines — closing /update and opening it again
25895
+ * must not spawn a second `npm install`.
25896
+ */
25897
+ function runUpdateApply(apply, plan, onLine) {
25898
+ if (updateApplyJob !== void 0) {
25899
+ if (onLine !== void 0) {
25900
+ for (const line of updateApplyJob.lines) onLine(line);
25901
+ updateApplyJob.lineListeners.add(onLine);
25902
+ }
25903
+ return updateApplyJob.promise;
25904
+ }
25905
+ const lines = [];
25906
+ const lineListeners = /* @__PURE__ */ new Set();
25907
+ if (onLine !== void 0) lineListeners.add(onLine);
25908
+ const promise = apply((line) => {
25909
+ const text = singleLineText(line);
25910
+ lines.push(text);
25911
+ if (lines.length > 800) lines.splice(0, lines.length - 800);
25912
+ for (const listener of lineListeners) listener(text);
25913
+ }, plan);
25914
+ updateApplyJob = {
25915
+ promise,
25916
+ lines,
25917
+ lineListeners
25918
+ };
25919
+ emitUpdateApplyRunning(true);
25920
+ const settle = () => {
25921
+ if (updateApplyJob?.promise === promise) {
25922
+ updateApplyJob = void 0;
25923
+ emitUpdateApplyRunning(false);
25924
+ }
25925
+ };
25926
+ promise.then(settle, settle);
25927
+ return promise;
25928
+ }
25774
25929
  /** Rendered facts of one probe: current/target versions, actions, refusals. */
25775
25930
  function updatePlanView(status) {
25776
25931
  const rows = [];
@@ -25870,8 +26025,33 @@ function UpdatePanel({ probe, apply, close, notify }) {
25870
26025
  const [applyError, setApplyError] = (0, import_react.useState)();
25871
26026
  const [anchor, setAnchor] = (0, import_react.useState)("tail");
25872
26027
  const [epoch, setEpoch] = (0, import_react.useState)(0);
26028
+ const appendLine = (line) => {
26029
+ setLines((previous) => clipUpdateLines([...previous, line]));
26030
+ };
25873
26031
  (0, import_react.useEffect)(() => {
25874
26032
  let disposed = false;
26033
+ const existing = currentUpdateApply();
26034
+ if (existing !== void 0) {
26035
+ setPhase("apply");
26036
+ setLines(existing.lines);
26037
+ setExit(void 0);
26038
+ setApplyError(void 0);
26039
+ setAnchor("tail");
26040
+ existing.lineListeners.add(appendLine);
26041
+ existing.promise.then((code) => {
26042
+ if (disposed) return;
26043
+ setExit(code);
26044
+ setPhase("done");
26045
+ }, (reason) => {
26046
+ if (disposed) return;
26047
+ setApplyError(reason instanceof Error ? reason.message : String(reason));
26048
+ setPhase("done");
26049
+ });
26050
+ return () => {
26051
+ disposed = true;
26052
+ existing.lineListeners.delete(appendLine);
26053
+ };
26054
+ }
25875
26055
  setPhase("probe");
25876
26056
  setStatus(void 0);
25877
26057
  setProbeError(void 0);
@@ -25887,6 +26067,7 @@ function UpdatePanel({ probe, apply, close, notify }) {
25887
26067
  });
25888
26068
  return () => {
25889
26069
  disposed = true;
26070
+ currentUpdateApply()?.lineListeners.delete(appendLine);
25890
26071
  };
25891
26072
  }, [epoch, probe]);
25892
26073
  const stdout = useStdout().stdout;
@@ -25894,14 +26075,13 @@ function UpdatePanel({ probe, apply, close, notify }) {
25894
26075
  const start = () => {
25895
26076
  if (phase !== "plan" || status === void 0) return;
25896
26077
  if (!updatePlanView(status).runnable) return;
26078
+ if (isUpdateApplyRunning()) return;
25897
26079
  setPhase("apply");
25898
26080
  setLines([]);
25899
26081
  setExit(void 0);
25900
26082
  setApplyError(void 0);
25901
26083
  setAnchor("tail");
25902
- apply((line) => {
25903
- setLines((previous) => clipUpdateLines([...previous, singleLineText(line)]));
25904
- }, status.plan).then((code) => {
26084
+ runUpdateApply(apply, status.plan, appendLine).then((code) => {
25905
26085
  setExit(code);
25906
26086
  setPhase("done");
25907
26087
  notify(code === 0 ? "update installed — restart dsh to activate" : `update failed (exit ${code})`, code === 0 ? "info" : "error");
@@ -26023,6 +26203,10 @@ function readPackageVersion(manifest = new URL("../package.json", import.meta.ur
26023
26203
  }
26024
26204
  /** Version of the installed dsh-code package. */
26025
26205
  const DSH_CODE_VERSION = readPackageVersion();
26206
+ /** Header brand line: hide a missing-manifest fallback so we never paint v0.0.0. */
26207
+ function headerBrandTitle(version = DSH_CODE_VERSION) {
26208
+ return version === "0.0.0" ? "DeepSeek Harness" : `DeepSeek Harness · v${version}`;
26209
+ }
26026
26210
  /** The harness host package: the dsh CLI whose process runs the TUI plugin. */
26027
26211
  const DSH_HOST_PACKAGE_NAME = "@deepseek-ai/dsh";
26028
26212
  /** Parent levels above the host entry allowed to hold its package manifest. */
@@ -29660,6 +29844,35 @@ function createMentions(ctx, agent, cwd) {
29660
29844
  //#endregion
29661
29845
  //#region src/session-directory.ts
29662
29846
  /** Lightweight session-directory projection for the /resume picker. */
29847
+ /**
29848
+ * Acquire every subtree member's cross-process write lease before deleting
29849
+ * any artifact. A partial acquisition is rolled back, so callers either hold
29850
+ * the whole deletion boundary or touch nothing.
29851
+ */
29852
+ async function acquireSessionDeletionLeases(persistence, ids) {
29853
+ const leases = [];
29854
+ try {
29855
+ for (const id of ids) leases.push(await persistence.open(SessionId(id), "write"));
29856
+ return leases;
29857
+ } catch (error) {
29858
+ try {
29859
+ await releaseSessionDeletionLeases(leases);
29860
+ } catch (releaseError) {
29861
+ throw new AggregateError([error, releaseError], "session deletion lease acquisition and rollback failed");
29862
+ }
29863
+ throw error;
29864
+ }
29865
+ }
29866
+ /** Release deletion leases in reverse acquisition order. */
29867
+ async function releaseSessionDeletionLeases(leases) {
29868
+ const failures = [];
29869
+ for (const lease of [...leases].reverse()) try {
29870
+ await lease.close();
29871
+ } catch (error) {
29872
+ failures.push(error);
29873
+ }
29874
+ if (failures.length > 0) throw new AggregateError(failures, "failed to release session deletion leases");
29875
+ }
29663
29876
  /** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
29664
29877
  const CASE_INSENSITIVE_FS = process.platform === "win32" || process.platform === "darwin";
29665
29878
  /** True only for delegated subagents; ordinary forks also carry lineage. */
@@ -29695,6 +29908,23 @@ function matchSessionId(headers, wanted) {
29695
29908
  if (matches.length > 1) throw new Error(`session prefix "${wanted}" is ambiguous (${matches.length} matches): use more of the id`);
29696
29909
  return matches[0];
29697
29910
  }
29911
+ /**
29912
+ * Unique picker-row match by exact id, unique prefix, or unique suffix.
29913
+ * The resume list shows `id.slice(-12)`, so `/delete` arguments are often
29914
+ * that tail rather than a leading prefix.
29915
+ */
29916
+ function matchSessionRow(rows, wanted) {
29917
+ const needle = wanted.trim();
29918
+ if (needle === "") throw new Error("no persisted session matches \"\"");
29919
+ const exact = rows.filter((row) => row.id === needle);
29920
+ if (exact[0] !== void 0 && exact.length === 1) return exact[0];
29921
+ const prefixed = rows.filter((row) => row.id.startsWith(needle));
29922
+ if (prefixed[0] !== void 0 && prefixed.length === 1) return prefixed[0];
29923
+ const suffixed = rows.filter((row) => row.id.endsWith(needle));
29924
+ if (suffixed[0] !== void 0 && suffixed.length === 1) return suffixed[0];
29925
+ if (prefixed.length > 1 || suffixed.length > 1) throw new Error(`session prefix "${needle}" is ambiguous (${Math.max(prefixed.length, suffixed.length)} matches): use more of the id`);
29926
+ throw new Error(`no persisted session matches "${needle}"`);
29927
+ }
29698
29928
  /** The newest persisted ROOT session pinned to this cwd, or undefined. */
29699
29929
  function newestRootForCwd(headers, cwd) {
29700
29930
  return headers.filter((header) => !isSubagentSession(header) && samePath(header.cwd, cwd)).sort((left, right) => right.createdAt - left.createdAt)[0];
@@ -29727,7 +29957,13 @@ function projectSessionRows(records, options, updated) {
29727
29957
  persisted: record.persisted,
29728
29958
  preset: record.header.agentPreset ?? "standard"
29729
29959
  };
29730
- }).filter((row) => needle === "" || `${row.id} ${row.cwd} ${row.workspace} ${row.preset}`.toLowerCase().includes(needle)).sort((left, right) => options.sort === "newest" ? right.updatedAt - left.updatedAt || right.createdAt - left.createdAt : left.updatedAt - right.updatedAt || left.createdAt - right.createdAt);
29960
+ }).filter((row) => sessionRowMatchesQuery(row, needle)).sort((left, right) => options.sort === "newest" ? right.updatedAt - left.updatedAt || right.createdAt - left.createdAt : left.updatedAt - right.updatedAt || left.createdAt - right.createdAt);
29961
+ }
29962
+ /** True when the picker query hits id, path, preset, or the displayed title. */
29963
+ function sessionRowMatchesQuery(row, query) {
29964
+ const needle = query.trim().toLowerCase();
29965
+ if (needle === "") return true;
29966
+ return `${row.id} ${row.cwd} ${row.workspace} ${row.preset} ${row.title ?? ""}`.toLowerCase().includes(needle);
29731
29967
  }
29732
29968
  /** Merge page-local title observations without disturbing directory order. */
29733
29969
  function mergeSessionTitles(rows, observations) {
@@ -29856,7 +30092,10 @@ function jsonlSessionRoot(persistence) {
29856
30092
  */
29857
30093
  function collectDeletionSubtree(records, id) {
29858
30094
  const parentOf = /* @__PURE__ */ new Map();
29859
- for (const record of records) parentOf.set(record.header.id, record.header.parentSession);
30095
+ for (const record of records) {
30096
+ if (!isSubagentSession(record.header)) continue;
30097
+ parentOf.set(record.header.id, record.header.parentSession);
30098
+ }
29860
30099
  const doomed = /* @__PURE__ */ new Set([id]);
29861
30100
  for (let pass = 0; pass < 2; pass += 1) for (const candidate of parentOf.keys()) {
29862
30101
  if (doomed.has(candidate)) continue;
@@ -30391,7 +30630,11 @@ function formatDuration(ms) {
30391
30630
  const s = ms / 1e3;
30392
30631
  if (s < 60) return String(Math.round(s * 10) / 10) + "s";
30393
30632
  const whole = Math.round(s);
30394
- return Math.floor(whole / 60) + "m" + whole % 60 + "s";
30633
+ const hours = Math.floor(whole / 3600);
30634
+ const minutes = Math.floor(whole % 3600 / 60);
30635
+ const seconds = whole % 60;
30636
+ if (hours > 0) return `${hours}h${minutes}m${seconds === 0 ? "" : `${seconds}s`}`;
30637
+ return `${minutes}m${seconds}s`;
30395
30638
  }
30396
30639
  /**
30397
30640
  * Compact decode rate: one decimal under a hundred, whole below a thousand,
@@ -30569,6 +30812,75 @@ const STATUS_ITEMS = [
30569
30812
  side: "left"
30570
30813
  }
30571
30814
  ];
30815
+ const STATUSLINE_ITEM_KEYS = {
30816
+ model: {
30817
+ label: "statusline.item.model",
30818
+ description: "statusline.item.model.desc"
30819
+ },
30820
+ cwd: {
30821
+ label: "statusline.item.cwd",
30822
+ description: "statusline.item.cwd.desc"
30823
+ },
30824
+ mode: {
30825
+ label: "statusline.item.mode",
30826
+ description: "statusline.item.mode.desc"
30827
+ },
30828
+ branch: {
30829
+ label: "statusline.item.branch",
30830
+ description: "statusline.item.branch.desc"
30831
+ },
30832
+ context: {
30833
+ label: "statusline.item.context",
30834
+ description: "statusline.item.context.desc"
30835
+ },
30836
+ permission: {
30837
+ label: "statusline.item.permission",
30838
+ description: "statusline.item.permission.desc"
30839
+ },
30840
+ plan: {
30841
+ label: "statusline.item.plan",
30842
+ description: "statusline.item.plan.desc"
30843
+ },
30844
+ turns: {
30845
+ label: "statusline.item.turns",
30846
+ description: "statusline.item.turns.desc"
30847
+ },
30848
+ durations: {
30849
+ label: "statusline.item.durations",
30850
+ description: "statusline.item.durations.desc"
30851
+ },
30852
+ cache: {
30853
+ label: "statusline.item.cache",
30854
+ description: "statusline.item.cache.desc"
30855
+ },
30856
+ tokens: {
30857
+ label: "statusline.item.tokens",
30858
+ description: "statusline.item.tokens.desc"
30859
+ },
30860
+ title: {
30861
+ label: "statusline.item.title",
30862
+ description: "statusline.item.title.desc"
30863
+ },
30864
+ goal: {
30865
+ label: "statusline.item.goal",
30866
+ description: "statusline.item.goal.desc"
30867
+ },
30868
+ sandbox: {
30869
+ label: "statusline.item.sandbox",
30870
+ description: "statusline.item.sandbox.desc"
30871
+ }
30872
+ };
30873
+ /** Picker rows with labels/descriptions in the active interface language. */
30874
+ function localizedStatusItems() {
30875
+ return STATUS_ITEMS.map((item) => {
30876
+ const keys = STATUSLINE_ITEM_KEYS[item.id];
30877
+ return {
30878
+ ...item,
30879
+ label: t(keys.label),
30880
+ description: t(keys.description)
30881
+ };
30882
+ });
30883
+ }
30572
30884
  /**
30573
30885
  * Default order: the whole catalog (matches the pre-customization bar).
30574
30886
  * The busy dot is not an item — it always leads the identity cluster.
@@ -30597,9 +30909,10 @@ const WIDTH_SAFETY = 1;
30597
30909
  /** Column budget for the session title before it ellipsizes. */
30598
30910
  const TITLE_BUDGET = 48;
30599
30911
  /**
30600
- * Primary-row drop ranks: context drops before permission; the identity
30601
- * cluster never drops and ellipsizes only after the right badge is gone.
30602
- * Secondary-row groups reuse the remaining ranks independently.
30912
+ * Primary-row drop ranks: identity never drops; permission outranks context.
30913
+ * The drop ladder peels hint and trailing identity facts before removing
30914
+ * the context meter, so occupancy stays visible on a typical 120-column
30915
+ * terminal. Secondary-row groups reuse the remaining ranks independently.
30603
30916
  */
30604
30917
  const RANK_TITLE = 10;
30605
30918
  const RANK_TOKENS = 50;
@@ -30637,6 +30950,23 @@ function sep() {
30637
30950
  tone: "label"
30638
30951
  };
30639
30952
  }
30953
+ /** True for the dim ` · ` that joins identity facts. */
30954
+ function isIdentitySep(span) {
30955
+ return span.text === " · " && span.tone === "label";
30956
+ }
30957
+ /**
30958
+ * Drop the trailing identity fact (branch, then mode, then cwd) so the
30959
+ * context meter can keep a column. Returns undefined once only the busy
30960
+ * dot and model remain.
30961
+ */
30962
+ function peelIdentityFact(spans) {
30963
+ if (spans.length < 3) return void 0;
30964
+ const last = spans[spans.length - 1];
30965
+ const before = spans[spans.length - 2];
30966
+ const modeSep = spans[spans.length - 3];
30967
+ if (last?.tone === "accent" && modeSep !== void 0 && isIdentitySep(modeSep) && before?.tone === "label") return spans.slice(0, -3);
30968
+ if ((last?.tone === "branch" || last?.tone === "path") && before !== void 0 && isIdentitySep(before)) return spans.slice(0, -2);
30969
+ }
30640
30970
  /** Total visible columns of a span list (separators ride inside the spans). */
30641
30971
  function spansWidth(spans) {
30642
30972
  let width = 0;
@@ -30805,7 +31135,10 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
30805
31135
  });
30806
31136
  if (facts.goal !== void 0 && enabled.has("goal")) row2.push({
30807
31137
  group: { spans: [{
30808
- text: facts.goal.phase === "active" ? "◎ round " + facts.goal.rounds + "/" + facts.goal.max : "◎ " + safe(facts.goal.phase),
31138
+ text: facts.goal.phase === "active" ? t("status.goal.round", {
31139
+ current: facts.goal.rounds,
31140
+ max: facts.goal.max
31141
+ }) : t("status.goal.phase", { phase: safe(facts.goal.phase) }),
30809
31142
  tone: "accent"
30810
31143
  }] },
30811
31144
  rank: RANK_GOAL,
@@ -30814,7 +31147,7 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
30814
31147
  const sandbox = safe(facts.sandbox ?? "");
30815
31148
  if (sandbox !== "" && sandbox.toLowerCase() !== facts.permission.toLowerCase() && enabled.has("sandbox")) row2.push({
30816
31149
  group: { spans: [{
30817
- text: "sandbox " + sandbox,
31150
+ text: t("status.label.sandbox") + " " + sandbox,
30818
31151
  tone: "warn"
30819
31152
  }] },
30820
31153
  rank: RANK_SANDBOX,
@@ -30826,7 +31159,7 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
30826
31159
  if (permission !== "" && enabled.has("permission")) {
30827
31160
  right.push({
30828
31161
  span: planStation ? {
30829
- text: "plan on",
31162
+ text: t("status.plan.on"),
30830
31163
  tone: "plan"
30831
31164
  } : {
30832
31165
  text: permission,
@@ -30839,7 +31172,7 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
30839
31172
  }
30840
31173
  if (facts.plan && enabled.has("plan")) row2.push({
30841
31174
  group: { spans: [{
30842
- text: "plan",
31175
+ text: t("status.plan.mark"),
30843
31176
  tone: "accent"
30844
31177
  }] },
30845
31178
  rank: RANK2_PLAN,
@@ -30855,7 +31188,8 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
30855
31188
  /**
30856
31189
  * Compose the two-row footer layout under a column budget. Row 1 keeps model,
30857
31190
  * cwd, mode, branch, context, then the right-pinned permission badge and cycle
30858
- * hint. It drops hint, context, and permission before ellipsizing identity.
31191
+ * hint. It shrinks the context bar, drops the hint, and peels trailing
31192
+ * identity facts before removing the context group or the permission badge.
30859
31193
  * Row 2 fits all secondary figures and state within its own budget.
30860
31194
  * @param facts - identity facts resolved by the runner.
30861
31195
  * @param stats - session figures folded from the durable log.
@@ -30912,26 +31246,24 @@ function layoutStatusBar(facts, stats, columns, options = {}) {
30912
31246
  rebuildContext();
30913
31247
  continue;
30914
31248
  }
30915
- leftKept.splice(leftKept.findIndex((entry) => entry.id === "context"), 1);
31249
+ }
31250
+ if (hint) {
31251
+ hint = false;
30916
31252
  continue;
30917
31253
  }
30918
- if (hint && rightKept.length > 0 && leftKept.length > 0) {
30919
- const identity = leftKept[0];
30920
- const identityText = identity.group.spans.map((span) => span.text).join("");
30921
- const identityBudget = budget - joinWidth(rightKept.map((entry) => visibleColumns(entry.span.text)), itemSeparator) - LEFT_RIGHT_GAP - visibleColumns(statusCycleHint());
30922
- if (identityBudget > 0 && visibleColumns(identityText) > identityBudget) {
31254
+ const identityEntry = leftKept[0];
31255
+ if (identityEntry?.id === "identity") {
31256
+ const peeled = peelIdentityFact(identityEntry.group.spans);
31257
+ if (peeled !== void 0) {
30923
31258
  leftKept[0] = {
30924
- ...identity,
30925
- group: { spans: [{
30926
- text: truncateColumns(identityText, identityBudget),
30927
- tone: "model"
30928
- }] }
31259
+ ...identityEntry,
31260
+ group: { spans: peeled }
30929
31261
  };
30930
31262
  continue;
30931
31263
  }
30932
31264
  }
30933
- if (hint) {
30934
- hint = false;
31265
+ if (leftKept.some((entry) => entry.id === "context")) {
31266
+ leftKept.splice(leftKept.findIndex((entry) => entry.id === "context"), 1);
30935
31267
  continue;
30936
31268
  }
30937
31269
  let dropLeft = -1;
@@ -32092,7 +32424,7 @@ function ModePanel({ current, load, select, close }) {
32092
32424
  const [cursor, setCursor] = (0, import_react.useState)(0);
32093
32425
  const [loading, setLoading] = (0, import_react.useState)(true);
32094
32426
  const [error, setError] = (0, import_react.useState)();
32095
- const refresh = () => {
32427
+ const refresh = (0, import_react.useCallback)(() => {
32096
32428
  setLoading(true);
32097
32429
  setError(void 0);
32098
32430
  Promise.resolve().then(load).then((value) => {
@@ -32102,8 +32434,8 @@ function ModePanel({ current, load, select, close }) {
32102
32434
  setError(reason instanceof Error ? reason.message : String(reason));
32103
32435
  setLoading(false);
32104
32436
  });
32105
- };
32106
- (0, import_react.useEffect)(refresh, []);
32437
+ }, [load]);
32438
+ (0, import_react.useEffect)(refresh, [refresh]);
32107
32439
  const visible = (0, import_react.useMemo)(() => rows.filter((row) => `${row.id} ${row.name ?? ""} ${row.description ?? ""}`.toLowerCase().includes(query.toLowerCase())), [rows, query]);
32108
32440
  (0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, visible.length - 1))), [visible.length]);
32109
32441
  useInput((input, key) => {
@@ -32139,7 +32471,7 @@ function PermissionPanel({ current, load, select, close }) {
32139
32471
  const [cursor, setCursor] = (0, import_react.useState)(0);
32140
32472
  const [loading, setLoading] = (0, import_react.useState)(true);
32141
32473
  const [error, setError] = (0, import_react.useState)();
32142
- const refresh = () => {
32474
+ const refresh = (0, import_react.useCallback)(() => {
32143
32475
  setLoading(true);
32144
32476
  setError(void 0);
32145
32477
  Promise.resolve().then(load).then((value) => {
@@ -32149,8 +32481,8 @@ function PermissionPanel({ current, load, select, close }) {
32149
32481
  setError(reason instanceof Error ? reason.message : String(reason));
32150
32482
  setLoading(false);
32151
32483
  });
32152
- };
32153
- (0, import_react.useEffect)(refresh, []);
32484
+ }, [load]);
32485
+ (0, import_react.useEffect)(refresh, [refresh]);
32154
32486
  const visible = (0, import_react.useMemo)(() => rows.filter((row) => `${row.id} ${row.description ?? ""}`.toLowerCase().includes(query.toLowerCase())), [rows, query]);
32155
32487
  (0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, visible.length - 1))), [visible.length]);
32156
32488
  useInput((input, key) => {
@@ -32184,7 +32516,11 @@ function PluginPanel({ load, close, initialQuery = "" }) {
32184
32516
  const [query, setQuery] = (0, import_react.useState)(initialQuery);
32185
32517
  const [cursor, setCursor] = (0, import_react.useState)(0);
32186
32518
  const [expanded, setExpanded] = (0, import_react.useState)(false);
32187
- const rows = (0, import_react.useMemo)(() => load().filter((row) => `${row.entryId} ${row.moduleName} ${row.phase ?? ""}`.toLowerCase().includes(query.toLowerCase())), [epoch, query]);
32519
+ const rows = (0, import_react.useMemo)(() => load().filter((row) => `${row.entryId} ${row.moduleName} ${row.phase ?? ""}`.toLowerCase().includes(query.toLowerCase())), [
32520
+ epoch,
32521
+ load,
32522
+ query
32523
+ ]);
32188
32524
  (0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, rows.length - 1))), [rows.length]);
32189
32525
  useInput((input, key) => {
32190
32526
  if (key.escape) return close();
@@ -32263,10 +32599,10 @@ function JobsPanel({ load, close }) {
32263
32599
  footer: t("panel.footer.inspectRefresh")
32264
32600
  });
32265
32601
  }
32266
- function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken = 0, deleteMode = false, close }) {
32602
+ function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken = 0, deleteMode = false, presetId, close }) {
32267
32603
  const [options, setOptions] = (0, import_react.useState)({
32268
- sessions: "roots",
32269
- cwd: "current",
32604
+ sessions: presetId === void 0 || presetId === "" ? "roots" : "all",
32605
+ cwd: presetId === void 0 || presetId === "" ? "current" : "all",
32270
32606
  sort: "newest",
32271
32607
  currentCwd,
32272
32608
  query: ""
@@ -32301,8 +32637,30 @@ function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete,
32301
32637
  }
32302
32638
  });
32303
32639
  return () => controller.abort();
32304
- }, [options, reloadToken]);
32640
+ }, [
32641
+ load,
32642
+ options,
32643
+ reloadToken
32644
+ ]);
32305
32645
  (0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, rows.length - 1))), [rows.length]);
32646
+ const presetArmed = (0, import_react.useRef)(false);
32647
+ (0, import_react.useEffect)(() => {
32648
+ if (presetArmed.current || presetId === void 0 || presetId === "" || rows.length === 0 || requestDelete === void 0) return;
32649
+ try {
32650
+ const row = matchSessionRow(rows, presetId);
32651
+ presetArmed.current = true;
32652
+ const index = rows.findIndex((candidate) => candidate.id === row.id);
32653
+ if (index >= 0) setCursor(index);
32654
+ requestDelete(row);
32655
+ } catch (reason) {
32656
+ presetArmed.current = true;
32657
+ setError(reason instanceof Error ? reason.message : String(reason));
32658
+ }
32659
+ }, [
32660
+ rows,
32661
+ presetId,
32662
+ requestDelete
32663
+ ]);
32306
32664
  const cycle = () => {
32307
32665
  if (focus === 3) {
32308
32666
  setDensity((current) => current === "comfortable" ? "dense" : "comfortable");
@@ -32361,7 +32719,6 @@ function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete,
32361
32719
  if (key.pageDown) return setCursor((value) => Math.min(rows.length - 1, value + 8));
32362
32720
  if (input === "g") return setCursor(0);
32363
32721
  if (input === "G") return setCursor(Math.max(0, rows.length - 1));
32364
- if (input === "d" && rows[cursor] !== void 0 && requestDelete !== void 0) return requestDelete(rows[cursor]);
32365
32722
  if (input === "e" && rows[cursor] !== void 0) return setExpanded((value) => value === rows[cursor].id ? void 0 : rows[cursor].id);
32366
32723
  if (input === "t" && rows[cursor] !== void 0) {
32367
32724
  const row = rows[cursor];
@@ -32382,7 +32739,13 @@ function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete,
32382
32739
  });
32383
32740
  return;
32384
32741
  }
32385
- if (key.return && rows[cursor]?.resumable === true) return select(rows[cursor]);
32742
+ if (key.return && rows[cursor] !== void 0) {
32743
+ if (deleteMode) {
32744
+ if (requestDelete !== void 0 && !rows[cursor].live) requestDelete(rows[cursor]);
32745
+ return;
32746
+ }
32747
+ if (rows[cursor].resumable) select(rows[cursor]);
32748
+ }
32386
32749
  }, { isActive: transcript === void 0 });
32387
32750
  if (transcript !== void 0) return (0, import_react.createElement)(DocumentPanel, {
32388
32751
  title: t("panel.document.title", { id: transcript.id }),
@@ -32396,10 +32759,17 @@ function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete,
32396
32759
  const pendingRow = deleteConfirmId === void 0 ? void 0 : rows.find((row) => row.id === deleteConfirmId);
32397
32760
  const toolbar = `[${focus === 0 ? ">" : ""}${options.sessions}] [${focus === 1 ? ">" : ""}${options.cwd} cwd] [${focus === 2 ? ">" : ""}${options.sort}] [${focus === 3 ? ">" : ""}${density}]`;
32398
32761
  return (0, import_react.createElement)(ListFrame, {
32399
- title: deleteConfirmId === void 0 ? `/resume${deleteMode ? " — delete mode" : ""}${searching ? " — searching" : ""} · ${toolbar}` : `permanently delete ${pendingRow === void 0 ? deleteConfirmId.slice(-12) : pendingRow.title ?? pendingRow.id}? this cannot be undone · subagent threads go too`,
32762
+ title: deleteConfirmId !== void 0 ? t("panel.resume.deleteTitle", { target: pendingRow === void 0 ? deleteConfirmId.slice(-12) : pendingRow.title ?? pendingRow.id }) : deleteMode ? t("panel.delete.title", {
32763
+ search: searching ? ` — ${t("panel.searching")}` : "",
32764
+ toolbar
32765
+ }) : t("panel.resume.title", {
32766
+ mode: "",
32767
+ search: searching ? ` — ${t("panel.searching")}` : "",
32768
+ toolbar
32769
+ }),
32400
32770
  rows: rows.map((row) => ({
32401
32771
  key: row.id,
32402
- disabled: !row.resumable,
32772
+ disabled: deleteMode ? row.live : !row.resumable,
32403
32773
  text: `${row.subagent ? "↳" : "○"} ${row.title ?? row.id.slice(-12)}${density === "comfortable" ? ` · ${formatRelativeTime(row.updatedAt ?? row.createdAt, now)} · ${row.workspace} · ${row.preset}` : ""}${row.live ? " · live" : ""}${expanded === row.id ? ` · ${row.id} · ${row.cwd}${row.parent === void 0 ? "" : ` · parent ${row.parent}`}` : ""}`
32404
32774
  })),
32405
32775
  cursor,
@@ -32407,7 +32777,7 @@ function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete,
32407
32777
  error,
32408
32778
  query: options.query,
32409
32779
  searching,
32410
- footer: t("panel.footer.resume")
32780
+ footer: t(deleteMode ? "panel.footer.delete" : "panel.footer.resume")
32411
32781
  });
32412
32782
  }
32413
32783
  function documentStyleProps(style) {
@@ -32886,7 +33256,7 @@ function StatuslinePanel({ enabled, change, close }) {
32886
33256
  const bodyRows = Math.max(1, viewport.bodyRows - 1);
32887
33257
  const offset = revealRow(0, cursor, order.length, bodyRows);
32888
33258
  const visible = order.slice(offset, offset + bodyRows);
32889
- const meta = new Map(STATUS_ITEMS.map((item) => [item.id, item]));
33259
+ const meta = new Map(localizedStatusItems().map((item) => [item.id, item]));
32890
33260
  const accent = panelAccent("statusline", getPalette().dim, getPalette().brandBright);
32891
33261
  return (0, import_react.createElement)(Box, {
32892
33262
  width: viewport.outerColumns,
@@ -33002,7 +33372,7 @@ function AgentsPanel({ live, load, readTranscript, close }) {
33002
33372
  const [transcript, setTranscript] = (0, import_react.useState)();
33003
33373
  const transcriptLoad = (0, import_react.useRef)();
33004
33374
  (0, import_react.useEffect)(() => () => transcriptLoad.current?.abort(), []);
33005
- const refresh = () => {
33375
+ const refresh = (0, import_react.useCallback)(() => {
33006
33376
  setLoading(true);
33007
33377
  setError(void 0);
33008
33378
  Promise.resolve().then(load).then((value) => {
@@ -33012,8 +33382,8 @@ function AgentsPanel({ live, load, readTranscript, close }) {
33012
33382
  setError(reason instanceof Error ? reason.message : String(reason));
33013
33383
  setLoading(false);
33014
33384
  });
33015
- };
33016
- (0, import_react.useEffect)(refresh, []);
33385
+ }, [load]);
33386
+ (0, import_react.useEffect)(refresh, [refresh]);
33017
33387
  const rows = (0, import_react.useMemo)(() => {
33018
33388
  const seen = new Set(live.map((row) => row.id));
33019
33389
  const feedRows = live.map((row) => ({
@@ -33104,7 +33474,7 @@ function SubagentPanel({ current, load, pick, inherit, close }) {
33104
33474
  const [loading, setLoading] = (0, import_react.useState)(true);
33105
33475
  const [cursor, setCursor] = (0, import_react.useState)(0);
33106
33476
  const [effortFor, setEffortFor] = (0, import_react.useState)(void 0);
33107
- const refresh = () => {
33477
+ const refresh = (0, import_react.useCallback)(() => {
33108
33478
  setLoading(true);
33109
33479
  setError(void 0);
33110
33480
  Promise.resolve().then(load).then((value) => {
@@ -33114,8 +33484,8 @@ function SubagentPanel({ current, load, pick, inherit, close }) {
33114
33484
  setError(reason instanceof Error ? reason.message : String(reason));
33115
33485
  setLoading(false);
33116
33486
  });
33117
- };
33118
- (0, import_react.useEffect)(refresh, []);
33487
+ }, [load]);
33488
+ (0, import_react.useEffect)(refresh, [refresh]);
33119
33489
  const rows = (0, import_react.useMemo)(() => directory?.rows ?? [], [directory]);
33120
33490
  (0, import_react.useEffect)(() => {
33121
33491
  if (current === "" || rows.length === 0) return;
@@ -33373,6 +33743,19 @@ function beginRecall(entries, draft) {
33373
33743
  };
33374
33744
  }
33375
33745
  /**
33746
+ * Join a panel-recalled entry onto the draft already in the composer: the
33747
+ * draft is extended, never replaced, so picking a history row cannot discard
33748
+ * work in progress. An empty draft takes the entry as-is; otherwise the entry
33749
+ * starts on its own line unless the draft already ends one.
33750
+ * @param draft - the composer text before the recall.
33751
+ * @param entry - the sanitized text of the accepted row.
33752
+ * @returns the text to place in the composer.
33753
+ */
33754
+ function appendRecall(draft, entry) {
33755
+ if (draft === "") return entry;
33756
+ return draft.endsWith("\n") ? draft + entry : `${draft}\n${entry}`;
33757
+ }
33758
+ /**
33376
33759
  * Move one entry older (Up, toward index +1 in the newest-first space). The
33377
33760
  * first Up saves the current draft so Down past the newest can restore it
33378
33761
  * (Claude-Code shell recall — the draft is never lost); the oldest entry
@@ -33814,14 +34197,17 @@ function ProviderAuthorizationPanel(props) {
33814
34197
  props.cancel(props.row.key);
33815
34198
  decline();
33816
34199
  };
34200
+ const propsRef = (0, import_react.useRef)(props);
34201
+ propsRef.current = props;
34202
+ const rowKey = props.row.key;
33817
34203
  (0, import_react.useEffect)(() => () => {
33818
34204
  controllerRef.current?.abort();
33819
- props.cancel(props.row.key);
34205
+ propsRef.current.cancel(rowKey);
33820
34206
  const reply = replyRef.current;
33821
34207
  replyRef.current = void 0;
33822
34208
  reply?.detach();
33823
34209
  reply?.reject(new AuthorizationDeclinedError());
33824
- }, [props.row.key]);
34210
+ }, [rowKey]);
33825
34211
  const start = (method) => {
33826
34212
  setPhase("running");
33827
34213
  setError(void 0);
@@ -34086,17 +34472,37 @@ function detectImageMediaType(data) {
34086
34472
  function looksLikeImagePath(path) {
34087
34473
  return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
34088
34474
  }
34475
+ /** Strip one layer of ASCII or Unicode quotes and shell-escaped spaces. */
34476
+ function unwrapDroppedPath(token) {
34477
+ const trimmed = token.trim();
34478
+ return (/^[\u2018\u201C"'](.+)[\u2019\u201D"']$/u.exec(trimmed)?.[1] ?? trimmed).replace(/\\ /gu, " ");
34479
+ }
34480
+ /** Absolute/relative drop with a dotted leaf — including unquoted spaces. */
34481
+ function looksLikeFilesystemDrop(path) {
34482
+ if (path.startsWith("file://")) return true;
34483
+ if (!/^(?:\/|[A-Za-z]:[\\/]|\\\\|\.\.?\/)/u.test(path)) return false;
34484
+ return looksLikeImagePath(path) || /\.[A-Za-z0-9]{1,16}$/u.test(path);
34485
+ }
34089
34486
  /**
34090
- * Parse a paste/drop into its image and file paths: image-suffixed tokens
34091
- * stay images, other path-shaped tokens ride as file attachments (0.1.5
34092
- * file blocks), and anything that is neither leaves both empty — the caller
34093
- * then treats the paste as plain text.
34487
+ * A composer draft that is a filesystem path, not a slash command.
34488
+ * `/usage` stays a command; `/Users/foo.png` and `C:\temp\a.png` are drops.
34489
+ */
34490
+ function looksLikePathDraft(value) {
34491
+ const path = unwrapDroppedPath(value);
34492
+ if (looksLikeFilesystemDrop(path)) return true;
34493
+ return /^\/(?:Users|home|tmp|var|etc|opt|mnt|Volumes)\//u.test(path);
34494
+ }
34495
+ /**
34496
+ * Parse a paste or file-drop into image and file paths: image-suffixed
34497
+ * tokens stay images, other path-shaped tokens ride as file attachments
34498
+ * (0.1.5 file blocks), and anything that is neither leaves both empty —
34499
+ * the caller then treats the paste as plain text.
34094
34500
  *
34095
34501
  * File tokens are held to an absolute-path-with-shape bar (drive/backslash
34096
- * or a dot-suffixed leaf after a separator): a dropped terminal path always
34502
+ * or a dotted leaf after a separator): a dropped terminal path always
34097
34503
  * carries one of those, while prose, slash commands, and option flags never
34098
- * do. A POSIX absolute path without any dot-suffixed leaf falls through as
34099
- * text — the @ mention route still attaches such files deliberately.
34504
+ * do. A POSIX absolute path without a dotted leaf falls through as text —
34505
+ * the @ mention route still attaches such files deliberately.
34100
34506
  */
34101
34507
  function parsePastedAttachmentPaths(input) {
34102
34508
  const text = input.trim();
@@ -34107,32 +34513,41 @@ function parsePastedAttachmentPaths(input) {
34107
34513
  const images = [];
34108
34514
  const files = [];
34109
34515
  const looksLikeDroppedFile = (path) => /^[A-Za-z]:[\\/]/u.test(path) || /^\\\\/u.test(path) || /^\/|^\.\.?\//u.test(path) && /\.[A-Za-z0-9]{1,16}$/u.test(path);
34110
- for (const match of text.matchAll(/"([^"]+)"|'([^']+)'|(\S+)/gu)) {
34111
- const token = match[1] ?? match[2] ?? match[3];
34112
- if (token === void 0) continue;
34113
- let path = token;
34114
- if (path.startsWith("file://")) {
34115
- try {
34116
- path = fileURLToPath(path);
34117
- } catch {
34118
- return {
34119
- images: [],
34120
- files: []
34121
- };
34122
- }
34123
- if (looksLikeImagePath(path)) images.push(path);
34124
- else files.push(path);
34125
- continue;
34516
+ const classify = (raw) => {
34517
+ let path = unwrapDroppedPath(raw);
34518
+ if (path.startsWith("file://")) try {
34519
+ path = fileURLToPath(path);
34520
+ } catch {
34521
+ return "reject";
34126
34522
  }
34523
+ else if (/%[0-9A-Fa-f]{2}/u.test(path)) try {
34524
+ path = decodeURIComponent(path);
34525
+ } catch {}
34127
34526
  if (looksLikeImagePath(path)) {
34128
34527
  images.push(path);
34129
- continue;
34528
+ return "image";
34529
+ }
34530
+ if (looksLikeDroppedFile(path) || looksLikeFilesystemDrop(path)) {
34531
+ files.push(path);
34532
+ return "file";
34130
34533
  }
34131
- if (!looksLikeDroppedFile(path)) return {
34534
+ return "reject";
34535
+ };
34536
+ const whole = unwrapDroppedPath(text);
34537
+ if ((text.match(/(?:^|[\s"'])(?:\/|[A-Za-z]:[\\/]|\\\\|\.\.?\/|file:\/\/)/gu) ?? []).length <= 1 && looksLikeFilesystemDrop(whole) && (/\s/u.test(whole) || /^[\u2018\u201C"']/u.test(text))) {
34538
+ classify(whole);
34539
+ return {
34540
+ images,
34541
+ files
34542
+ };
34543
+ }
34544
+ for (const match of text.matchAll(/"([^"]+)"|'([^']+)'|(\S+)/gu)) {
34545
+ const token = match[1] ?? match[2] ?? match[3];
34546
+ if (token === void 0) continue;
34547
+ if (classify(token) === "reject") return {
34132
34548
  images: [],
34133
34549
  files: []
34134
34550
  };
34135
- files.push(path);
34136
34551
  }
34137
34552
  return {
34138
34553
  images,
@@ -34301,6 +34716,12 @@ function readSettledRowCap() {
34301
34716
  }
34302
34717
  /** Reset region/style, clear the visible screen and scrollback, then home. */
34303
34718
  const RESIZE_REFLOW_CLEAR = "\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H";
34719
+ /**
34720
+ * Same as {@link RESIZE_REFLOW_CLEAR} without wiping native scrollback.
34721
+ * History-cap trims remount `<Static>` but must not `\x1b[3J` a user who is
34722
+ * reading earlier messages above the fold.
34723
+ */
34724
+ const TRIM_REFLOW_CLEAR = "\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[H";
34304
34725
  /** Ask terminals supporting DEC synchronized updates to hold the frame. */
34305
34726
  const SYNCHRONIZED_UPDATE_BEGIN = "\x1B[?2026h";
34306
34727
  /** Release the held frame after Ink has replayed the source-backed Static rows. */
@@ -34440,6 +34861,35 @@ const LOCAL_COMMANDS = [
34440
34861
  }
34441
34862
  ];
34442
34863
  const LOCAL_COMMAND_NAMES = new Set(LOCAL_COMMANDS.map((command) => command.label.slice(1)));
34864
+ /**
34865
+ * TUI-local commands that take no input. Extra tokens used to fall through
34866
+ * as a prompt (`/queue clear` reached the model); they now surface usage.
34867
+ */
34868
+ const BARE_LOCAL_COMMANDS = /* @__PURE__ */ new Set([
34869
+ "quit",
34870
+ "help",
34871
+ "clear",
34872
+ "copy",
34873
+ "update",
34874
+ "schedule",
34875
+ "statusline",
34876
+ "theme",
34877
+ "history",
34878
+ "queue",
34879
+ "usage",
34880
+ "agents",
34881
+ "todos",
34882
+ "subagent"
34883
+ ]);
34884
+ /** Split a slash line into the command name and any trailing input. */
34885
+ function slashNameAndArgs(text) {
34886
+ const match = /^\/([a-z][a-z0-9_-]*)(?:$|[\t ](.*))$/u.exec(text);
34887
+ if (match === null || match[1] === void 0) return void 0;
34888
+ return {
34889
+ name: match[1],
34890
+ args: (match[2] ?? "").trim()
34891
+ };
34892
+ }
34443
34893
  /** Pad text with spaces to a visible-column target (menu name column). */
34444
34894
  function padColumns(text, width) {
34445
34895
  const clipped = truncateColumns(singleLineText(text), width);
@@ -34559,12 +35009,20 @@ function DeepDivingLine({ since, animated = true }) {
34559
35009
  * cap counts explicit newlines and terminal wrapping, slicing from the END so
34560
35010
  * the freshest tokens stay visible while a long reply streams; the complete
34561
35011
  * text lands in the flushed scrollback once the turn assembles it.
35012
+ *
35013
+ * Body wrap width for a streaming tail. `rowColumns` is the same width
35014
+ * passed to `transcriptEntryLines` (terminal minus the last-column safety);
35015
+ * the hanging prefix then shrinks the body so streamed text and settled
35016
+ * markdown wrap on the same column.
34562
35017
  */
34563
- function StreamTail({ text, dim, maxRows, prefix = "", continuationPrefix = prefix, children }) {
34564
- const columns = useStdout().stdout?.columns ?? 80;
34565
- const safeRows = Math.max(1, maxRows);
35018
+ function streamTailBodyColumns(rowColumns, prefix, continuationPrefix = prefix) {
35019
+ const width = Math.max(1, Math.floor(rowColumns));
34566
35020
  const prefixColumns = Math.max(visibleColumns(prefix), visibleColumns(continuationPrefix));
34567
- const contentColumns = Math.max(1, columns - 1 - prefixColumns);
35021
+ return Math.max(1, width - prefixColumns);
35022
+ }
35023
+ function StreamTail({ text, dim, maxRows, prefix = "", continuationPrefix = prefix, children, columns }) {
35024
+ const safeRows = Math.max(1, maxRows);
35025
+ const contentColumns = streamTailBodyColumns(columns, prefix, continuationPrefix);
34568
35026
  const initial = displayTail(text, contentColumns, safeRows);
34569
35027
  const tail = initial.truncated && safeRows > 1 ? displayTail(text, contentColumns, safeRows - 1) : initial;
34570
35028
  const rows = tail.text.split("\n");
@@ -34823,9 +35281,9 @@ function Header({ resumed }) {
34823
35281
  const version = dshKernelVersion();
34824
35282
  return version === void 0 ? void 0 : `dsh-v${version}`;
34825
35283
  })();
34826
- const title = `DeepSeek Harness · v${DSH_CODE_VERSION}`;
35284
+ const title = headerBrandTitle();
34827
35285
  const slogan = "Into the Unknown 探索未至之境";
34828
- const hint = resumed ? "resumed · /help · Esc interrupt" : "/help · Esc interrupt · Ctrl+C quit";
35286
+ const hint = resumed ? t("header.hintResumed") : t("header.hint");
34829
35287
  const copyWidths = [
34830
35288
  visibleColumns(title),
34831
35289
  visibleColumns(slogan),
@@ -35276,23 +35734,14 @@ function StatusLine({ facts, stats, busy, columns, items, onRows, animated }) {
35276
35734
  const flowTick = useFrames(125, flowActive);
35277
35735
  const flowMs = flowActive ? flowTick * 125 + (flow?.phaseMs ?? 0) : void 0;
35278
35736
  const language = getLanguage();
35279
- const layout = (0, import_react.useMemo)(() => layoutStatusBar(facts, stats, Math.max(8, columns - 2), {
35280
- busy,
35281
- items,
35282
- contextWidth: Math.max(5, columns - 6)
35283
- }), [
35284
- facts.model,
35285
- facts.mode,
35286
- facts.cwd,
35287
- facts.branch,
35288
- facts.sessionId,
35289
- facts.title,
35290
- facts.sandbox,
35291
- facts.plan,
35292
- facts.permission,
35293
- facts.goal?.phase,
35294
- facts.goal?.rounds,
35295
- facts.goal?.max,
35737
+ const layout = (0, import_react.useMemo)(() => {
35738
+ return layoutStatusBar(facts, stats, Math.max(8, columns - 2), {
35739
+ busy,
35740
+ items,
35741
+ contextWidth: Math.max(5, columns - 6)
35742
+ });
35743
+ }, [
35744
+ facts,
35296
35745
  stats,
35297
35746
  busy,
35298
35747
  columns,
@@ -35534,7 +35983,7 @@ function QuestionBar({ store, snapshot, locked }) {
35534
35983
  setSubmitted(false);
35535
35984
  }, [request]);
35536
35985
  const question = pending?.request.questions[index];
35537
- const options = question?.options ?? [];
35986
+ const options = (0, import_react.useMemo)(() => question?.options ?? [], [question]);
35538
35987
  const isPlan = question?.intent?.kind === "plan-review";
35539
35988
  const isMulti = question?.multiSelect === true;
35540
35989
  const { cursor, selected, mode, custom, scroll, manualScroll, followCustomTail } = drafts[index] ?? initialQuestionDraft(question);
@@ -35844,7 +36293,7 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
35844
36293
  const [cursor, setCursor] = (0, import_react.useState)(0);
35845
36294
  const stdout = useStdout().stdout;
35846
36295
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
35847
- const rows = directory?.rows ?? [];
36296
+ const rows = (0, import_react.useMemo)(() => directory?.rows ?? [], [directory]);
35848
36297
  const filtered = (0, import_react.useMemo)(() => {
35849
36298
  if (query === "") return rows;
35850
36299
  const needle = query.toLowerCase();
@@ -37171,9 +37620,8 @@ function entryKindLabel(entry) {
37171
37620
  * retained entry is converted to physical rows, but only one viewport slice
37172
37621
  * reaches Ink, so even a huge reasoning block cannot grow the dynamic tree.
37173
37622
  */
37174
- function VerbosePanel({ entries, onClose }) {
37175
- const stdout = useStdout().stdout;
37176
- const viewport = inspectorViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
37623
+ function VerbosePanel({ entries, onClose, columns, rows }) {
37624
+ const viewport = inspectorViewport(columns, rows);
37177
37625
  const [cursor, setCursor] = (0, import_react.useState)(() => Math.max(0, entries.length - 1));
37178
37626
  const [scroll, setScroll] = (0, import_react.useState)(0);
37179
37627
  const savedScroll = (0, import_react.useRef)(/* @__PURE__ */ new Map());
@@ -37182,6 +37630,8 @@ function VerbosePanel({ entries, onClose }) {
37182
37630
  const entry = entries[cursor];
37183
37631
  const allLines = (0, import_react.useMemo)(() => entry === void 0 ? [] : transcriptEntryLines(entry, viewport.contentColumns), [entry, viewport.contentColumns]);
37184
37632
  const visibleScroll = clampScroll(scroll, allLines.length, viewport.bodyRows);
37633
+ const visibleScrollRef = (0, import_react.useRef)(visibleScroll);
37634
+ visibleScrollRef.current = visibleScroll;
37185
37635
  (0, import_react.useEffect)(() => {
37186
37636
  cursorRef.current = cursor;
37187
37637
  }, [cursor]);
@@ -37189,7 +37639,7 @@ function VerbosePanel({ entries, onClose }) {
37189
37639
  const current = cursorRef.current;
37190
37640
  const next = followInspectorCursor(current, previousLength.current, entries.length);
37191
37641
  if (next !== current) {
37192
- savedScroll.current.set(current, visibleScroll);
37642
+ savedScroll.current.set(current, visibleScrollRef.current);
37193
37643
  setCursor(next);
37194
37644
  setScroll(savedScroll.current.get(next) ?? 0);
37195
37645
  }
@@ -37282,6 +37732,15 @@ function StaticTranscript({ items }) {
37282
37732
  }
37283
37733
  const MemoStaticTranscript = (0, import_react.memo)(StaticTranscript);
37284
37734
  /**
37735
+ * Wrap one completion-menu cursor step. An empty menu keeps the index at 0:
37736
+ * `% 0` is NaN, and a NaN index silently disables the highlight, the accept
37737
+ * key, and any later Enter that runs through the menu.
37738
+ */
37739
+ function stepCompletionIndex(index, delta, count) {
37740
+ if (count <= 0) return 0;
37741
+ return ((index + delta) % count + count) % count;
37742
+ }
37743
+ /**
37285
37744
  * Resolve completion candidates for the current input: TUI-local commands,
37286
37745
  * the live registry descriptors, and user-invocable skills, filtered by the
37287
37746
  * typed prefix. Command names win collisions (the dispatch tries the
@@ -37454,22 +37913,20 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
37454
37913
  (0, import_react.useEffect)(() => {
37455
37914
  if (historyFill === void 0) return;
37456
37915
  const safe = sanitizeDraftText(historyFill.text);
37457
- draftImagesRef.current = [];
37458
- setDraftImages([]);
37459
- draftFilesRef.current = [];
37460
- setDraftFiles([]);
37461
- valueRef.current = safe;
37462
- cursorRef.current = safe.length;
37463
- setValue(safe);
37464
- setCursor(safe.length);
37916
+ const current = valueRef.current;
37917
+ const joined = appendRecall(current, safe);
37918
+ valueRef.current = joined;
37919
+ cursorRef.current = joined.length;
37920
+ setValue(joined);
37921
+ setCursor(joined.length);
37465
37922
  resetCursorBlink();
37466
37923
  preferredColumnRef.current = null;
37467
37924
  setDismissedMenuValue(void 0);
37468
37925
  recall.current = {
37469
37926
  entries: recallSpace,
37470
37927
  index: historyFill.index,
37471
- savedDraft: safe,
37472
- lastRecalled: safe
37928
+ savedDraft: current,
37929
+ lastRecalled: joined
37473
37930
  };
37474
37931
  historyConsumed();
37475
37932
  }, [
@@ -37510,7 +37967,11 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
37510
37967
  return () => {
37511
37968
  stdin.read = originalRead;
37512
37969
  };
37513
- }, [focusReporting, stdin]);
37970
+ }, [
37971
+ focusReporting,
37972
+ inputStdout,
37973
+ stdin
37974
+ ]);
37514
37975
  if (recall.current.entries !== recallSpace) {
37515
37976
  const index = recall.current.index === null || recall.current.index < recallSpace.length ? recall.current.index : null;
37516
37977
  recall.current = {
@@ -37522,7 +37983,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
37522
37983
  const [completionIndex, setCompletionIndex] = (0, import_react.useState)(0);
37523
37984
  const [dismissedMenuValue, setDismissedMenuValue] = (0, import_react.useState)(void 0);
37524
37985
  const candidates = completionCandidates(value, descriptors, skills);
37525
- const slashActive = candidates.length > 0 && value.startsWith("/") && !value.includes(" ") && !value.includes("\n");
37986
+ const slashActive = candidates.length > 0 && value.startsWith("/") && !value.includes(" ") && !value.includes("\n") && !looksLikePathDraft(value);
37526
37987
  const beforeCursor = value.slice(0, cursor);
37527
37988
  const lastLine = beforeCursor.split("\n").at(-1) ?? "";
37528
37989
  const tokenMatch = /(^|\s)@([^\s]*)$/u.exec(lastLine);
@@ -37638,8 +38099,8 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
37638
38099
  const requestId = mentionRequestRef.current + 1;
37639
38100
  mentionRequestRef.current = requestId;
37640
38101
  if (!active || !mentionActive) {
37641
- setMentionRows([]);
37642
- setMentionError(void 0);
38102
+ setMentionRows((current) => current.length === 0 ? current : []);
38103
+ setMentionError((current) => current === void 0 ? current : void 0);
37643
38104
  return;
37644
38105
  }
37645
38106
  setMentionError(void 0);
@@ -37661,6 +38122,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
37661
38122
  };
37662
38123
  }, [
37663
38124
  active,
38125
+ loadMentions,
37664
38126
  mentionActive,
37665
38127
  mentionToken?.query
37666
38128
  ]);
@@ -37681,6 +38143,22 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
37681
38143
  origin: "mention"
37682
38144
  })) : candidates;
37683
38145
  const menuHeightRows = menuActive ? completionMenuRowCount(inputTerminalRows, menuRows.length) : 0;
38146
+ /**
38147
+ * What accepting the highlighted completion candidate would insert, or ''
38148
+ * when nothing can be accepted (no rows, or an image row that resolves
38149
+ * asynchronously). The Enter branch uses it to tell a real accept from a
38150
+ * no-op that must fall through to submission.
38151
+ */
38152
+ const highlightedCompletion = () => {
38153
+ if (mentionActive) {
38154
+ const row = rankedMentionRows[completionIndex % Math.max(1, rankedMentionRows.length)];
38155
+ if (row === void 0) return "";
38156
+ if (row.kind === "file" && row.path !== void 0 && looksLikeImagePath(row.path)) return "";
38157
+ return row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`;
38158
+ }
38159
+ const candidate = candidates[completionIndex % Math.max(1, candidates.length)];
38160
+ return candidate === void 0 ? "" : `${candidate.label} `;
38161
+ };
37684
38162
  /** Accept the highlighted completion-menu candidate into the draft. */
37685
38163
  const acceptMenuCandidate = () => {
37686
38164
  if (mentionActive && mentionToken !== void 0) {
@@ -37768,6 +38246,14 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
37768
38246
  preferredColumnRef.current = null;
37769
38247
  setCompletionIndex(0);
37770
38248
  setDismissedMenuValue(void 0);
38249
+ const dropped = parsePastedAttachmentPaths(edit.value);
38250
+ if (dropped.images.length > 0 || dropped.files.length > 0) {
38251
+ valueRef.current = "";
38252
+ cursorRef.current = 0;
38253
+ setValue("");
38254
+ setCursor(0);
38255
+ insertDroppedAttachments(dropped.images, dropped.files);
38256
+ }
37771
38257
  };
37772
38258
  /** Move the cursor without editing; horizontal moves clear the column preference. */
37773
38259
  const moveCursorTo = (next) => {
@@ -37934,7 +38420,10 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
37934
38420
  return;
37935
38421
  }
37936
38422
  if (menuActive) {
37937
- if (!(!mentionActive && candidates.some((candidate) => candidate.label === liveValue))) {
38423
+ const highlighted = highlightedCompletion();
38424
+ const repeatsToken = mentionActive && mentionToken !== void 0 && cursor === liveValue.length && liveValue.slice(mentionToken.start, cursor) === highlighted;
38425
+ const exactSlash = !mentionActive && candidates.some((candidate) => candidate.label === liveValue);
38426
+ if (!(highlighted === "" || repeatsToken || exactSlash)) {
37938
38427
  acceptMenuCandidate();
37939
38428
  return;
37940
38429
  }
@@ -38020,7 +38509,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
38020
38509
  return;
38021
38510
  }
38022
38511
  if (text === "/copy") {
38023
- copyLastResponse().then((outcome) => notify(outcome), (error) => notify(`copy failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
38512
+ copyLastResponse().then((outcome) => notify(outcome), (error) => notify(t("notice.copyFailed", { message: error instanceof Error ? error.message : String(error) }), "error"));
38024
38513
  return;
38025
38514
  }
38026
38515
  if (text === "/diff" || text.startsWith("/diff ")) {
@@ -38161,6 +38650,11 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
38161
38650
  openDelete(text.slice(7).trim());
38162
38651
  return;
38163
38652
  }
38653
+ const slash = slashNameAndArgs(text);
38654
+ if (slash !== void 0 && BARE_LOCAL_COMMANDS.has(slash.name) && slash.args !== "") {
38655
+ notify(t("notice.usage.bareCommand", { name: slash.name }), "warning");
38656
+ return;
38657
+ }
38164
38658
  if (submitMode === "steer") steer(text);
38165
38659
  else dispatch(text);
38166
38660
  return;
@@ -38176,11 +38670,11 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
38176
38670
  return;
38177
38671
  }
38178
38672
  if (menuActive && key.upArrow) {
38179
- setCompletionIndex((index) => (index + menuRows.length - 1) % menuRows.length);
38673
+ setCompletionIndex((index) => stepCompletionIndex(index, -1, menuRows.length));
38180
38674
  return;
38181
38675
  }
38182
38676
  if (menuActive && key.downArrow) {
38183
- setCompletionIndex((index) => (index + 1) % menuRows.length);
38677
+ setCompletionIndex((index) => stepCompletionIndex(index, 1, menuRows.length));
38184
38678
  return;
38185
38679
  }
38186
38680
  const rawTokens = rawEditorTokens.current;
@@ -38270,13 +38764,12 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
38270
38764
  clearTimeout(timer);
38271
38765
  pasteBracketCancelRef.current = void 0;
38272
38766
  };
38273
- text = text.replaceAll(PASTE_START_MARKER, "");
38274
38767
  }
38275
38768
  if (text.includes("[201~")) {
38276
38769
  pasteBracketRef.current = false;
38277
38770
  pasteBracketCancelRef.current?.();
38278
- text = text.replaceAll(PASTE_END_MARKER, "");
38279
38771
  }
38772
+ text = stripPasteMarkers(text);
38280
38773
  if (text === "") return;
38281
38774
  if (text.length > 1) {
38282
38775
  const dropped = parsePastedAttachmentPaths(text);
@@ -38333,7 +38826,9 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
38333
38826
  }), 2 + caret.column);
38334
38827
  (0, import_react.useEffect)(() => {
38335
38828
  onMenuRows(menuHeightRows);
38336
- return () => onMenuRows(0);
38829
+ return () => {
38830
+ if (menuHeightRows !== 0) onMenuRows(0);
38831
+ };
38337
38832
  }, [menuHeightRows, onMenuRows]);
38338
38833
  const bandWidth = Math.max(1, columns - 1);
38339
38834
  const bandBg = inkColor(getPalette().composerBand);
@@ -38665,6 +39160,8 @@ function App(props) {
38665
39160
  const [authorizationDirectory, setAuthorizationDirectory] = (0, import_react.useState)(void 0);
38666
39161
  const [authorizationError, setAuthorizationError] = (0, import_react.useState)(void 0);
38667
39162
  const [modelLoadEpoch, setModelLoadEpoch] = (0, import_react.useState)(0);
39163
+ /** Bumped when /effort's catalog lookup must be ignored (panel closed or superseded). */
39164
+ const effortLookupEpoch = (0, import_react.useRef)(0);
38668
39165
  const [notice, setNotice] = (0, import_react.useState)(void 0);
38669
39166
  const notify = (0, import_react.useCallback)((text, tone = "info") => {
38670
39167
  setNotice({
@@ -38672,15 +39169,16 @@ function App(props) {
38672
39169
  tone
38673
39170
  });
38674
39171
  }, []);
39172
+ const { loadModels, loadModelProviders, loadProviderAuthorizations, onBridgeReady } = props;
38675
39173
  (0, import_react.useEffect)(() => {
38676
- props.onBridgeReady({ notify });
38677
- }, []);
39174
+ onBridgeReady({ notify });
39175
+ }, [notify, onBridgeReady]);
38678
39176
  (0, import_react.useEffect)(() => {
38679
39177
  if (!modelOpen) return;
38680
39178
  let cancelled = false;
38681
39179
  setDirectory(void 0);
38682
39180
  setModelError(void 0);
38683
- Promise.resolve().then(() => props.loadModels()).then((loaded) => {
39181
+ Promise.resolve().then(() => loadModels()).then((loaded) => {
38684
39182
  if (!cancelled) setDirectory(loaded);
38685
39183
  }, (error) => {
38686
39184
  if (!cancelled) setModelError(error instanceof Error ? error.message : String(error));
@@ -38689,16 +39187,16 @@ function App(props) {
38689
39187
  cancelled = true;
38690
39188
  };
38691
39189
  }, [
39190
+ loadModels,
38692
39191
  modelOpen,
38693
- modelLoadEpoch,
38694
- props.loadModels
39192
+ modelLoadEpoch
38695
39193
  ]);
38696
39194
  (0, import_react.useEffect)(() => {
38697
- if (!modelOpen || props.loadModelProviders === void 0) return;
39195
+ if (!modelOpen || loadModelProviders === void 0) return;
38698
39196
  let cancelled = false;
38699
39197
  setProviderDirectory(void 0);
38700
39198
  setProviderError(void 0);
38701
- Promise.resolve().then(() => props.loadModelProviders()).then((loaded) => {
39199
+ Promise.resolve().then(() => loadModelProviders()).then((loaded) => {
38702
39200
  if (!cancelled) setProviderDirectory(loaded);
38703
39201
  }, (error) => {
38704
39202
  if (!cancelled) setProviderError(error instanceof Error ? error.message : String(error));
@@ -38707,16 +39205,16 @@ function App(props) {
38707
39205
  cancelled = true;
38708
39206
  };
38709
39207
  }, [
39208
+ loadModelProviders,
38710
39209
  modelOpen,
38711
- modelLoadEpoch,
38712
- props.loadModelProviders
39210
+ modelLoadEpoch
38713
39211
  ]);
38714
39212
  (0, import_react.useEffect)(() => {
38715
- if (!modelOpen || props.loadProviderAuthorizations === void 0) return;
39213
+ if (!modelOpen || loadProviderAuthorizations === void 0) return;
38716
39214
  let cancelled = false;
38717
39215
  setAuthorizationDirectory(void 0);
38718
39216
  setAuthorizationError(void 0);
38719
- Promise.resolve().then(() => props.loadProviderAuthorizations()).then((loaded) => {
39217
+ Promise.resolve().then(() => loadProviderAuthorizations()).then((loaded) => {
38720
39218
  if (!cancelled) setAuthorizationDirectory(loaded);
38721
39219
  }, (error) => {
38722
39220
  if (!cancelled) setAuthorizationError(error instanceof Error ? error.message : String(error));
@@ -38725,9 +39223,9 @@ function App(props) {
38725
39223
  cancelled = true;
38726
39224
  };
38727
39225
  }, [
39226
+ loadProviderAuthorizations,
38728
39227
  modelOpen,
38729
- modelLoadEpoch,
38730
- props.loadProviderAuthorizations
39228
+ modelLoadEpoch
38731
39229
  ]);
38732
39230
  (0, import_react.useEffect)(() => {
38733
39231
  const subscribe = props.subscribeModelProviders;
@@ -38770,6 +39268,8 @@ function App(props) {
38770
39268
  const [pluginOpen, setPluginOpen] = (0, import_react.useState)(false);
38771
39269
  const [pluginQuery, setPluginQuery] = (0, import_react.useState)("");
38772
39270
  const [updateOpen, setUpdateOpen] = (0, import_react.useState)(false);
39271
+ const [updateApplying, setUpdateApplying] = (0, import_react.useState)(false);
39272
+ (0, import_react.useEffect)(() => subscribeUpdateApplyRunning(setUpdateApplying), []);
38773
39273
  const [scheduleOpen, setScheduleOpen] = (0, import_react.useState)(false);
38774
39274
  const [jobsOpen, setJobsOpen] = (0, import_react.useState)(false);
38775
39275
  const [statuslineOpen, setStatuslineOpen] = (0, import_react.useState)(false);
@@ -38781,7 +39281,7 @@ function App(props) {
38781
39281
  const [subagentOpen, setSubagentOpen] = (0, import_react.useState)(false);
38782
39282
  const [todosOpen, setTodosOpen] = (0, import_react.useState)(false);
38783
39283
  const [usageOpen, setUsageOpen] = (0, import_react.useState)(false);
38784
- /** /delete state: delete-mode hint plus an optional pre-armed row id. */
39284
+ /** /delete state: dedicated picker mode plus an optional pre-armed row id. */
38785
39285
  const [resumeDelete, setResumeDelete] = (0, import_react.useState)({ mode: false });
38786
39286
  /** The row id awaiting y/n in the COMPOSER (codex delete confirm): the
38787
39287
  * composer takes the keys, the resume panel yields until it settles. */
@@ -38794,19 +39294,20 @@ function App(props) {
38794
39294
  const cancelDelete = (0, import_react.useCallback)(() => {
38795
39295
  setDeleteConfirmId(void 0);
38796
39296
  }, []);
39297
+ const deleteSession = props.deleteSession;
38797
39298
  const confirmDelete = (0, import_react.useCallback)(() => {
38798
39299
  const id = deleteConfirmId;
38799
39300
  if (id === void 0) return;
38800
39301
  setDeleteConfirmId(void 0);
38801
- props.deleteSession(id).then((outcome) => {
39302
+ deleteSession(id).then((outcome) => {
38802
39303
  notify(outcome);
38803
39304
  setDeleteReloadToken((token) => token + 1);
38804
39305
  }, (reason) => {
38805
39306
  notify(t("notice.deleteFailed", { message: reason instanceof Error ? reason.message : String(reason) }), "error");
38806
39307
  });
38807
39308
  }, [
39309
+ deleteSession,
38808
39310
  deleteConfirmId,
38809
- props.deleteSession,
38810
39311
  notify
38811
39312
  ]);
38812
39313
  /** The /history panel's accepted entry: text plus its recall-space index. */
@@ -38841,31 +39342,137 @@ function App(props) {
38841
39342
  const agentRows = (0, import_react.useSyncExternalStore)(subscribeSubagents, readAgentRows);
38842
39343
  const approvalPending = approvalSnapshot.pending !== void 0;
38843
39344
  const questionPending = questionSnapshot.pending !== void 0;
38844
- const inputActive = deleteConfirmId !== void 0 ? !approvalPending && !questionPending : !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && diffView === void 0 && !reviewPickerOpen && !approvalPending && !questionPending;
38845
- const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && diffView === void 0 && !reviewPickerOpen && !approvalPending && !questionPending;
39345
+ /**
39346
+ * Every keyboard-owning surface that is mutually exclusive with the composer,
39347
+ * in precedence order. This ONE list drives the composer gate, the transcript
39348
+ * visibility, the frozen band's hint, and the hand-off when a human approval
39349
+ * or question arrives, so a panel cannot be wired into one of them and
39350
+ * forgotten in the others.
39351
+ */
39352
+ const panelSurfaces = [
39353
+ {
39354
+ hint: "the diff review",
39355
+ open: diffView !== void 0,
39356
+ close: () => setDiffView(void 0)
39357
+ },
39358
+ {
39359
+ hint: "the review picker",
39360
+ open: reviewPickerOpen,
39361
+ close: () => setReviewPickerOpen(false)
39362
+ },
39363
+ {
39364
+ hint: "/model",
39365
+ open: modelOpen,
39366
+ close: () => {
39367
+ setModelOpen(false);
39368
+ setProviderOpen(false);
39369
+ setProviderAction(void 0);
39370
+ setEffortFor(void 0);
39371
+ }
39372
+ },
39373
+ {
39374
+ hint: "/help",
39375
+ open: helpOpen,
39376
+ close: () => setHelpOpen(false)
39377
+ },
39378
+ {
39379
+ hint: "/mode",
39380
+ open: modeOpen,
39381
+ close: () => setModeOpen(false)
39382
+ },
39383
+ {
39384
+ hint: "/permission",
39385
+ open: permissionOpen,
39386
+ close: () => setPermissionOpen(false)
39387
+ },
39388
+ {
39389
+ hint: "/resume",
39390
+ open: resumeOpen,
39391
+ close: () => setResumeOpen(false)
39392
+ },
39393
+ {
39394
+ hint: "/search",
39395
+ open: searchOpen,
39396
+ close: () => setSearchOpen(false)
39397
+ },
39398
+ {
39399
+ hint: "/plugin",
39400
+ open: pluginOpen,
39401
+ close: () => setPluginOpen(false)
39402
+ },
39403
+ {
39404
+ hint: "/update",
39405
+ open: updateOpen,
39406
+ close: () => setUpdateOpen(false)
39407
+ },
39408
+ {
39409
+ hint: "/schedule",
39410
+ open: scheduleOpen,
39411
+ close: () => setScheduleOpen(false)
39412
+ },
39413
+ {
39414
+ hint: "/jobs",
39415
+ open: jobsOpen,
39416
+ close: () => setJobsOpen(false)
39417
+ },
39418
+ {
39419
+ hint: "/statusline",
39420
+ open: statuslineOpen,
39421
+ close: () => setStatuslineOpen(false)
39422
+ },
39423
+ {
39424
+ hint: "/theme",
39425
+ open: themeOpen,
39426
+ close: () => setThemeOpen(false)
39427
+ },
39428
+ {
39429
+ hint: "/language",
39430
+ open: languageOpen,
39431
+ close: () => setLanguageOpen(false)
39432
+ },
39433
+ {
39434
+ hint: "/history",
39435
+ open: historyOpen,
39436
+ close: () => setHistoryOpen(false)
39437
+ },
39438
+ {
39439
+ hint: "/queue",
39440
+ open: queueOpen,
39441
+ close: () => setQueueOpen(false)
39442
+ },
39443
+ {
39444
+ hint: "/agents",
39445
+ open: agentsOpen,
39446
+ close: () => setAgentsOpen(false)
39447
+ },
39448
+ {
39449
+ hint: "/subagent",
39450
+ open: subagentOpen,
39451
+ close: () => setSubagentOpen(false)
39452
+ },
39453
+ {
39454
+ hint: "/todos",
39455
+ open: todosOpen,
39456
+ close: () => setTodosOpen(false)
39457
+ },
39458
+ {
39459
+ hint: "/usage",
39460
+ open: usageOpen,
39461
+ close: () => setUsageOpen(false)
39462
+ }
39463
+ ];
39464
+ const panelSurfacesRef = (0, import_react.useRef)(panelSurfaces);
39465
+ panelSurfacesRef.current = panelSurfaces;
39466
+ const openPanel = panelSurfaces.find((surface) => surface.open);
39467
+ const inspectorVisible = verboseOpen && !approvalPending && !questionPending;
39468
+ const modalVisible = openPanel !== void 0 || inspectorVisible || approvalPending || questionPending;
39469
+ const inputActive = deleteConfirmId !== void 0 ? !approvalPending && !questionPending : !modalVisible;
39470
+ const transcriptVisible = !modalVisible;
38846
39471
  (0, import_react.useEffect)(() => {
38847
39472
  if (!approvalPending && !questionPending) return;
38848
- setModelOpen(false);
38849
- setProviderOpen(false);
38850
- setProviderAction(void 0);
38851
- setEffortFor(void 0);
38852
- setHelpOpen(false);
38853
- setModeOpen(false);
38854
- setPermissionOpen(false);
38855
- setResumeOpen(false);
38856
- setPluginOpen(false);
38857
- setUpdateOpen(false);
38858
- setScheduleOpen(false);
38859
- setStatuslineOpen(false);
38860
- setThemeOpen(false);
38861
- setHistoryOpen(false);
38862
- setQueueOpen(false);
38863
- setAgentsOpen(false);
38864
- setSubagentOpen(false);
38865
- setTodosOpen(false);
39473
+ for (const surface of panelSurfacesRef.current) if (surface.open) surface.close();
38866
39474
  setDeleteConfirmId(void 0);
38867
39475
  setVerboseOpen(false);
38868
- setDiffView(void 0);
38869
39476
  }, [approvalPending, questionPending]);
38870
39477
  const appStdout = useStdout().stdout;
38871
39478
  const [terminalSize, setTerminalSize] = (0, import_react.useState)(() => ({
@@ -38975,20 +39582,28 @@ function App(props) {
38975
39582
  const auditedLiveLines = liveAudit.allocation.live === visibleLiveLines.length ? visibleLiveLines : visibleLiveLines.slice(-liveAudit.allocation.live);
38976
39583
  const auditedReasoningRows = liveAudit.allocation.reasoning;
38977
39584
  const auditedAnswerRows = liveAudit.allocation.answer;
38978
- const inspectorVisible = verboseOpen && !approvalPending && !questionPending;
38979
- const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || updateOpen || scheduleOpen || jobsOpen || statuslineOpen || themeOpen || languageOpen || historyOpen || queueOpen || agentsOpen || subagentOpen || todosOpen || usageOpen || inspectorVisible || diffView !== void 0 || reviewPickerOpen || approvalPending || questionPending;
38980
- const keyboardOwner = approvalPending ? "the approval prompt" : questionPending ? "the question" : diffView !== void 0 ? "the diff review" : reviewPickerOpen ? "the review picker" : modelOpen ? "/model" : helpOpen ? "/help" : modeOpen ? "/mode" : permissionOpen ? "/permission" : resumeOpen ? "/resume" : pluginOpen ? "/plugin" : updateOpen ? "/update" : scheduleOpen ? "/schedule" : jobsOpen ? "/jobs" : statuslineOpen ? "/statusline" : themeOpen ? "/theme" : languageOpen ? "/language" : historyOpen ? "/history" : agentsOpen ? "/agents" : subagentOpen ? "/subagent" : todosOpen ? "/todos" : usageOpen ? "/usage" : inspectorVisible ? "history details" : void 0;
38981
- const frozenHint = keyboardOwner === void 0 ? void 0 : `keys go to ${keyboardOwner} · esc ${approvalPending ? "rejects" : questionPending ? "cancels" : "closes"}`;
39585
+ const keyboardOwner = approvalPending ? "the approval prompt" : questionPending ? "the question" : openPanel?.hint ?? (inspectorVisible ? "history details" : void 0);
39586
+ useStableInput((input, key) => {
39587
+ if (!(key.ctrl && input === "c")) return;
39588
+ if (approvalPending || questionPending || deleteConfirmId !== void 0) return;
39589
+ if (openPanel !== void 0) openPanel.close();
39590
+ else if (inspectorVisible) setVerboseOpen(false);
39591
+ }, true);
39592
+ const frozenHint = keyboardOwner === void 0 ? void 0 : t("frozen.keysGoTo", {
39593
+ owner: keyboardOwner,
39594
+ action: approvalPending ? t("frozen.action.rejects") : questionPending ? t("frozen.action.cancels") : updateApplying && updateOpen ? t("frozen.action.waits") : t("frozen.action.closes")
39595
+ });
38982
39596
  const closeInspector = (0, import_react.useCallback)(() => {
38983
39597
  setVerboseOpen(false);
38984
39598
  }, []);
38985
- const refreshScreen = () => {
39599
+ const refreshScreen = (0, import_react.useCallback)((opts) => {
39600
+ const clear = opts?.wipeScrollback === false ? TRIM_REFLOW_CLEAR : RESIZE_REFLOW_CLEAR;
38986
39601
  if (appStdout !== void 0) {
38987
39602
  synchronizedReplayPending.current = true;
38988
- appStdout.write("\x1B[?2026h\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H");
39603
+ appStdout.write(SYNCHRONIZED_UPDATE_BEGIN + clear);
38989
39604
  }
38990
39605
  setRefreshEpoch((epoch) => epoch + 1);
38991
- };
39606
+ }, [appStdout]);
38992
39607
  const applyRainbow = (seed) => {
38993
39608
  rerollRainbow(seed);
38994
39609
  setTheme("rainbow");
@@ -39005,10 +39620,11 @@ function App(props) {
39005
39620
  const settledNeedsTrim = settledRowsCache.current?.needsTrim === true;
39006
39621
  (0, import_react.useEffect)(() => {
39007
39622
  if (!settledNeedsTrim || busy || streamingActive) return;
39008
- refreshScreen();
39623
+ refreshScreen({ wipeScrollback: false });
39009
39624
  }, [
39010
- settledNeedsTrim,
39011
39625
  busy,
39626
+ refreshScreen,
39627
+ settledNeedsTrim,
39012
39628
  streamingActive
39013
39629
  ]);
39014
39630
  const sessionHasImages = (0, import_react.useMemo)(() => view.entries.some((entry) => (entry.kind === "user" || entry.kind === "pending") && (entry.images?.length ?? 0) > 0), [view.entries]);
@@ -39033,6 +39649,7 @@ function App(props) {
39033
39649
  setModelLoadEpoch((epoch) => epoch + 1);
39034
39650
  };
39035
39651
  const closeModelSurface = () => {
39652
+ effortLookupEpoch.current += 1;
39036
39653
  setModelOpen(false);
39037
39654
  setProviderOpen(false);
39038
39655
  setProviderAction(void 0);
@@ -39244,7 +39861,8 @@ function App(props) {
39244
39861
  prefix: "✻ ",
39245
39862
  continuationPrefix: " ",
39246
39863
  dim: true,
39247
- maxRows: auditedReasoningRows
39864
+ maxRows: auditedReasoningRows,
39865
+ columns: Math.max(1, terminalColumns - 2)
39248
39866
  }) : view.streaming === "" ? (0, import_react.createElement)(ShimmerLine, {
39249
39867
  text: "✻ Thinking… (Ctrl/Alt+R to expand)",
39250
39868
  animated: animations
@@ -39253,12 +39871,14 @@ function App(props) {
39253
39871
  prefix: "✻ ",
39254
39872
  continuationPrefix: " ",
39255
39873
  dim: true,
39256
- maxRows: auditedReasoningRows
39874
+ maxRows: auditedReasoningRows,
39875
+ columns: Math.max(1, terminalColumns - 2)
39257
39876
  }) : void 0, view.streaming !== "" && auditedAnswerRows > 0 ? (0, import_react.createElement)(StreamTail, {
39258
39877
  text: view.streaming,
39259
39878
  dim: false,
39260
39879
  maxRows: auditedAnswerRows,
39261
- prefix: " "
39880
+ prefix: " ",
39881
+ columns: Math.max(1, terminalColumns - 2)
39262
39882
  }, busy ? (0, import_react.createElement)(Caret, { animated: animations }) : void 0) : void 0, deepDivingVisible ? (0, import_react.createElement)(DeepDivingLine, {
39263
39883
  since: view.busySince,
39264
39884
  animated: animations
@@ -39310,7 +39930,9 @@ function App(props) {
39310
39930
  close: () => setReviewPickerOpen(false)
39311
39931
  }) : void 0, verboseOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(MemoVerbosePanel, {
39312
39932
  entries: view.entries,
39313
- onClose: closeInspector
39933
+ onClose: closeInspector,
39934
+ columns: terminalColumns,
39935
+ rows: terminalRows
39314
39936
  }) : void 0, modeOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(ModePanel, {
39315
39937
  current: props.mode,
39316
39938
  load: props.loadPresets,
@@ -39338,11 +39960,13 @@ function App(props) {
39338
39960
  currentCwd: props.workspaceRoot,
39339
39961
  load: props.loadSessions,
39340
39962
  readTranscript: props.loadSessionTranscript,
39341
- requestDelete,
39963
+ requestDelete: resumeDelete.mode ? requestDelete : void 0,
39342
39964
  deleteConfirmId,
39343
39965
  reloadToken: deleteReloadToken,
39344
39966
  deleteMode: resumeDelete.mode,
39967
+ presetId: resumeDelete.id,
39345
39968
  select: (row) => {
39969
+ if (resumeDelete.mode) return;
39346
39970
  props.switchSession(row);
39347
39971
  setResumeOpen(false);
39348
39972
  },
@@ -39467,6 +40091,7 @@ function App(props) {
39467
40091
  interrupt: props.interrupt,
39468
40092
  quit: props.quit,
39469
40093
  openModel: () => {
40094
+ effortLookupEpoch.current += 1;
39470
40095
  setDirectory(void 0);
39471
40096
  setModelError(void 0);
39472
40097
  setProviderDirectory(void 0);
@@ -39479,7 +40104,9 @@ function App(props) {
39479
40104
  setModelOpen(true);
39480
40105
  },
39481
40106
  openEffort: () => {
40107
+ const epoch = ++effortLookupEpoch.current;
39482
40108
  props.loadModels().then((loaded) => {
40109
+ if (epoch !== effortLookupEpoch.current) return;
39483
40110
  const [provider, model] = modelLabel.split("/");
39484
40111
  const row = loaded.rows.find((candidate) => candidate.provider === provider && candidate.model === model) ?? loaded.rows.find((candidate) => candidate.model === model && candidate.reasoning !== void 0) ?? loaded.rows.find((candidate) => candidate.model === model);
39485
40112
  if (row === void 0) {
@@ -39542,7 +40169,7 @@ function App(props) {
39542
40169
  mode: true,
39543
40170
  ...armed === void 0 ? {} : { id: armed }
39544
40171
  });
39545
- setDeleteConfirmId(armed);
40172
+ setDeleteConfirmId(void 0);
39546
40173
  setResumeOpen(true);
39547
40174
  },
39548
40175
  openDiff: (argument) => {
@@ -39765,7 +40392,23 @@ function createKeypressSplitter() {
39765
40392
  }
39766
40393
  const head = buffer[0];
39767
40394
  if (head !== "\x1B") {
39768
- const take = head >= "\ud800" && head <= "\udbff" && buffer[1] !== void 0 ? 2 : 1;
40395
+ if (head < " " || head === "") {
40396
+ units.push(head);
40397
+ buffer = buffer.slice(1);
40398
+ continue;
40399
+ }
40400
+ let take = 0;
40401
+ while (take < buffer.length) {
40402
+ const ch = buffer[take];
40403
+ if (ch === "\x1B" || ch < " " || ch === "") break;
40404
+ if (ch >= "\ud800" && ch <= "\udbff") {
40405
+ if (buffer[take + 1] === void 0) break;
40406
+ take += 2;
40407
+ continue;
40408
+ }
40409
+ take += 1;
40410
+ }
40411
+ if (take === 0) break;
39769
40412
  units.push(buffer.slice(0, take));
39770
40413
  buffer = buffer.slice(take);
39771
40414
  continue;
@@ -40457,8 +41100,23 @@ function createTranscriptStore(replay) {
40457
41100
  }
40458
41101
  },
40459
41102
  reset() {
41103
+ const live = {
41104
+ streaming: acc.streaming,
41105
+ streamingReasoning: acc.streamingReasoning,
41106
+ busy: acc.busy,
41107
+ busySince: acc.busySince,
41108
+ model: acc.model,
41109
+ firstChunkAt: acc.firstChunkAt,
41110
+ stepStart: acc.stepStart
41111
+ };
40460
41112
  acc = createReplayAccumulator();
40461
- attemptKeys.clear();
41113
+ acc.streaming = live.streaming;
41114
+ acc.streamingReasoning = live.streamingReasoning;
41115
+ acc.busy = live.busy;
41116
+ acc.busySince = live.busySince;
41117
+ acc.model = live.model;
41118
+ acc.firstChunkAt = live.firstChunkAt;
41119
+ acc.stepStart = live.stepStart;
40462
41120
  dirty = true;
40463
41121
  notify();
40464
41122
  }
@@ -41183,7 +41841,12 @@ async function resolveEditorKeysStartupHint({ env, paths, flagPath }) {
41183
41841
  function selectForkSeed(events, atSeq) {
41184
41842
  if (atSeq !== void 0 && (!Number.isSafeInteger(atSeq) || atSeq < 0)) throw new Error("fork event sequence must be a non-negative integer");
41185
41843
  const lastSeq = events.at(-1)?.seq ?? -1;
41186
- const boundary = (atSeq === void 0 ? void 0 : events.find((event) => event.type === "turn/end" && event.seq >= atSeq)) ?? (atSeq === void 0 || atSeq > lastSeq ? events.findLast((event) => event.type === "turn/end") : void 0);
41844
+ let boundary;
41845
+ if (atSeq === void 0 || atSeq > lastSeq) boundary = events.findLast((event) => event.type === "turn/end");
41846
+ else {
41847
+ const start = events.findLast((event) => event.type === "turn/start" && event.seq <= atSeq);
41848
+ boundary = start === void 0 ? void 0 : events.find((event) => event.type === "turn/end" && event.seq >= start.seq);
41849
+ }
41187
41850
  if (boundary === void 0) throw new Error(atSeq !== void 0 && atSeq <= lastSeq ? `the turn containing event ${atSeq} has not completed` : "this session has no completed turn to fork from");
41188
41851
  let cut = events.indexOf(boundary) + 1;
41189
41852
  while (cut < events.length && events[cut]?.type !== "turn/start") cut += 1;
@@ -41199,6 +41862,7 @@ var SessionSwitchQueue = class {
41199
41862
  failed;
41200
41863
  pending;
41201
41864
  pumping = false;
41865
+ running = false;
41202
41866
  constructor(execute, failed) {
41203
41867
  this.execute = execute;
41204
41868
  this.failed = failed;
@@ -41219,6 +41883,15 @@ var SessionSwitchQueue = class {
41219
41883
  this.pending = void 0;
41220
41884
  return true;
41221
41885
  }
41886
+ /**
41887
+ * Whether a queued change is being activated right now. Between the idle
41888
+ * wait and the handoff the old session is still installed, so a submission
41889
+ * made in that window would start a turn the handoff then discards; callers
41890
+ * use this to refuse one instead of losing it.
41891
+ */
41892
+ get activating() {
41893
+ return this.running;
41894
+ }
41222
41895
  async pump() {
41223
41896
  this.pumping = true;
41224
41897
  try {
@@ -41227,10 +41900,13 @@ var SessionSwitchQueue = class {
41227
41900
  await observed.activity.whenIdle();
41228
41901
  if (this.pending !== observed) continue;
41229
41902
  this.pending = void 0;
41903
+ this.running = true;
41230
41904
  try {
41231
41905
  await this.execute(observed.value);
41232
41906
  } catch (error) {
41233
41907
  this.failed(error);
41908
+ } finally {
41909
+ this.running = false;
41234
41910
  }
41235
41911
  }
41236
41912
  } finally {
@@ -42350,7 +43026,7 @@ async function run(ctx, startup, io) {
42350
43026
  try {
42351
43027
  parsed = currentMentions.parse(line);
42352
43028
  } catch (error) {
42353
- bridge.notify(`invalid session reference: ${error instanceof Error ? error.message : String(error)}`, "error");
43029
+ bridge.notify(t("notice.invalidReference", { message: error instanceof Error ? error.message : String(error) }), "error");
42354
43030
  return;
42355
43031
  }
42356
43032
  if (deliveryChain.epoch !== epoch) deliveryChain = {
@@ -42376,7 +43052,10 @@ async function run(ctx, startup, io) {
42376
43052
  if (mode === "steer") currentAgent.steer(message);
42377
43053
  else currentAgent.followup(message);
42378
43054
  } catch (error) {
42379
- bridge.notify(`${mode === "steer" ? "steering" : "message"} failed: ${error instanceof Error ? error.message : String(error)}`, "error");
43055
+ bridge.notify(t("notice.messageFailed", {
43056
+ kind: mode === "steer" ? "steering" : "message",
43057
+ message: error instanceof Error ? error.message : String(error)
43058
+ }), "error");
42380
43059
  }
42381
43060
  };
42382
43061
  if (parsed.references.length === 0) {
@@ -42392,7 +43071,7 @@ async function run(ctx, startup, io) {
42392
43071
  }, (error) => {
42393
43072
  pendingControllers.delete(controller);
42394
43073
  if (controller.signal.aborted || epoch !== atEpoch) return;
42395
- bridge.notify(`session reference failed: ${error instanceof Error ? error.message : String(error)}`, "error");
43074
+ bridge.notify(t("notice.referenceFailed", { message: error instanceof Error ? error.message : String(error) }), "error");
42396
43075
  });
42397
43076
  });
42398
43077
  };
@@ -42451,7 +43130,7 @@ async function run(ctx, startup, io) {
42451
43130
  }
42452
43131
  await next.handle.dispose().catch(() => {});
42453
43132
  if (!quitting) renderCurrent();
42454
- bridge.notify(`session activation failed: ${error instanceof Error ? error.message : String(error)}`, "error");
43133
+ bridge.notify(t("notice.activationFailed", { message: error instanceof Error ? error.message : String(error) }), "error");
42455
43134
  return;
42456
43135
  }
42457
43136
  abortPendingControllers();
@@ -42467,23 +43146,27 @@ async function run(ctx, startup, io) {
42467
43146
  }
42468
43147
  }).catch((error) => {
42469
43148
  pendingInputs.length = 0;
42470
- bridge.notify(`session creation failed: ${error instanceof Error ? error.message : String(error)}`, "error");
43149
+ bridge.notify(t("notice.creationFailed", { message: error instanceof Error ? error.message : String(error) }), "error");
42471
43150
  });
42472
43151
  };
42473
43152
  /** Deliver one readable line to the agent, expanding session mentions first. */
42474
43153
  const sendNow = (text, images = [], mode = "followup") => {
42475
43154
  const line = submissionPayload(text);
42476
43155
  if (line.trim() === "" && images.length === 0) return;
43156
+ if (switchQueue.activating) {
43157
+ bridge.notify(t("notice.switchInProgress"), "warning");
43158
+ return;
43159
+ }
42477
43160
  if (images.length === 0 && line.startsWith("/mode ")) {
42478
- switchModeAction(line.slice(6).trim()).then((selected) => bridge.notify(`mode ${selected}`), (error) => bridge.notify(`mode switch failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
43161
+ switchModeAction(line.slice(6).trim()).then((selected) => bridge.notify(t("notice.modeChanged", { value: selected })), (error) => bridge.notify(t("notice.modeChangeFailed", { message: error instanceof Error ? error.message : String(error) }), "error"));
42479
43162
  return;
42480
43163
  }
42481
43164
  if (images.length === 0 && line.startsWith("/permission ")) {
42482
43165
  try {
42483
43166
  const selected = setPermissionAction(line.slice(12).trim());
42484
- bridge.notify(`permission ${selected}`);
43167
+ bridge.notify(t("notice.permissionChanged", { value: selected }));
42485
43168
  } catch (error) {
42486
- bridge.notify(`permission change failed: ${error instanceof Error ? error.message : String(error)}`, "error");
43169
+ bridge.notify(t("notice.permissionChangeFailed", { message: error instanceof Error ? error.message : String(error) }), "error");
42487
43170
  }
42488
43171
  return;
42489
43172
  }
@@ -42533,7 +43216,7 @@ async function run(ctx, startup, io) {
42533
43216
  bridge.notify(t(preserved > 0 ? "notice.turnCancelledKeepQueue" : "notice.turnCancelled"));
42534
43217
  return true;
42535
43218
  } catch (error) {
42536
- bridge.notify(`cancel failed: ${error instanceof Error ? error.message : String(error)}`, "error");
43219
+ bridge.notify(t("notice.cancelFailed", { message: error instanceof Error ? error.message : String(error) }), "error");
42537
43220
  return false;
42538
43221
  }
42539
43222
  };
@@ -42562,7 +43245,7 @@ async function run(ctx, startup, io) {
42562
43245
  */
42563
43246
  const cycleMode = () => {
42564
43247
  if (permissionPresets === void 0 || permissionPresets.names.length === 0) {
42565
- bridge.notify("permission presets are not mounted in this composition", "warning");
43248
+ bridge.notify(t("notice.permissionPresetsUnmounted"), "warning");
42566
43249
  return "";
42567
43250
  }
42568
43251
  try {
@@ -42605,7 +43288,7 @@ async function run(ctx, startup, io) {
42605
43288
  selectPermission(permissionPresets, session, decision.preset);
42606
43289
  return `plan → off · permission → ${decision.preset}`;
42607
43290
  } catch (error) {
42608
- bridge.notify(`mode change failed: ${error instanceof Error ? error.message : String(error)}`, "error");
43291
+ bridge.notify(t("notice.modeChangeFailed", { message: error instanceof Error ? error.message : String(error) }), "error");
42609
43292
  return "";
42610
43293
  }
42611
43294
  };
@@ -42620,7 +43303,7 @@ async function run(ctx, startup, io) {
42620
43303
  if (active === void 0) pendingSelection = selection;
42621
43304
  else active.selection.picked = selection;
42622
43305
  defaultModel.saveSelection(selection).catch((error) => {
42623
- bridge.notify(`model switch applies to this session but was not saved as the default: ${error instanceof Error ? error.message : String(error)}`, "warning");
43306
+ bridge.notify(t("notice.modelNotDefault", { message: error instanceof Error ? error.message : String(error) }), "warning");
42624
43307
  });
42625
43308
  const llm = ctx.get("llm");
42626
43309
  const resolveCallConfig = llm?.resolveCallConfig;
@@ -42629,7 +43312,7 @@ async function run(ctx, startup, io) {
42629
43312
  model: selection.model,
42630
43313
  ...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort }
42631
43314
  })).catch((error) => {
42632
- bridge.notify(`model selection rejected: ${error instanceof Error ? error.message : String(error)} — reopen /model to pick again`, "error");
43315
+ bridge.notify(t("notice.modelSelectionRejected", { message: error instanceof Error ? error.message : String(error) }), "error");
42633
43316
  });
42634
43317
  return `${row.provider}/${row.model}`;
42635
43318
  };
@@ -42653,7 +43336,7 @@ async function run(ctx, startup, io) {
42653
43336
  */
42654
43337
  const exportTranscript = async (argument) => {
42655
43338
  if (session === void 0) {
42656
- bridge.notify("no session yet — submit a message to start", "warning");
43339
+ bridge.notify(t("notice.noSessionYet"), "warning");
42657
43340
  return;
42658
43341
  }
42659
43342
  const wanted = argument.trim();
@@ -42663,9 +43346,9 @@ async function run(ctx, startup, io) {
42663
43346
  const markdown = buildExportMarkdown(store.getView(), session.id);
42664
43347
  try {
42665
43348
  await writeFile(target, `${markdown}\n`, "utf8");
42666
- bridge.notify(`exported to ${target}`);
43349
+ bridge.notify(t("notice.exported", { path: target }));
42667
43350
  } catch (error) {
42668
- bridge.notify(`export failed: ${error instanceof Error ? error.message : String(error)}`, "error");
43351
+ bridge.notify(t("notice.exportFailed", { message: error instanceof Error ? error.message : String(error) }), "error");
42669
43352
  }
42670
43353
  };
42671
43354
  /**
@@ -42700,10 +43383,14 @@ async function run(ctx, startup, io) {
42700
43383
  if (Number.isFinite(newest)) updated.set(record.header.id, newest);
42701
43384
  } catch {}
42702
43385
  }));
42703
- const projected = projectSessionRows(records, options, updated);
42704
- const page = projected.slice(0, 32);
43386
+ const projected = projectSessionRows(records, {
43387
+ ...options,
43388
+ query: ""
43389
+ }, updated);
43390
+ const titleBudget = options.query.trim() === "" ? 32 : Math.min(projected.length, 128);
43391
+ const page = projected.slice(0, titleBudget);
42705
43392
  if (page.length === 0) return projected;
42706
- return mergeSessionTitles(projected, await sessionQuery.readTitleSnapshots(page.map((row) => row.id), signal));
43393
+ return mergeSessionTitles(projected, await sessionQuery.readTitleSnapshots(page.map((row) => row.id), signal)).filter((row) => sessionRowMatchesQuery(row, options.query));
42707
43394
  };
42708
43395
  /**
42709
43396
  * Delete one session subtree (/delete, codex semantics: subagent threads go
@@ -42720,9 +43407,12 @@ async function run(ctx, startup, io) {
42720
43407
  * Backends without a derivable artifact (non-JSONL) refuse the WHOLE
42721
43408
  * deletion here — no file has been touched yet, so a backend or layout
42722
43409
  * surprise can never strand a half-deleted subtree.
42723
- * 3. Artifacts are removed children-first: only an I/O error mid-delete
42724
- * can stop it short (reported with removed/total counts), leaving the
42725
- * shallowest lineage intact.
43410
+ * 3. Acquire every node's public persistence write handle before touching
43411
+ * files. The JSONL backend holds its cross-process kernel lease for each
43412
+ * handle, so another terminal's live session refuses the whole deletion.
43413
+ * 4. Artifacts are removed children-first while every lease remains held:
43414
+ * only an I/O error mid-delete can stop it short (reported with
43415
+ * removed/total counts), leaving the shallowest lineage intact.
42726
43416
  *
42727
43417
  * @param id - the root session id to delete.
42728
43418
  * @returns the outcome line for the panel/notice.
@@ -42734,7 +43424,7 @@ async function run(ctx, startup, io) {
42734
43424
  const plan = planSessionDeletion(records, id);
42735
43425
  if (!plan.ok) return plan.reason;
42736
43426
  const root = jsonlSessionRoot(persistence);
42737
- if (root === void 0) return "session backend exposes no deletable artifact (deletion is unsupported on this backend)";
43427
+ if (root === void 0 || persistence === void 0) return "session backend exposes no deletable artifact (deletion is unsupported on this backend)";
42738
43428
  const byId = new Map(records.map((record) => [record.header.id, record]));
42739
43429
  const dirs = /* @__PURE__ */ new Map();
42740
43430
  for (const node of plan.nodes) {
@@ -42744,7 +43434,15 @@ async function run(ctx, startup, io) {
42744
43434
  if (dir === void 0) return `refusing to delete: unexpected artifact layout for ${node.id.slice(-12)}`;
42745
43435
  dirs.set(node.id, dir);
42746
43436
  }
43437
+ let leases;
43438
+ try {
43439
+ leases = await acquireSessionDeletionLeases(persistence, plan.nodes.map((node) => node.id));
43440
+ } catch (error) {
43441
+ if (error instanceof SessionAlreadyOwnedError) return `cannot delete ${error.sessionId.slice(-12)} — it is open in this or another process`;
43442
+ return `cannot safely lock sessions for deletion: ${error instanceof Error ? error.message : String(error)}`;
43443
+ }
42747
43444
  let removed = 0;
43445
+ let outcome;
42748
43446
  for (const node of plan.nodes) {
42749
43447
  const dir = dirs.get(node.id);
42750
43448
  try {
@@ -42756,10 +43454,17 @@ async function run(ctx, startup, io) {
42756
43454
  }).catch(() => {});
42757
43455
  removed += 1;
42758
43456
  } catch (error) {
42759
- return `delete failed for ${node.id.slice(-12)} after ${removed} of ${plan.nodes.length}: ${error instanceof Error ? error.message : String(error)}`;
43457
+ outcome = `delete failed for ${node.id.slice(-12)} after ${removed} of ${plan.nodes.length}: ${error instanceof Error ? error.message : String(error)}`;
43458
+ break;
42760
43459
  }
42761
43460
  }
42762
- return `deleted ${removed} session${removed === 1 ? "" : "s"}`;
43461
+ outcome ??= `deleted ${removed} session${removed === 1 ? "" : "s"}`;
43462
+ try {
43463
+ await releaseSessionDeletionLeases(leases);
43464
+ } catch (error) {
43465
+ return `${outcome}; failed to release deletion locks: ${error instanceof Error ? error.message : String(error)}`;
43466
+ }
43467
+ return outcome;
42763
43468
  };
42764
43469
  const loadSessionTranscript = async (id, signal) => {
42765
43470
  if (sessionQuery === void 0) throw new Error("session query is unavailable in this profile");
@@ -42859,7 +43564,10 @@ async function run(ctx, startup, io) {
42859
43564
  epoch += 1;
42860
43565
  if (previous === void 0) {
42861
43566
  setTimeout(() => {
42862
- bridge.notify(`${next.resumed ? "resumed" : "created"} ${next.session.id.slice(-12)} · mode ${next.mode}`);
43567
+ bridge.notify(t(next.resumed ? "notice.sessionResumed" : "notice.sessionCreated", {
43568
+ id: next.session.id.slice(-12),
43569
+ mode: next.mode
43570
+ }));
42863
43571
  }, 0);
42864
43572
  return;
42865
43573
  }
@@ -42867,31 +43575,39 @@ async function run(ctx, startup, io) {
42867
43575
  try {
42868
43576
  await sessions.flush(previous.session);
42869
43577
  } catch (error) {
42870
- cleanupWarning = `previous session flush failed: ${error instanceof Error ? error.message : String(error)}`;
43578
+ cleanupWarning = t("notice.flushFailed", { message: error instanceof Error ? error.message : String(error) });
42871
43579
  }
42872
43580
  try {
42873
43581
  await previous.handle.dispose();
42874
43582
  } catch (error) {
42875
- cleanupWarning = `${cleanupWarning === void 0 ? "" : `${cleanupWarning}; `}previous agent release failed: ${error instanceof Error ? error.message : String(error)}`;
42876
- }
42877
- bridge.notify(cleanupWarning === void 0 ? `${next.resumed ? "resumed" : "created"} ${next.session.id.slice(-12)} · mode ${next.mode}` : `switched to ${next.session.id.slice(-12)}, but ${cleanupWarning}`, cleanupWarning === void 0 ? "info" : "warning");
43583
+ const release = t("notice.agentReleaseFailed", { message: error instanceof Error ? error.message : String(error) });
43584
+ cleanupWarning = cleanupWarning === void 0 ? release : `${cleanupWarning}; ${release}`;
43585
+ }
43586
+ const shortId = next.session.id.slice(-12);
43587
+ bridge.notify(cleanupWarning === void 0 ? t(next.resumed ? "notice.sessionResumed" : "notice.sessionCreated", {
43588
+ id: shortId,
43589
+ mode: next.mode
43590
+ }) : t("notice.sessionSwitchedDirty", {
43591
+ id: shortId,
43592
+ detail: cleanupWarning
43593
+ }), cleanupWarning === void 0 ? "info" : "warning");
42878
43594
  });
42879
43595
  };
42880
43596
  const switchQueue = new SessionSwitchQueue(async (request) => {
42881
43597
  if (!quitting) await activate(request.target);
42882
- }, (error) => bridge.notify(`session switch failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
43598
+ }, (error) => bridge.notify(t("notice.sessionSwitchFailed", { message: error instanceof Error ? error.message : String(error) }), "error"));
42883
43599
  const requestSwitch = (request) => {
42884
43600
  if (session === void 0) {
42885
43601
  activate(request.target).catch((error) => {
42886
- bridge.notify(`session switch failed: ${error instanceof Error ? error.message : String(error)}`, "error");
43602
+ bridge.notify(t("notice.sessionSwitchFailed", { message: error instanceof Error ? error.message : String(error) }), "error");
42887
43603
  });
42888
43604
  return;
42889
43605
  }
42890
43606
  if (request.target.sessionId === session.id) {
42891
- bridge.notify("that session is already active", "warning");
43607
+ bridge.notify(t("notice.alreadyActive"), "warning");
42892
43608
  return;
42893
43609
  }
42894
- if (switchQueue.request(agent, request) === "queued") bridge.notify(`will switch to ${request.label} when the current turn finishes · /resume cancel to abort`);
43610
+ if (switchQueue.request(agent, request) === "queued") bridge.notify(t("notice.switchQueued", { label: request.label }));
42895
43611
  };
42896
43612
  const resolveResumeId = async (wanted) => {
42897
43613
  if (wanted === "") throw new Error("usage: /resume <id|prefix>");
@@ -42915,7 +43631,7 @@ async function run(ctx, startup, io) {
42915
43631
  },
42916
43632
  label: id.slice(-12)
42917
43633
  });
42918
- }, (error) => bridge.notify(`resume failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
43634
+ }, (error) => bridge.notify(t("notice.resumeFailed", { message: error instanceof Error ? error.message : String(error) }), "error"));
42919
43635
  };
42920
43636
  const createSession = (mode) => {
42921
43637
  if (session === void 0) {
@@ -42964,7 +43680,7 @@ async function run(ctx, startup, io) {
42964
43680
  };
42965
43681
  const forkSession = (argument) => {
42966
43682
  if (session === void 0 || active === void 0) {
42967
- bridge.notify("no session yet - submit a message to start", "warning");
43683
+ bridge.notify(t("notice.noSessionYet"), "warning");
42968
43684
  return;
42969
43685
  }
42970
43686
  try {
@@ -42986,12 +43702,12 @@ async function run(ctx, startup, io) {
42986
43702
  label: id.slice(-12)
42987
43703
  });
42988
43704
  } catch (error) {
42989
- bridge.notify(`fork failed: ${error instanceof Error ? error.message : String(error)}`, "error");
43705
+ bridge.notify(t("notice.forkFailed", { message: error instanceof Error ? error.message : String(error) }), "error");
42990
43706
  }
42991
43707
  };
42992
43708
  const switchSession = (row) => {
42993
43709
  if (!row.resumable) {
42994
- bridge.notify("subagent conversations are read-only", "warning");
43710
+ bridge.notify(t("notice.subagentsReadOnly"), "warning");
42995
43711
  return;
42996
43712
  }
42997
43713
  requestSwitch({
@@ -43137,36 +43853,42 @@ async function run(ctx, startup, io) {
43137
43853
  };
43138
43854
  mountRef.current = io.mount(appElement());
43139
43855
  if (startup.prompt !== void 0 || (startup.images?.length ?? 0) > 0) {
43140
- if ((startup.images?.length ?? 0) > 0) bridge.notify(`processing ${startup.images.length} startup image${startup.images.length === 1 ? "" : "s"}…`);
43856
+ if ((startup.images?.length ?? 0) > 0) bridge.notify(t("notice.startupImages", {
43857
+ count: startup.images.length,
43858
+ plural: startup.images.length === 1 ? "" : "s"
43859
+ }));
43141
43860
  inputGate.run(async (deliver) => {
43142
43861
  const images = await saveImagePaths(startup.images ?? [], ctx.get("attachments"));
43143
- if (images.length > 0) bridge.notify(`${images.length} startup image${images.length === 1 ? "" : "s"} attached`);
43862
+ if (images.length > 0) bridge.notify(t("notice.startupImagesAttached", {
43863
+ count: images.length,
43864
+ plural: images.length === 1 ? "" : "s"
43865
+ }));
43144
43866
  deliver({
43145
43867
  text: startup.prompt ?? "",
43146
43868
  mode: "followup",
43147
43869
  images
43148
43870
  });
43149
43871
  }).catch((error) => {
43150
- bridge.notify(`initial prompt failed: ${error instanceof Error ? error.message : String(error)}`, "error");
43872
+ bridge.notify(t("notice.initialPromptFailed", { message: error instanceof Error ? error.message : String(error) }), "error");
43151
43873
  });
43152
43874
  }
43153
43875
  async function copyLastResponse() {
43154
43876
  const text = latestAssistantText(store.getView());
43155
- if (text === void 0) return "nothing to copy yet";
43877
+ if (text === void 0) return t("notice.copyEmpty");
43156
43878
  await copyText(text);
43157
- return "copied latest response";
43879
+ return t("notice.copied");
43158
43880
  }
43159
43881
  if (statuslineWarning !== void 0) setTimeout(() => {
43160
- bridge.notify("statusline config unreadable, using defaults: " + statuslineWarning, "warning");
43882
+ bridge.notify(t("notice.statuslineConfigUnreadable", { message: statuslineWarning }), "warning");
43161
43883
  }, 50);
43162
43884
  if (languageWarning !== void 0) setTimeout(() => {
43163
43885
  bridge.notify(t("notice.languageConfigUnreadable", { message: languageWarning }), "warning");
43164
43886
  }, 0);
43165
43887
  if (themeWarning !== void 0) setTimeout(() => {
43166
- bridge.notify("theme config unreadable, using dark: " + themeWarning, "warning");
43888
+ bridge.notify(t("notice.themeConfigUnreadable", { message: themeWarning }), "warning");
43167
43889
  }, 50);
43168
43890
  if (animationsWarning !== void 0) setTimeout(() => {
43169
- bridge.notify("animations config unreadable, animations stay on: " + animationsWarning, "warning");
43891
+ bridge.notify(t("notice.animationsConfigUnreadable", { message: animationsWarning }), "warning");
43170
43892
  }, 50);
43171
43893
  resolveEditorKeysStartupHint(editorKeysEnv).then((hint) => {
43172
43894
  if (hint === void 0) return;