lark-remote 0.5.6 → 0.5.8

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.
Files changed (85) hide show
  1. package/CHANGELOG.md +46 -6
  2. package/README.en.md +2 -2
  3. package/README.md +2 -2
  4. package/dist/bridge/index.d.ts +63 -1
  5. package/dist/bridge/index.js +147 -14
  6. package/dist/bridge/queue-manager.js +41 -58
  7. package/dist/card/approval-render.js +2 -1
  8. package/dist/card/bash-card-session.d.ts +0 -1
  9. package/dist/card/bash-card-session.js +0 -8
  10. package/dist/card/bash-renderer.js +2 -1
  11. package/dist/card/card-budget.d.ts +37 -0
  12. package/dist/card/card-budget.js +143 -24
  13. package/dist/card/card-session.d.ts +3 -13
  14. package/dist/card/card-session.js +4 -18
  15. package/dist/card/card-shared.d.ts +11 -0
  16. package/dist/card/card-shared.js +19 -13
  17. package/dist/card/run-card-session.d.ts +0 -1
  18. package/dist/card/run-card-session.js +0 -6
  19. package/dist/card/run-renderer.d.ts +18 -0
  20. package/dist/card/run-renderer.js +330 -62
  21. package/dist/card/run-state.d.ts +26 -0
  22. package/dist/card/run-state.js +45 -0
  23. package/dist/card/text-truncate.d.ts +3 -5
  24. package/dist/card/text-truncate.js +3 -5
  25. package/dist/card/time.d.ts +14 -0
  26. package/dist/card/time.js +30 -0
  27. package/dist/card/tool-render.d.ts +15 -2
  28. package/dist/card/tool-render.js +42 -82
  29. package/dist/common/text-width.d.ts +24 -0
  30. package/dist/common/text-width.js +93 -0
  31. package/dist/common/tool-summary.d.ts +19 -0
  32. package/dist/common/tool-summary.js +171 -0
  33. package/dist/common/turn-failure.d.ts +13 -0
  34. package/dist/common/turn-failure.js +18 -0
  35. package/dist/config/index.d.ts +1 -1
  36. package/dist/config/index.js +1 -1
  37. package/dist/connector/index.js +1 -1
  38. package/dist/index.js +66 -13
  39. package/dist/order/index.d.ts +2 -0
  40. package/dist/order/index.js +2 -0
  41. package/dist/platform/command.js +1 -1
  42. package/dist/platform/shell.d.ts +2 -2
  43. package/dist/platform/spawn.d.ts +1 -1
  44. package/dist/platform/spawn.js +2 -2
  45. package/dist/platform/types.d.ts +1 -1
  46. package/dist/router/card-helpers.d.ts +18 -16
  47. package/dist/router/card-helpers.js +88 -46
  48. package/dist/router/config/codex.js +2 -3
  49. package/dist/router/config/common/model-patch.d.ts +8 -0
  50. package/dist/router/config/common/model-patch.js +13 -0
  51. package/dist/router/config/common/render.js +30 -77
  52. package/dist/router/config/dsh.js +2 -1
  53. package/dist/router/config/kimi.js +3 -3
  54. package/dist/router/index.d.ts +9 -11
  55. package/dist/router/index.js +165 -356
  56. package/dist/router/order-exec-dispatch.js +2 -1
  57. package/dist/runner/claude/session.d.ts +1 -1
  58. package/dist/runner/claude/session.js +3 -3
  59. package/dist/runner/common/defaults.d.ts +2 -0
  60. package/dist/runner/common/defaults.js +3 -0
  61. package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
  62. package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
  63. package/dist/runner/common/jsonrpc/transport.js +3 -3
  64. package/dist/runner/common/spawning-runner.d.ts +1 -1
  65. package/dist/runner/common/spawning-runner.js +2 -2
  66. package/dist/runner/dsh/auth.js +2 -1
  67. package/dist/runner/dsh/client.d.ts +100 -3
  68. package/dist/runner/dsh/client.js +290 -6
  69. package/dist/runner/dsh/exit-cancel.d.ts +32 -0
  70. package/dist/runner/dsh/exit-cancel.js +72 -0
  71. package/dist/runner/dsh/inflight.d.ts +62 -0
  72. package/dist/runner/dsh/inflight.js +140 -0
  73. package/dist/runner/dsh/runner.d.ts +90 -4
  74. package/dist/runner/dsh/runner.js +436 -18
  75. package/dist/runner/dsh/translator.d.ts +1 -1
  76. package/dist/runner/dsh/translator.js +103 -1
  77. package/dist/runner/dsh/types.d.ts +60 -0
  78. package/dist/runner/types.d.ts +95 -2
  79. package/dist/runner/types.js +9 -0
  80. package/dist/session/claude/session-index.d.ts +0 -4
  81. package/dist/session/claude/session-index.js +0 -8
  82. package/dist/session/claude/sessions.js +2 -2
  83. package/dist/session/dsh/sessions.js +157 -17
  84. package/dist/update/install.js +1 -1
  85. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,12 +5,15 @@ import { parseCliArgs, resolveConfigDir, setConfigDir, printHelp, printVersion,
5
5
  import { FeishuConnector } from './connector/index.js';
6
6
  import { ClaudeRunner, CodexAppServerRunner, OpencodeAcpRunner, PiRpcRunner, KimiAcpRunner, DshRunner, } from './runner/index.js';
7
7
  import { AgentRegistry } from './runner/registry.js';
8
+ import { AGENT_DISPLAY_NAMES, AGENT_KINDS } from './runner/types.js';
8
9
  import { probeAllAgents } from './runner/probe.js';
9
10
  import { warmCodexCatalogCache } from './config/codex-config.js';
10
11
  import { SessionReaderRegistry, SessionStore } from './session/index.js';
11
12
  import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, DshSessionReader, } from './session/index.js';
12
13
  import { CommandRouter, isImmediateAction, DIRECT_RETURN_CMDS, SESSION_MUTATING_ACTION_CMDS, COMPACT_ACTION_CMDS, } from './router/index.js';
13
14
  import { DshDaemon } from './runner/dsh/daemon.js';
15
+ import { DshClient } from './runner/dsh/client.js';
16
+ import { DshInflightStore, dshInflightPath, reconcileInflightDshRuns, } from './runner/dsh/inflight.js';
14
17
  import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
15
18
  import { buildCardActionFullValue } from './router/card-action-payload.js';
16
19
  import { actionFeedbackText } from './router/card-action-feedback.js';
@@ -29,7 +32,7 @@ import { WorkspaceStore } from './workspace/index.js';
29
32
  import { InboundTurnAssembler } from './inbound/turn-assembler.js';
30
33
  import { InboundAuthorizer } from './inbound/authorize.js';
31
34
  import { stripPlaceholders } from './inbound/placeholder.js';
32
- import { buildSessionHistoryCard } from './router/card-helpers.js';
35
+ import { buildSessionHistoryCard, pickResumeCardEvents } from './router/card-helpers.js';
33
36
  import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
34
37
  import path from 'node:path';
35
38
  import fs from 'node:fs';
@@ -43,16 +46,18 @@ function isStopCommand(text) {
43
46
  return t === '/stop' || t === '/t';
44
47
  }
45
48
  /** Send auto-resume card for a restored session */
46
- async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false) {
49
+ async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false, stillRunning = false) {
47
50
  if (!contact.userId)
48
51
  return;
49
52
  const recipient = contact.chatId ?? contact.userId;
50
53
  if (!recipient)
51
54
  return;
52
- // Limit history events to prevent card size exceeded error (11310)
55
+ // Limit history events to prevent card size exceeded error (11310).
56
+ // 窗口按"最后 N 条"截,但强制保住最后一条用户消息与最后一条 assistant 正文,
57
+ // 否则工具往返会把它俩一起挤出卡(2026-10-19 恢复卡只剩 5 个 bash 面板)。
53
58
  const MAX_HISTORY_EVENTS = 5;
54
- const eventsToShow = state.events.slice(-MAX_HISTORY_EVENTS);
55
- const hiddenCount = state.events.length - MAX_HISTORY_EVENTS;
59
+ const eventsToShow = pickResumeCardEvents(state.events, MAX_HISTORY_EVENTS);
60
+ const hiddenCount = state.events.length - eventsToShow.length;
56
61
  // Action buttons: Compact (runCompact-capable runner) + new session.
57
62
  const agentKind = state.agentKind ?? 'claude';
58
63
  const actionButtons = [];
@@ -73,6 +78,7 @@ async function sendAutoResumeCard(connector, contact, cwd, session, state, compa
73
78
  headerText: `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`,
74
79
  title: `🔁 自动恢复会话 · ${agentDisplayName(agentKind)}`,
75
80
  hiddenCount,
81
+ stillRunning,
76
82
  actions: actionButtons,
77
83
  });
78
84
  await connector.sendWithRetry(recipient, { card });
@@ -300,6 +306,9 @@ function initializeRunner(config, configDir, cliArgs) {
300
306
  provider: dshConf?.provider,
301
307
  model: dshConf?.model,
302
308
  reasoningEffort: dshConf?.reasoningEffort,
309
+ // Durable markers for turns that outlive this process; reconciled on the
310
+ // next startup (see reconcileInflightDshRuns).
311
+ inflightPath: dshInflightPath(configDir),
303
312
  });
304
313
  });
305
314
  sessionReaderRegistry.register('dsh', dshSessionReader);
@@ -308,13 +317,10 @@ function initializeRunner(config, configDir, cliArgs) {
308
317
  getLogger().warn(`[dsh] startup probe failed: ${error.message}`);
309
318
  });
310
319
  }
311
- // 2026-07-17: 注册所有 agent 的显示名,实现单点真相
312
- agentRegistry.registerDisplayName('claude', 'Claude');
313
- agentRegistry.registerDisplayName('codex', 'Codex');
314
- agentRegistry.registerDisplayName('opencode', 'Opencode');
315
- agentRegistry.registerDisplayName('pi', 'Pi');
316
- agentRegistry.registerDisplayName('kimi', 'Kimi');
317
- agentRegistry.registerDisplayName('dsh', 'DSH');
320
+ // 显示名单源在 runner/types.ts 的 AGENT_DISPLAY_NAMES(启动注册与回退共用)
321
+ for (const kind of AGENT_KINDS) {
322
+ agentRegistry.registerDisplayName(kind, AGENT_DISPLAY_NAMES[kind]);
323
+ }
318
324
  // 设置全局 registry,供 agentDisplayName 等全局函数使用
319
325
  AgentRegistry.setGlobalInstance(agentRegistry);
320
326
  // Probe agent CLI availability (fire-and-forget, populates cache for /config card).
@@ -834,6 +840,33 @@ async function main() {
834
840
  })();
835
841
  }
836
842
  }
843
+ // 上次进程可能死在 DSH turn 中途:清掉残留的待执行 prompt,并对仍在跑的
844
+ // turn 提示 owner(不自动 cancel——它可能是 Web UI 的 turn)。清理不依赖是否
845
+ // 已绑定;没有收件人时只是不发提示。
846
+ const dshConf = getAgentConfig(config, 'dsh');
847
+ if (dshConf) {
848
+ const contact = startupContactStore.getContact();
849
+ const recipient = contact?.chatId ?? contact?.userId;
850
+ void reconcileInflightDshRuns({
851
+ store: new DshInflightStore(dshInflightPath(configDir)),
852
+ client: new DshClient(dshConf.host ?? 'http://127.0.0.1:3080', {
853
+ dshHome: dshConf.dshHome,
854
+ token: dshConf.token,
855
+ credentialsFile: dshConf.credentialsFile,
856
+ // Host may be down: bound the probe instead of waiting the default 60s.
857
+ requestTimeoutMs: 3_000,
858
+ }),
859
+ ...(recipient
860
+ ? {
861
+ notify: async (text) => {
862
+ await connector.sendWithRetry(recipient, { text });
863
+ },
864
+ }
865
+ : {}),
866
+ }).catch((err) => {
867
+ logger.warn(`[startup] dsh inflight reconcile failed: ${err.message}`);
868
+ });
869
+ }
837
870
  // Auto-restore: resume the persisted sessionId if available
838
871
  const restoredContact = startupContactStore.getContact();
839
872
  if (restoredContact?.userId) {
@@ -845,6 +878,26 @@ async function main() {
845
878
  const startupReader = sessionReaderRegistry.get(config.defaultAgent);
846
879
  sessionStore.setSessionIdAndCwd(restoredContact.userId, config.defaultAgent, sessionIdToResume, restoredCwd);
847
880
  const content = startupReader.readSessionContent(sessionIdToResume, restoredCwd);
881
+ // 该会话可能仍有在途 turn(上一个实例退出/崩溃后留在常驻 host 里)。
882
+ // 不取消、不接管,但必须在卡上说明,否则用户会把它当"卡住了"。
883
+ //
884
+ // 只有 DSH reader 的 isSessionActive() 是服务端的 running 事实。其他
885
+ // reader 的同名方法只表示"最近一段时间有写入",拿它当 turn 在跑会在
886
+ // 每次自动恢复时误报。
887
+ const stillRunning = config.defaultAgent === 'dsh'
888
+ ? (() => {
889
+ try {
890
+ return startupReader.isSessionActive(sessionIdToResume, restoredCwd);
891
+ }
892
+ catch (err) {
893
+ getLogger().warn(`[startup] isSessionActive failed: ${err.message}`);
894
+ return false;
895
+ }
896
+ })()
897
+ : false;
898
+ if (stillRunning) {
899
+ getLogger().warn(`[startup] session ${sessionIdToResume} still has a running turn; auto-resume card is a snapshot`);
900
+ }
848
901
  // 完整透传 usage(含 inputTokens/outputTokens/totalTokens),
849
902
  // 让 formatUsageStats 走真实值路径,与终态卡片//resume 卡片口径一致。
850
903
  const usage = content.usage;
@@ -862,7 +915,7 @@ async function main() {
862
915
  agentKind: config.defaultAgent,
863
916
  },
864
917
  // Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
865
- bridge.hasRunCompact(restoredCwd, config.defaultAgent));
918
+ bridge.hasRunCompact(restoredCwd, config.defaultAgent), stillRunning);
866
919
  }
867
920
  catch (err) {
868
921
  getLogger().warn('[startup] sendAutoResumeCard failed:', err);
@@ -1,3 +1,5 @@
1
+ /** 指令已被删除/不存在时的统一用户提示。 */
2
+ export declare const ORDER_NOT_FOUND_TEXT = "\u6307\u4EE4\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664";
1
3
  export interface OrderEntry {
2
4
  id: string;
3
5
  text: string;
@@ -2,6 +2,8 @@ import path from 'node:path';
2
2
  import os from 'node:os';
3
3
  import { atomicWriteJson } from '../persistence/atomic-write.js';
4
4
  import { loadJsonFile } from '../persistence/load-json-file.js';
5
+ /** 指令已被删除/不存在时的统一用户提示。 */
6
+ export const ORDER_NOT_FOUND_TEXT = '指令不存在或已被删除';
5
7
  const MAX_TEXT_LENGTH = 200;
6
8
  const MAX_ORDERS = 50;
7
9
  /** 别名名称规则:`[A-Za-z_][A-Za-z0-9_]*`,≤20 字符,不能数字开头。 */
@@ -5,7 +5,7 @@
5
5
  * 自更新安装源检测 update/install.ts)。**执行**统一走 platform/spawn.ts 的
6
6
  * cross-spawn 包装(PATHEXT 解析 + .cmd 经 cmd.exe 受控执行),本模块不再
7
7
  * 解析 .cmd/.bat 垫片内容——垫片格式是 npm/pnpm 的上游实现细节(历史变过、
8
- * pnpm 生成形态不同),手写解析器等于耦合该细节(v2 §4.2)。
8
+ * pnpm 生成形态不同),手写解析器等于耦合该细节。
9
9
  *
10
10
  * posix:PATH 目录顺序 + X_OK(现状语义迁移)。
11
11
  * win32:PATH 目录顺序 × PATHEXT(.exe/.cmd/.bat) 解析,.cmd 命中即报告垫片
@@ -8,7 +8,7 @@
8
8
  * 由调用方转成明确错误卡(提示需要 Git Bash)。
9
9
  *
10
10
  * kimi ACP 的 terminal/create 固定 `bash -c`,因此 Git Bash 不是 `!` 的专属
11
- * 需求,而是 kimi agent 在 Windows 上可用的前置条件(§4.4)。
11
+ * 需求,而是 kimi agent 在 Windows 上可用的前置条件。
12
12
  */
13
13
  import type { ChildProcess, SpawnOptions } from 'node:child_process';
14
14
  import { type ResolveOptions, type LaunchSpec } from './command.js';
@@ -31,7 +31,7 @@ export interface ShellBackend {
31
31
  * - posix:原样透传(与迁移前 `spawnProcess(command, args, ...)` 逐字一致);
32
32
  * - win32:首 token 是 shell(bash/sh/zsh/dash)时按 Git Bash 解析,缺失则抛
33
33
  * {@link ShellUnavailableError}——kimi agent 在 Windows 上可用的前置条件
34
- * (§4.4)。其余命令交给 cross-spawn 做 PATH/PATHEXT 解析,不做命令翻译。
34
+ * 。其余命令交给 cross-spawn 做 PATH/PATHEXT 解析,不做命令翻译。
35
35
  */
36
36
  spawnArgv(file: string, args: readonly string[], opts?: {
37
37
  cwd?: string;
@@ -35,7 +35,7 @@ export declare function mergeProcessEnv(base?: NodeJS.ProcessEnv, overrides?: No
35
35
  */
36
36
  export declare function useDetachedProcessGroup(platform?: NodeJS.Platform): boolean;
37
37
  /**
38
- * win32 command-not-found 行嗅探(§4.4):命中即视为「命令缺失」失败而非
38
+ * win32 command-not-found 行嗅探:命中即视为「命令缺失」失败而非
39
39
  * 普通非零退出。platform 注入使其在任意宿主可测。
40
40
  */
41
41
  export declare function isWindowsCommandNotFoundLine(line: string, platform?: NodeJS.Platform): boolean;
@@ -10,7 +10,7 @@
10
10
  * 必须经它合并(先按不区分大小写删旧键再设新值);
11
11
  * - isWindowsCommandNotFoundLine:win32 经 cmd 垫片启动失败不是 spawn 的
12
12
  * error 事件 ENOENT,而是子进程往 stderr 打
13
- * "'xxx' is not recognized as an internal or external command"(§4.4)。
13
+ * "'xxx' is not recognized as an internal or external command"(见本文件 command-not-found 嗅探)。
14
14
  */
15
15
  import crossSpawn from 'cross-spawn';
16
16
  import { currentPlatform, isWin32 } from './select.js';
@@ -68,7 +68,7 @@ export function useDetachedProcessGroup(platform = currentPlatform) {
68
68
  return !isWin32(platform);
69
69
  }
70
70
  /**
71
- * win32 command-not-found 行嗅探(§4.4):命中即视为「命令缺失」失败而非
71
+ * win32 command-not-found 行嗅探:命中即视为「命令缺失」失败而非
72
72
  * 普通非零退出。platform 注入使其在任意宿主可测。
73
73
  */
74
74
  export function isWindowsCommandNotFoundLine(line, platform = currentPlatform) {
@@ -2,7 +2,7 @@
2
2
  * 平台 seam 纯类型。
3
3
  */
4
4
  /**
5
- * spawn 发现规格 —— resolveExecutable 的产出(v2 §4.3,仅探测/诊断用)。
5
+ * spawn 发现规格 —— resolveExecutable 的产出(仅探测/诊断用)。
6
6
  * v2 收窄:.cmd 垫片命中即报告垫片路径本身,不再解析内容定位入口;
7
7
  * 执行统一走 platform/spawn.ts 的 cross-spawn 包装。
8
8
  */
@@ -10,22 +10,6 @@ import { markdownDiv } from '../card/collapsible.js';
10
10
  import type { AgentSessionContentEvent } from '../runner/index.js';
11
11
  import { formatUsageStats } from './usage-format.js';
12
12
  type SessionUsageLike = Parameters<typeof formatUsageStats>[0];
13
- /**
14
- * Build a collapsible panel for a single session content event.
15
- *
16
- * Header carries a type emoji (👤 user / 🤖 assistant·text / 💭 thinking /
17
- * 🔧 tool_use / 🟢🔴 tool_result) with optional timestamp.
18
- * Body: the event's content (markdown).
19
- *
20
- * The last `tailExpandedCount` events are expanded by default so the user
21
- * sees the most recent context without clicking; older events are collapsed.
22
- *
23
- * @param ev - Session content event
24
- * @param index - Index in the events array
25
- * @param totalEvents - Total number of events
26
- * @param tailExpandedCount - How many events from the end to keep expanded (default 2)
27
- * @param agentKind - Agent type for display name (e.g. 'claude', 'pi'). Defaults to 'claude'.
28
- */
29
13
  export declare function sessionEventPanel(ev: AgentSessionContentEvent, index: number, totalEvents: number, tailExpandedCount?: number, agentKind?: string): object;
30
14
  export { markdownDiv };
31
15
  /** 页码输入非法时的统一提示文案(/ls /ws /resume /active /order 共用)。 */
@@ -115,6 +99,19 @@ export declare function searchBar(opts: {
115
99
  currentQuery?: string;
116
100
  extra?: Record<string, unknown>;
117
101
  }): object;
102
+ /**
103
+ * 选出 resume/历史卡该展示的事件。
104
+ *
105
+ * 旧口径是 `events.slice(-max)`:按**原始事件条数**截尾。工具往返(bash 调用 →
106
+ * job 登记 → 结果)一段就能把窗口占满,于是重返会话的用户真正需要的两条信息
107
+ * ——最后一句我说了什么、最后一句它答了什么——被挤出窗口。2026-10-19 实测:
108
+ * 自动恢复卡 5 个面板全是 bash 调用,一条用户消息、一条 assistant 正文都没有。
109
+ *
110
+ * 契约:先取最新 `max` 条,再把最后一条 `user` 与最后一条 `assistant`(正文)
111
+ * 不在窗口内时补进来——补位时淘汰最早的**非锚点**事件。输出保持时间顺序,
112
+ * 长度不超过 `max`。
113
+ */
114
+ export declare function pickResumeCardEvents(events: AgentSessionContentEvent[], max: number): AgentSessionContentEvent[];
118
115
  /**
119
116
  * Build a session-history resume card (shared by auto-restore, /resume <id>,
120
117
  * config-switch resume, and run-completion notification).
@@ -145,6 +142,11 @@ export declare function buildSessionHistoryCard(state: {
145
142
  emptyPlaceholder?: string;
146
143
  /** Hidden earlier-events count (auto-restore only). */
147
144
  hiddenCount?: number;
145
+ /**
146
+ * 该会话此刻仍有在途 turn(仅 DSH 的 `isSessionActive` 是服务端 running
147
+ * 事实)。为 true 时卡上显式提示"这是历史快照、实时输出不会落在这张卡上"。
148
+ */
149
+ stillRunning?: boolean;
148
150
  /** Action buttons to render (already built, protocol-specific). */
149
151
  actions?: object[];
150
152
  /** Optional card header template (e.g. 'green' for completion cards). */
@@ -6,9 +6,10 @@
6
6
  * specific builders (e.g. `sessionEventPanel`) so `router/index.ts` can fold
7
7
  * long session histories into collapsible panels without duplicating logic.
8
8
  */
9
- import { agentDisplayName } from '../card/card-shared.js';
9
+ import { agentDisplayName, cardButton } from '../card/card-shared.js';
10
10
  import { collapsibleMarkdownPanel, markdownDiv } from '../card/collapsible.js';
11
- import { formatTimestamp } from '../card/time.js';
11
+ import { formatMonthDayClock } from '../card/time.js';
12
+ import { displayWidth, truncateMiddleToDisplayWidth } from '../common/text-width.js';
12
13
  import { formatUsageStats } from './usage-format.js';
13
14
  /**
14
15
  * Build a collapsible panel for a single session content event.
@@ -26,10 +27,21 @@ import { formatUsageStats } from './usage-format.js';
26
27
  * @param tailExpandedCount - How many events from the end to keep expanded (default 2)
27
28
  * @param agentKind - Agent type for display name (e.g. 'claude', 'pi'). Defaults to 'claude'.
28
29
  */
30
+ /**
31
+ * resume / 历史卡片的事件面板标题宽度上限(半角单位)。
32
+ *
33
+ * 与 run 卡的工具面板同口径:手机上一行约 42 个半角宽,标题超了会折行。
34
+ */
35
+ const SESSION_PANEL_TITLE_MAX = 42;
29
36
  export function sessionEventPanel(ev, index, totalEvents, tailExpandedCount = 2, agentKind = 'claude') {
30
- const label = eventLabel(ev, agentKind);
31
- const ts = formatTimestamp(ev.timestamp);
32
- const title = ts ? `${label} (${ts})` : label;
37
+ // reader 可以给事件一个"标题即摘要"的名字(工具调用:`🔧 bash — npm test`);
38
+ // 缺省用类型标签。标题连同时间戳一起收进一行显示宽度——手机上折叠面板只有
39
+ // 一行可用(2026-10-19 工具标题被长 URL 撑成两行那次)。
40
+ const label = ev.title ?? eventLabel(ev, agentKind);
41
+ const ts = formatMonthDayClock(ev.timestamp);
42
+ const suffix = ts ? ` (${ts})` : '';
43
+ const labelMax = Math.max(8, SESSION_PANEL_TITLE_MAX - displayWidth(suffix));
44
+ const title = `${truncateMiddleToDisplayWidth(label, labelMax)}${suffix}`;
33
45
  const expanded = index >= totalEvents - tailExpandedCount;
34
46
  return collapsibleMarkdownPanel({
35
47
  title,
@@ -130,18 +142,7 @@ export function paginationBar(opts) {
130
142
  width: 'auto',
131
143
  vertical_align: 'center',
132
144
  elements: [
133
- {
134
- tag: 'button',
135
- text: { tag: 'plain_text', content: prevText },
136
- type: 'default',
137
- size: 'small',
138
- behaviors: [
139
- {
140
- type: 'callback',
141
- value: { cmd: opts.cmd, offset: opts.offset - opts.pageSize, ...opts.extra },
142
- },
143
- ],
144
- },
145
+ cardButton(prevText, { cmd: opts.cmd, offset: opts.offset - opts.pageSize, ...opts.extra }, { type: 'default', size: 'small' }),
145
146
  ],
146
147
  });
147
148
  }
@@ -173,18 +174,7 @@ export function paginationBar(opts) {
173
174
  width: 'auto',
174
175
  vertical_align: 'center',
175
176
  elements: [
176
- {
177
- tag: 'button',
178
- text: { tag: 'plain_text', content: nextText },
179
- type: 'default',
180
- size: 'small',
181
- behaviors: [
182
- {
183
- type: 'callback',
184
- value: { cmd: opts.cmd, offset: opts.offset + opts.pageSize, ...opts.extra },
185
- },
186
- ],
187
- },
177
+ cardButton(nextText, { cmd: opts.cmd, offset: opts.offset + opts.pageSize, ...opts.extra }, { type: 'default', size: 'small' }),
188
178
  ],
189
179
  });
190
180
  }
@@ -252,23 +242,56 @@ export function searchBar(opts) {
252
242
  width: 'auto',
253
243
  vertical_align: 'center',
254
244
  elements: [
255
- {
256
- tag: 'button',
257
- text: { tag: 'plain_text', content: '清除筛选' },
258
- type: 'default',
259
- size: 'small',
260
- behaviors: [
261
- {
262
- type: 'callback',
263
- value: { cmd: opts.cmd, clearQuery: true, ...opts.extra },
264
- },
265
- ],
266
- },
245
+ cardButton('清除筛选', { cmd: opts.cmd, clearQuery: true, ...opts.extra }, { type: 'default', size: 'small' }),
267
246
  ],
268
247
  });
269
248
  }
270
249
  return { tag: 'column_set', columns };
271
250
  }
251
+ /**
252
+ * 选出 resume/历史卡该展示的事件。
253
+ *
254
+ * 旧口径是 `events.slice(-max)`:按**原始事件条数**截尾。工具往返(bash 调用 →
255
+ * job 登记 → 结果)一段就能把窗口占满,于是重返会话的用户真正需要的两条信息
256
+ * ——最后一句我说了什么、最后一句它答了什么——被挤出窗口。2026-10-19 实测:
257
+ * 自动恢复卡 5 个面板全是 bash 调用,一条用户消息、一条 assistant 正文都没有。
258
+ *
259
+ * 契约:先取最新 `max` 条,再把最后一条 `user` 与最后一条 `assistant`(正文)
260
+ * 不在窗口内时补进来——补位时淘汰最早的**非锚点**事件。输出保持时间顺序,
261
+ * 长度不超过 `max`。
262
+ */
263
+ export function pickResumeCardEvents(events, max) {
264
+ if (max <= 0)
265
+ return [];
266
+ if (events.length <= max)
267
+ return events.slice();
268
+ const picked = new Set();
269
+ for (let i = events.length - 1; i >= 0 && picked.size < max; i -= 1)
270
+ picked.add(i);
271
+ const lastIndexOf = (predicate) => {
272
+ for (let i = events.length - 1; i >= 0; i -= 1) {
273
+ const ev = events[i];
274
+ if (ev && predicate(ev))
275
+ return i;
276
+ }
277
+ return -1;
278
+ };
279
+ const anchors = [
280
+ lastIndexOf((ev) => ev.type === 'user'),
281
+ lastIndexOf((ev) => ev.type === 'assistant'),
282
+ ].filter((i) => i >= 0);
283
+ for (const anchor of anchors) {
284
+ if (picked.has(anchor))
285
+ continue;
286
+ // 腾位置时只淘汰非锚点:两个锚点之间不互相挤掉。
287
+ const victim = [...picked].filter((i) => !anchors.includes(i)).sort((a, b) => a - b)[0];
288
+ if (victim === undefined)
289
+ break;
290
+ picked.delete(victim);
291
+ picked.add(anchor);
292
+ }
293
+ return [...picked].sort((a, b) => a - b).map((i) => events[i]);
294
+ }
272
295
  /**
273
296
  * Build a session-history resume card (shared by auto-restore, /resume <id>,
274
297
  * config-switch resume, and run-completion notification).
@@ -283,20 +306,39 @@ export function buildSessionHistoryCard(state, opts) {
283
306
  const { displayTitle, aiTitle, recap, events, usage } = state;
284
307
  let header = opts.headerText;
285
308
  const sections = [];
309
+ const preview = (text) => (text.length > 200 ? text.slice(0, 197) + '...' : text);
286
310
  // Reader 层不再截断 displayTitle/summary;统一在消费侧这里截到 200 字符。
311
+ //
312
+ // `aiTitle` 与 `displayTitle` 是两种东西:前者是 agent 自己起的会话标题
313
+ // (DSH 的 projections.title / claude 的 ai-title),后者是最近一条用户输入。
314
+ // claude 的 displayTitle 本身就取 aiTitle,两者相等 → 保持旧的「AI 标题」
315
+ // 单段文案;DSH 两者都有(标题 + 末轮 prompt)→ 各占一段,别把用户那句话
316
+ // 挂在「AI 标题」下面(2026-10-21)。
287
317
  if (displayTitle) {
288
- const label = aiTitle ? 'AI 标题' : '最近输入';
289
- const preview = displayTitle.length > 200 ? displayTitle.slice(0, 197) + '...' : displayTitle;
290
- sections.push(`🏷️ **${label}**\n${preview}`);
318
+ if (aiTitle && aiTitle !== displayTitle) {
319
+ sections.push(`🏷️ **标题**\n${preview(aiTitle)}`);
320
+ sections.push(`✏️ **最近输入**\n${preview(displayTitle)}`);
321
+ }
322
+ else {
323
+ const label = aiTitle ? 'AI 标题' : '最近输入';
324
+ sections.push(`🏷️ **${label}**\n${preview(displayTitle)}`);
325
+ }
326
+ }
327
+ else if (aiTitle) {
328
+ sections.push(`🏷️ **AI 标题**\n${preview(aiTitle)}`);
291
329
  }
292
330
  if (recap) {
293
- const recapPreview = recap.length > 200 ? recap.slice(0, 197) + '...' : recap;
294
- sections.push(`📝 **Recap**\n${recapPreview}`);
331
+ sections.push(`📝 **Recap**\n${preview(recap)}`);
295
332
  }
296
333
  if (sections.length > 0) {
297
334
  header += '\n\n' + sections.join('\n\n──\n\n');
298
335
  }
299
336
  const elements = [markdownDiv(header), { tag: 'hr' }];
337
+ // 恢复时该会话仍有一个 turn 在跑(典型来源:上一个桥接实例退出/崩溃后,
338
+ // turn 留在常驻 host 里继续执行)。卡片只是快照,别让用户以为这就是全部。
339
+ if (opts.stillRunning) {
340
+ elements.push(markdownDiv('⚠️ **该会话仍有一个 turn 在运行**(可能来自已退出的实例):以下内容是历史快照,实时输出不会出现在这张卡上。'));
341
+ }
300
342
  if (opts.hiddenCount && opts.hiddenCount > 0) {
301
343
  elements.push(markdownDiv(`📜 还有 ${opts.hiddenCount} 个更早的事件未显示`));
302
344
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { loadCodexConfig, getReasoningEffortOptions, getDefaultReasoningEffort, } from '../../config/codex-config.js';
5
5
  import { DEFAULT_TURN_IDLE_TIMEOUT_MINUTES } from '../../config/index.js';
6
- import { resetModelPatch } from './common/model-patch.js';
6
+ import { pickMiddleValue, resetModelPatch } from './common/model-patch.js';
7
7
  export class CodexConfigBuilder {
8
8
  buildFields(displayConfig) {
9
9
  const fields = [];
@@ -116,8 +116,7 @@ export class CodexConfigBuilder {
116
116
  const isCurrentEffortValid = currentReasoningEffort && newModelSupportedEfforts.includes(currentReasoningEffort);
117
117
  if (isCurrentEffortValid)
118
118
  return null;
119
- const middle = newModelSupportedEfforts[Math.floor((newModelSupportedEfforts.length - 1) / 2)];
120
- const newEffort = middle ?? getDefaultReasoningEffort(newModel);
119
+ const newEffort = pickMiddleValue(newModelSupportedEfforts) ?? getDefaultReasoningEffort(newModel);
121
120
  return { key: 'agents.codex.reasoningEffort', value: newEffort };
122
121
  }
123
122
  }
@@ -19,3 +19,11 @@ export declare function resetModelPatch(modelKey: string, currentModel: string |
19
19
  key: string;
20
20
  value: unknown;
21
21
  } | null;
22
+ /**
23
+ * 取档位列表的中位值(切换模型后当前档位不受支持时的兜底)。
24
+ *
25
+ * 偶数长度的中位口径由调用方显式选择,避免各 builder 各写一套公式后漂移:
26
+ * - `lower`:`(len-1)/2`,kimi/codex 既有口径;
27
+ * - `upper`:`len/2`,DSH 需要偏高的一档(off/low → low)。
28
+ */
29
+ export declare function pickMiddleValue<T>(values: readonly T[], bias?: 'lower' | 'upper'): T | undefined;
@@ -22,4 +22,17 @@ export function resetModelPatch(modelKey, currentModel, newModelOptions) {
22
22
  }
23
23
  return null;
24
24
  }
25
+ /**
26
+ * 取档位列表的中位值(切换模型后当前档位不受支持时的兜底)。
27
+ *
28
+ * 偶数长度的中位口径由调用方显式选择,避免各 builder 各写一套公式后漂移:
29
+ * - `lower`:`(len-1)/2`,kimi/codex 既有口径;
30
+ * - `upper`:`len/2`,DSH 需要偏高的一档(off/low → low)。
31
+ */
32
+ export function pickMiddleValue(values, bias = 'lower') {
33
+ if (values.length === 0)
34
+ return undefined;
35
+ const index = bias === 'upper' ? Math.floor(values.length / 2) : Math.floor((values.length - 1) / 2);
36
+ return values[index];
37
+ }
25
38
  //# sourceMappingURL=model-patch.js.map