lark-remote 0.5.7 → 0.5.9

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 (57) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/bridge/index.d.ts +47 -7
  3. package/dist/bridge/index.js +300 -129
  4. package/dist/bridge/queue-manager.d.ts +6 -0
  5. package/dist/bridge/queue-manager.js +61 -63
  6. package/dist/card/approval-render.js +2 -1
  7. package/dist/card/bash-card-session.d.ts +0 -1
  8. package/dist/card/bash-card-session.js +0 -8
  9. package/dist/card/bash-renderer.js +2 -1
  10. package/dist/card/card-budget.js +15 -19
  11. package/dist/card/card-session.d.ts +3 -13
  12. package/dist/card/card-session.js +4 -18
  13. package/dist/card/card-shared.d.ts +11 -0
  14. package/dist/card/card-shared.js +23 -13
  15. package/dist/card/run-card-session.d.ts +0 -1
  16. package/dist/card/run-card-session.js +0 -6
  17. package/dist/card/run-renderer.d.ts +11 -10
  18. package/dist/card/run-renderer.js +198 -35
  19. package/dist/card/run-state.d.ts +29 -2
  20. package/dist/card/run-state.js +23 -4
  21. package/dist/card/text-truncate.d.ts +3 -5
  22. package/dist/card/text-truncate.js +3 -5
  23. package/dist/card/tool-render.js +7 -21
  24. package/dist/common/tool-summary.d.ts +8 -0
  25. package/dist/common/tool-summary.js +38 -20
  26. package/dist/common/turn-failure.d.ts +13 -0
  27. package/dist/common/turn-failure.js +18 -0
  28. package/dist/index.js +13 -14
  29. package/dist/order/index.d.ts +2 -0
  30. package/dist/order/index.js +2 -0
  31. package/dist/router/card-helpers.js +23 -42
  32. package/dist/router/config/codex.js +2 -3
  33. package/dist/router/config/common/model-patch.d.ts +8 -0
  34. package/dist/router/config/common/model-patch.js +13 -0
  35. package/dist/router/config/common/render.js +30 -77
  36. package/dist/router/config/dsh.js +2 -1
  37. package/dist/router/config/kimi.js +3 -3
  38. package/dist/router/index.d.ts +0 -11
  39. package/dist/router/index.js +119 -360
  40. package/dist/router/order-exec-dispatch.js +2 -1
  41. package/dist/router/usage-format.d.ts +5 -0
  42. package/dist/router/usage-format.js +1 -1
  43. package/dist/runner/common/defaults.d.ts +2 -0
  44. package/dist/runner/common/defaults.js +3 -0
  45. package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
  46. package/dist/runner/dsh/client.d.ts +15 -0
  47. package/dist/runner/dsh/client.js +28 -2
  48. package/dist/runner/dsh/goal-view.d.ts +36 -0
  49. package/dist/runner/dsh/goal-view.js +106 -0
  50. package/dist/runner/dsh/runner.js +51 -2
  51. package/dist/runner/index.d.ts +1 -1
  52. package/dist/runner/types.d.ts +59 -1
  53. package/dist/runner/types.js +9 -0
  54. package/dist/session/claude/session-index.d.ts +0 -4
  55. package/dist/session/claude/session-index.js +0 -8
  56. package/dist/session/dsh/sessions.js +58 -2
  57. package/package.json +1 -1
@@ -6,7 +6,7 @@ import { getConfigDir, getConfigValue, setConfigValue, setConfigValues, mapAgent
6
6
  import { syncAgentChoices } from '../runner/index.js';
7
7
  import { resolveDshCredentialsFile } from '../runner/dsh/auth.js';
8
8
  import { WorkspaceStore } from '../workspace/index.js';
9
- import { OrderStore } from '../order/index.js';
9
+ import { OrderStore, ORDER_NOT_FOUND_TEXT } from '../order/index.js';
10
10
  import { resolveAlias } from '../order/alias-resolve.js';
11
11
  import { diffConfig, setNestedValue } from './config-diff.js';
12
12
  import { AGENT_KINDS } from '../runner/types.js';
@@ -16,7 +16,7 @@ import { clampInt } from '../common/clamp.js';
16
16
  import { activeRunUsage } from './usage-format.js';
17
17
  import { markdownDiv, buildSessionHistoryCard, paginationBar, searchBar, parseJumpOffset, SEARCH_INPUT_NAME, PAGE_JUMP_INVALID_HINT, } from './card-helpers.js';
18
18
  import { MAX_FILE_UPLOAD_SIZE } from '../connector/file-limits.js';
19
- import { newSessionButton, resumeCompactButton, agentDisplayName } from '../card/card-shared.js';
19
+ import { cardButton, newSessionButton, resumeCompactButton, agentDisplayName, } from '../card/card-shared.js';
20
20
  import { displayName } from '../platform/path.js';
21
21
  import { atomicWrite } from '../persistence/atomic-write.js';
22
22
  import { checkLatestVersion as defaultCheckLatestVersion, compareVersions as defaultCompareVersions, runInstallLatest as defaultRunInstallLatest, } from '../update/index.js';
@@ -133,6 +133,18 @@ const RESUME_CONTENT_PREFETCH = 5;
133
133
  * `New Session`、空串),都不是真实输入,不得渲染成行标题。
134
134
  */
135
135
  const RESUME_SUMMARY_PLACEHOLDERS = new Set(['', '(no user message)', '(无摘要)', 'New Session']);
136
+ /** 列表卡「最近输入 / 最后输出」单行预览的长度上限(中文按字符,手机约两行)。 */
137
+ const RESUME_PREVIEW_MAX = 60;
138
+ /**
139
+ * 列表卡单行预览:压平空白、去掉反引号,再按字符截断。
140
+ *
141
+ * 为什么去反引号:agent 的输出里 `code` 很常见,截断点落在一对反引号中间时
142
+ * 后半张卡片的 markdown 会被当成代码块渲染(lark_md 不闭合就不回退)。
143
+ */
144
+ function sessionPreviewLine(text, max) {
145
+ const flat = text.replace(/`/g, '').replace(/\s+/g, ' ').trim();
146
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
147
+ }
136
148
  /**
137
149
  * Card-action commands that must run with enqueue({ immediate: true }).
138
150
  * These bypass the serial queue and execute immediately.
@@ -771,9 +783,8 @@ export class CommandRouter {
771
783
  const { cwd, messageId } = payload;
772
784
  const removed = this.bridge.removeFromQueue(cwd, messageId);
773
785
  if (removed) {
774
- // Update the queue card in-place to "cancelled" state
775
- await this.bridge.updateQueueCardToCancelled(cwd, messageId);
776
- // Send a brief confirmation (user can see the card has been updated)
786
+ // 队列卡的「已撤销」更新由 removeFromQueue 统一触发(单一收口);
787
+ // 这里只负责给点击者一个即时反馈。
777
788
  await this.bridge.sendResult({ text: '✅ 已从队列中撤销' }, ctx);
778
789
  }
779
790
  else {
@@ -854,7 +865,6 @@ export class CommandRouter {
854
865
  const tasks = this.bridge.getQueuedTasks(cwd);
855
866
  const targetIdx = tasks.findIndex((t) => t.messageId === messageId);
856
867
  let removedCount = 0;
857
- const removedIds = [];
858
868
  // Phase 1: remove synchronously. The queue chain advances on any await
859
869
  // (card PATCH round trips included), so every task before the target must
860
870
  // be removed before yielding to the event loop — otherwise a task the user
@@ -862,20 +872,15 @@ export class CommandRouter {
862
872
  // in flight.
863
873
  for (let i = 0; i < targetIdx; i++) {
864
874
  const task = tasks[i];
875
+ // 撤销卡片由 removeFromQueue 统一触发(fire-and-forget),保持本循环
876
+ // 同步推进的既有不变式。
865
877
  if (this.bridge.removeFromQueue(cwd, task.messageId)) {
866
878
  removedCount++;
867
- removedIds.push(task.messageId);
868
879
  }
869
880
  }
870
881
  if (targetIdx < 0) {
871
882
  getLogger().debug(`[router] queue.immediate: target no longer queued (began/cancelled), not clearing tasks behind it`);
872
883
  }
873
- // Phase 2: update cancelled cards after all removals. All tasks before the
874
- // target are already gone, so any chain advance can only reach the target
875
- // itself — the card updates may safely run in the background.
876
- for (const removedId of removedIds) {
877
- void this.bridge.updateQueueCardToCancelled(cwd, removedId);
878
- }
879
884
  // 4. DO NOT remove the target task - keep it in queue to execute immediately
880
885
  // The queue will naturally execute it after the current task (which we just stopped)
881
886
  // 4.4. Refresh the replacement with the latest edited content (idempotent:
@@ -1504,7 +1509,7 @@ export class CommandRouter {
1504
1509
  return { toast: { type: 'error', content: CARD_PAYLOAD_MISSING } };
1505
1510
  }
1506
1511
  if (!this.orderStore.has(orderId)) {
1507
- return { toast: { type: 'error', content: '指令不存在或已被删除' } };
1512
+ return { toast: { type: 'error', content: ORDER_NOT_FOUND_TEXT } };
1508
1513
  }
1509
1514
  // Delete the order
1510
1515
  this.orderStore.remove(orderId);
@@ -1542,7 +1547,7 @@ export class CommandRouter {
1542
1547
  this.orderStore.reload();
1543
1548
  const order = this.orderStore.get().find((o) => o.id === orderId);
1544
1549
  if (!order) {
1545
- return { toast: { type: 'error', content: '指令不存在或已被删除' } };
1550
+ return { toast: { type: 'error', content: ORDER_NOT_FOUND_TEXT } };
1546
1551
  }
1547
1552
  // 列表卡显示的是截断版(≤100 字符 + ...),编辑卡预览保持一致
1548
1553
  const displayText = order.text.length > 100 ? order.text.slice(0, 97) + '...' : order.text;
@@ -1614,7 +1619,7 @@ export class CommandRouter {
1614
1619
  */
1615
1620
  async handleOrderAliasInput(value, ctx) {
1616
1621
  const name = value.inputValue ?? value.formValue?.['aliasName'];
1617
- return this.mutateOrderAndRefreshCard(value, ctx, { missingToast: '指令不存在或已被删除' }, (order) => {
1622
+ return this.mutateOrderAndRefreshCard(value, ctx, { missingToast: ORDER_NOT_FOUND_TEXT }, (order) => {
1618
1623
  const trimmed = name?.trim() ?? '';
1619
1624
  // 变更前快照旧别名:setAlias 会原地变异 order 对象(delete entry.alias),
1620
1625
  // 删除后 order.alias 已为 undefined,直接用会让成功文案显示 `$undefined`。
@@ -1676,7 +1681,7 @@ export class CommandRouter {
1676
1681
  */
1677
1682
  async handleOrderTextInput(value, ctx) {
1678
1683
  const raw = value.inputValue ?? value.formValue?.['text'];
1679
- return this.mutateOrderAndRefreshCard(value, ctx, { missingToast: '指令不存在或已被删除' }, (order) => {
1684
+ return this.mutateOrderAndRefreshCard(value, ctx, { missingToast: ORDER_NOT_FOUND_TEXT }, (order) => {
1680
1685
  let updated;
1681
1686
  try {
1682
1687
  updated = this.orderStore.updateText(order.id, raw ?? '');
@@ -1686,7 +1691,7 @@ export class CommandRouter {
1686
1691
  }
1687
1692
  if (!updated) {
1688
1693
  // reload + find 后 updateText 不应返回 undefined;防御性兜底(如并发删除)。
1689
- return { ok: false, error: '指令不存在或已被删除' };
1694
+ return { ok: false, error: ORDER_NOT_FOUND_TEXT };
1690
1695
  }
1691
1696
  const preview = updated.text.length > 50 ? updated.text.slice(0, 50) + '...' : updated.text;
1692
1697
  return { ok: true, toast: `✅ 已更新指令: ${preview}` };
@@ -2322,13 +2327,7 @@ export class CommandRouter {
2322
2327
  width: 'weighted',
2323
2328
  weight: 2,
2324
2329
  elements: [
2325
- {
2326
- tag: 'button',
2327
- text: { tag: 'plain_text', content: c.label },
2328
- type: 'default',
2329
- size: 'small',
2330
- behaviors: [{ type: 'callback', value: { cmd: `help.${c.cmd}` } }],
2331
- },
2330
+ cardButton(c.label, { cmd: `help.${c.cmd}` }, { type: 'default', size: 'small' }),
2332
2331
  ],
2333
2332
  },
2334
2333
  {
@@ -2485,6 +2484,11 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2485
2484
  '请在 DSH Web UI 检查;需要终止时再发一次 /stop。',
2486
2485
  };
2487
2486
  }
2487
+ // 多个 workspace 并行时 /stop 会停掉全部;把这件事说出来,避免用户以为
2488
+ // 只停了当前那个(旧实现只停一条且无提示)。
2489
+ const count = interrupt.stoppedCount ?? 1;
2490
+ if (count > 1)
2491
+ return { text: `✅ 已停止 ${count} 个运行中的任务` };
2488
2492
  return null;
2489
2493
  }
2490
2494
  async cmdExit() {
@@ -2622,6 +2626,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2622
2626
  activeRun.terminal !== 'error' &&
2623
2627
  activeRun.terminal !== 'interrupted' &&
2624
2628
  activeRun.terminal !== 'idle_timeout' &&
2629
+ activeRun.terminal !== 'goal_continuing' &&
2625
2630
  activeRun.sessionId === sessionId;
2626
2631
  const activeUsage = activeRunUsage(activeRun);
2627
2632
  return {
@@ -2804,17 +2809,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2804
2809
  buildResumeActionButtons(opts) {
2805
2810
  const actions = [];
2806
2811
  if (opts.isActive) {
2807
- actions.push({
2808
- tag: 'button',
2809
- text: { tag: 'plain_text', content: '⏹ 终止' },
2810
- type: 'danger',
2811
- behaviors: [
2812
- {
2813
- type: 'callback',
2814
- value: { cmd: 'stop', runId: opts.activeRunRunId, cwd: opts.cwd },
2815
- },
2816
- ],
2817
- });
2812
+ actions.push(cardButton('⏹ 终止', { cmd: 'stop', runId: opts.activeRunRunId, cwd: opts.cwd }, { type: 'danger' }));
2818
2813
  }
2819
2814
  if (opts.compact && !opts.isActive) {
2820
2815
  actions.push(resumeCompactButton(opts.compact.sessionId, opts.compact.agentKind));
@@ -2930,88 +2925,32 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2930
2925
  // Build header with parent button
2931
2926
  const headerElements = [];
2932
2927
  if (hasParent) {
2933
- headerElements.push({
2934
- tag: 'button',
2935
- text: { tag: 'plain_text', content: '上级' },
2936
- type: 'default',
2937
- size: 'small',
2938
- behaviors: [
2939
- { type: 'callback', value: { cmd: 'ls.browse', path: parentDir, root: browseRoot } },
2940
- ],
2941
- });
2928
+ headerElements.push(cardButton('上级', { cmd: 'ls.browse', path: parentDir, root: browseRoot }, { type: 'default', size: 'small' }));
2942
2929
  }
2943
2930
  // Show "返回" button when browsing away from the /ls root (回到 /ls 指定的目录)
2944
2931
  if (canReturn) {
2945
- headerElements.push({
2946
- tag: 'button',
2947
- text: { tag: 'plain_text', content: '返回' },
2948
- type: 'default',
2949
- size: 'small',
2950
- behaviors: [
2951
- { type: 'callback', value: { cmd: 'ls.browse', path: browseRoot, root: browseRoot } },
2952
- ],
2953
- });
2932
+ headerElements.push(cardButton('返回', { cmd: 'ls.browse', path: browseRoot, root: browseRoot }, { type: 'default', size: 'small' }));
2954
2933
  }
2955
- headerElements.push({
2956
- tag: 'button',
2957
- text: { tag: 'plain_text', content: '刷新' },
2958
- type: 'default',
2959
- size: 'small',
2960
- behaviors: [
2961
- {
2962
- type: 'callback',
2963
- value: {
2964
- cmd: 'ls.refresh',
2965
- path: targetDir,
2966
- offset: safeOffset,
2967
- root: browseRoot,
2968
- ...(filter ? { q: filter } : {}),
2969
- },
2970
- },
2971
- ],
2972
- });
2934
+ headerElements.push(cardButton('刷新', {
2935
+ cmd: 'ls.refresh',
2936
+ path: targetDir,
2937
+ offset: safeOffset,
2938
+ root: browseRoot,
2939
+ ...(filter ? { q: filter } : {}),
2940
+ }, { type: 'default', size: 'small' }));
2973
2941
  // Show "切换" button when viewing a subdirectory (to switch cwd to this directory)
2974
2942
  if (isSubdir) {
2975
- headerElements.push({
2976
- tag: 'button',
2977
- text: { tag: 'plain_text', content: '切换' },
2978
- type: 'primary',
2979
- size: 'small',
2980
- // 切换只改 cwd,**不重置浏览起点**:root 一路带到切换后的卡片,
2981
- // 否则「返回」会退化成「回到刚切换到的目录」(用户反馈的缺陷)。
2982
- behaviors: [
2983
- { type: 'callback', value: { cmd: 'ls.switch', path: targetDir, root: browseRoot } },
2984
- ],
2985
- });
2943
+ headerElements.push(
2944
+ // 切换只改 cwd,**不重置浏览起点**:root 一路带到切换后的卡片,
2945
+ // 否则「返回」会退化成「回到刚切换到的目录」(用户反馈的缺陷)。
2946
+ cardButton('切换', { cmd: 'ls.switch', path: targetDir, root: browseRoot }, { type: 'primary', size: 'small' }));
2986
2947
  }
2987
2948
  // Helper to create directory button - use targetDir
2988
- const dirButton = (name) => ({
2989
- tag: 'button',
2990
- text: { tag: 'plain_text', content: `📁 ${name}` },
2991
- type: 'default',
2992
- behaviors: [
2993
- {
2994
- type: 'callback',
2995
- value: {
2996
- cmd: 'ls.browse',
2997
- path: path.join(targetDir, name),
2998
- root: browseRoot,
2999
- },
3000
- },
3001
- ],
3002
- });
2949
+ const dirButton = (name) => cardButton(`📁 ${name}`, { cmd: 'ls.browse', path: path.join(targetDir, name), root: browseRoot }, { type: 'default' });
3003
2950
  // Helper to create file button with size - use targetDir
3004
2951
  const fileButton = (name, size) => {
3005
2952
  const sizeStr = size !== undefined ? ` (${formatBytes(size)})` : '';
3006
- return {
3007
- tag: 'button',
3008
- text: { tag: 'plain_text', content: `📄 ${name}${sizeStr}` },
3009
- type: 'default',
3010
- size: 'small',
3011
- behaviors: [
3012
- { type: 'callback', value: { cmd: 'ls.file', path: path.join(targetDir, name) } },
3013
- ],
3014
- };
2953
+ return cardButton(`📄 ${name}${sizeStr}`, { cmd: 'ls.file', path: path.join(targetDir, name) }, { type: 'default', size: 'small' });
3015
2954
  };
3016
2955
  // Build each section's elements (no tabs - use section headers)
3017
2956
  // Note: div+elements is not supported in CardKit 2.0, use column_set instead
@@ -3176,25 +3115,12 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3176
3115
  const mtimeText = stat ? stat.mtime.toLocaleString('zh-CN', { hour12: false }) : '未知';
3177
3116
  const parentDir = path.dirname(filePath);
3178
3117
  const buttons = [
3179
- {
3180
- tag: 'button',
3181
- text: { tag: 'plain_text', content: '📎 下载' },
3182
- type: 'primary',
3183
- size: 'small',
3184
- behaviors: [{ type: 'callback', value: { cmd: 'ls.file', path: filePath } }],
3185
- },
3118
+ cardButton('📎 下载', { cmd: 'ls.file', path: filePath }, { type: 'primary', size: 'small' }),
3186
3119
  ];
3187
3120
  if (parentDir !== filePath) {
3188
- buttons.push({
3189
- tag: 'button',
3190
- text: { tag: 'plain_text', content: '上级' },
3191
- type: 'default',
3192
- size: 'small',
3193
- // 浏览起点 = 文件所在目录:进入后「返回」回到这里,不回 工作目录
3194
- behaviors: [
3195
- { type: 'callback', value: { cmd: 'ls.browse', path: parentDir, root: parentDir } },
3196
- ],
3197
- });
3121
+ buttons.push(
3122
+ // 浏览起点 = 文件所在目录:进入后「返回」回到这里,不回 工作目录
3123
+ cardButton('上级', { cmd: 'ls.browse', path: parentDir, root: parentDir }, { type: 'default', size: 'small' }));
3198
3124
  }
3199
3125
  return {
3200
3126
  card: {
@@ -3373,18 +3299,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3373
3299
  tag: 'column',
3374
3300
  width: 'auto',
3375
3301
  elements: [
3376
- {
3377
- tag: 'button',
3378
- text: { tag: 'plain_text', content: `切换为 ${switchLabel}` },
3379
- type: 'default',
3380
- size: 'small',
3381
- behaviors: [
3382
- {
3383
- type: 'callback',
3384
- value: { cmd: 'ws.sort', ...(filter ? { q: filter } : {}) },
3385
- },
3386
- ],
3387
- },
3302
+ cardButton(`切换为 ${switchLabel}`, { cmd: 'ws.sort', ...(filter ? { q: filter } : {}) }, { type: 'default', size: 'small' }),
3388
3303
  ],
3389
3304
  },
3390
3305
  ],
@@ -3403,46 +3318,24 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3403
3318
  tag: 'column',
3404
3319
  width: 'auto',
3405
3320
  elements: [
3406
- {
3407
- tag: 'button',
3408
- text: { tag: 'plain_text', content: '切换' },
3409
- type: 'primary',
3410
- size: 'small',
3411
- behaviors: [
3412
- {
3413
- type: 'callback',
3414
- value: {
3415
- cmd: 'ws.use',
3416
- name: entry.name,
3417
- offset: safeOffset,
3418
- ...(filter ? { q: filter } : {}),
3419
- },
3420
- },
3421
- ],
3422
- },
3321
+ cardButton('切换', {
3322
+ cmd: 'ws.use',
3323
+ name: entry.name,
3324
+ offset: safeOffset,
3325
+ ...(filter ? { q: filter } : {}),
3326
+ }, { type: 'primary', size: 'small' }),
3423
3327
  ],
3424
3328
  },
3425
3329
  {
3426
3330
  tag: 'column',
3427
3331
  width: 'auto',
3428
3332
  elements: [
3429
- {
3430
- tag: 'button',
3431
- text: { tag: 'plain_text', content: '删除' },
3432
- type: 'danger',
3433
- size: 'small',
3434
- behaviors: [
3435
- {
3436
- type: 'callback',
3437
- value: {
3438
- cmd: 'ws.remove',
3439
- name: entry.name,
3440
- offset: safeOffset,
3441
- ...(filter ? { q: filter } : {}),
3442
- },
3443
- },
3444
- ],
3445
- },
3333
+ cardButton('删除', {
3334
+ cmd: 'ws.remove',
3335
+ name: entry.name,
3336
+ offset: safeOffset,
3337
+ ...(filter ? { q: filter } : {}),
3338
+ }, { type: 'danger', size: 'small' }),
3446
3339
  ],
3447
3340
  },
3448
3341
  ],
@@ -3477,17 +3370,6 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3477
3370
  }
3478
3371
  }
3479
3372
  }
3480
- /**
3481
- * degraded card when a session's export is non-empty but unparseable
3482
- * (truncated by opencode's pipe cap on huge sessions, or otherwise corrupt).
3483
- * The session exists, so reuse listSessions metadata (title) instead of the
3484
- * misleading "未找到". Reuses the same card-builder helpers (markdownDiv) as
3485
- * the normal resume card; enforceCardBudget still applies at send time.
3486
- */
3487
- /**
3488
- * /resume 核心实现(public:handleResumePage / handle / resume.use 共用,
3489
- * 也是测试的直接 seam,避免测试用 @ts-expect-error 摸私有方法)。
3490
- */
3491
3373
  /**
3492
3374
  * DSH /resume preset 一致性校验:返回 mismatch 详情,或 undefined(一致/不可判定)。
3493
3375
  *
@@ -3654,6 +3536,13 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3654
3536
  const sessionMeta = new Map();
3655
3537
  const prefetchCount = Math.min(RESUME_CONTENT_PREFETCH, allSessions.length);
3656
3538
  for (const s of allSessions.slice(0, prefetchCount)) {
3539
+ // 行上已经带了服务端投影预览(DSH:title + turnOutline 末条)→ 跳过预取。
3540
+ // DSH 的 readSessionContent 每行走 3 次 unary:`session/list`(cwd 守卫)、
3541
+ // `session/projections`(取 throughSeq)、再 `session/page`(maxMessages=10000,
3542
+ // 拉整段历史)。按前 5 行预取等于 15 次同步 HTTP,而这些信息一次
3543
+ // `session/list` 就已经给全了。
3544
+ if (s.title || s.lastUserMessage || s.lastAssistantMessage)
3545
+ continue;
3657
3546
  // 优先走轻量摘要(只取标题/用量,不构造 events)——codex 实现了
3658
3547
  // readSessionSummary,避免对前 N 条各做一次全量 JSONL 解析来构造
3659
3548
  // 立即被丢弃的 events 数组。其他 agent 无此方法时回退全量读取再取字段。
@@ -3697,26 +3586,33 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3697
3586
  const buttonText = isCurrent ? '✓ 当前会话' : '恢复此会话';
3698
3587
  const meta = sessionMeta.get(s.sessionId);
3699
3588
  // Title priority:
3700
- // 1. Prefetched displayTitle (aiTitle or last user message) keeps its
3589
+ // 1. 行级 title(服务端投影,DSH)→ 「标题」。
3590
+ // 2. Prefetched displayTitle (aiTitle or last user message) keeps its
3701
3591
  // original label (AI 标题 / 最近输入).
3702
- // 2. Otherwise the lightweight summary renders under the neutral
3592
+ // 3. Otherwise the lightweight summary renders under the neutral
3703
3593
  // "会话摘要" label — summary is the first user message, not the
3704
3594
  // last input, so "最近输入" would be semantically wrong.
3705
- // 3. Placeholder summaries (no user message) render no title section
3595
+ // 4. Placeholder summaries (no user message) render no title section
3706
3596
  // at all — never leak '(无摘要)'/'(no user message)'/'New Session'.
3707
3597
  const summaryIsPlaceholder = RESUME_SUMMARY_PLACEHOLDERS.has(s.summary.trim());
3708
3598
  const prefetchedTitle = meta?.displayTitle;
3599
+ // 行级 title 也过占位符过滤:DSH 对空会话可能给 "New Session",旧路径
3600
+ // 的 summary 会被挡掉,新路径如果不过滤就会把它当真实标题渲染。
3601
+ const rowTitleRaw = s.title?.trim() ?? '';
3602
+ const rowTitle = rowTitleRaw && !RESUME_SUMMARY_PLACEHOLDERS.has(rowTitleRaw) ? rowTitleRaw : undefined;
3709
3603
  // aiTitle 不能单独作为"预取标题"键:若 displayTitle 缺失(理论上 opencode
3710
3604
  // info.title 非空但 export 无 user text),标题兜底是 summary,label 必须
3711
3605
  // 是中性"会话摘要"而不是"最近输入"(Review 边缘分支)。
3712
- const titleText = prefetchedTitle ?? (summaryIsPlaceholder ? '' : s.summary);
3606
+ const titleText = prefetchedTitle ?? rowTitle ?? (summaryIsPlaceholder ? '' : s.summary);
3713
3607
  const titleLabel = prefetchedTitle
3714
3608
  ? meta?.aiTitle
3715
3609
  ? 'AI 标题'
3716
3610
  : '最近输入'
3717
- : summaryIsPlaceholder
3718
- ? ''
3719
- : '会话摘要';
3611
+ : rowTitle
3612
+ ? '标题'
3613
+ : summaryIsPlaceholder
3614
+ ? ''
3615
+ : '会话摘要';
3720
3616
  const sections = [];
3721
3617
  if (titleText) {
3722
3618
  sections.push(`🏷️ **${titleLabel}**\n${titleText.slice(0, 80)}${titleText.length > 80 ? '...' : ''}`);
@@ -3724,6 +3620,14 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3724
3620
  if (meta?.recap) {
3725
3621
  sections.push(`📝 **Recap**\n${meta.recap.slice(0, 60)}${meta.recap.length > 60 ? '...' : ''}`);
3726
3622
  }
3623
+ // 行级预览(DSH 服务端投影):最后一句我说了什么 / 最后一句它答了什么。
3624
+ // 只对提供了字段的 reader 渲染,其他 agent 的卡片一个字都不变。
3625
+ if (s.lastUserMessage) {
3626
+ sections.push(`✏️ **最近输入**\n${sessionPreviewLine(s.lastUserMessage, RESUME_PREVIEW_MAX)}`);
3627
+ }
3628
+ if (s.lastAssistantMessage) {
3629
+ sections.push(`📤 **最后输出**\n${sessionPreviewLine(s.lastAssistantMessage, RESUME_PREVIEW_MAX)}`);
3630
+ }
3727
3631
  const infoStr = sections.length > 0 ? '\n' + sections.join('\n──\n') : '';
3728
3632
  elements.push({ tag: 'hr' });
3729
3633
  // Both current and non-current sessions get a button for visual consistency
@@ -3741,18 +3645,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3741
3645
  tag: 'column',
3742
3646
  width: 'auto',
3743
3647
  elements: [
3744
- {
3745
- tag: 'button',
3746
- text: { tag: 'plain_text', content: buttonText },
3747
- type: isCurrent ? 'default' : 'primary',
3748
- disabled: isCurrent,
3749
- behaviors: [
3750
- {
3751
- type: 'callback',
3752
- value: { cmd: 'resume.use', sessionId: s.sessionId, agent: agentKind },
3753
- },
3754
- ],
3755
- },
3648
+ cardButton(buttonText, { cmd: 'resume.use', sessionId: s.sessionId, agent: agentKind }, { type: isCurrent ? 'default' : 'primary', disabled: isCurrent }),
3756
3649
  ],
3757
3650
  },
3758
3651
  ],
@@ -3830,14 +3723,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3830
3723
  content: `> session: ${run.sessionId.slice(0, 12)}... \n> 状态: ${statusLabel}`,
3831
3724
  },
3832
3725
  });
3833
- // Stop button - CardKit 2.0 with behaviors callback
3834
- elements.push({
3835
- tag: 'button',
3836
- text: { tag: 'plain_text', content: '⏹ 停止' },
3837
- type: 'danger',
3838
- size: 'small',
3839
- behaviors: [{ type: 'callback', value: { cmd: 'stop', runId: run.runId, cwd: run.cwd } }],
3840
- });
3726
+ elements.push(cardButton('⏹ 停止', { cmd: 'stop', runId: run.runId, cwd: run.cwd }, { type: 'danger', size: 'small' }));
3841
3727
  elements.push({ tag: 'hr' });
3842
3728
  }
3843
3729
  remaining -= end - start;
@@ -3862,14 +3748,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3862
3748
  tag: 'div',
3863
3749
  text: { tag: 'lark_md', content: `> 目录: ${run.cwd} \n> 状态: ${statusLabel}` },
3864
3750
  });
3865
- // Stop button - CardKit 2.0 with behaviors callback
3866
- elements.push({
3867
- tag: 'button',
3868
- text: { tag: 'plain_text', content: '⏹ 停止' },
3869
- type: 'danger',
3870
- size: 'small',
3871
- behaviors: [{ type: 'callback', value: { cmd: 'stop', runId: run.runId, cwd: run.cwd } }],
3872
- });
3751
+ elements.push(cardButton('⏹ 停止', { cmd: 'stop', runId: run.runId, cwd: run.cwd }, { type: 'danger', size: 'small' }));
3873
3752
  elements.push({ tag: 'hr' });
3874
3753
  }
3875
3754
  }
@@ -3976,55 +3855,29 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3976
3855
  // 使用 agent config builder 获取该 agent 的配置字段
3977
3856
  const defaultAgent = displayConfig.defaultAgent ?? 'claude';
3978
3857
  const configBuilder = getConfigBuilder(defaultAgent);
3979
- // Build defaultAgent selector with availability annotations.
3980
- // Direct inline construction (not via ConfigField) so that text (display label)
3981
- // and value (agentKind for config storage) can differ — uninstalled agents get
3982
- // a "⚠️ (未安装)" suffix in the label while the value stays clean.
3983
- // 固定展示顺序(Codex → Claude → OpenCode → Pi → Kimi),
3858
+ // defaultAgent 选择器作为普通 select 字段交给共享渲染(SelectOption 支持
3859
+ // text/value 分离:未安装 agent 的 label 带「⚠️ (未安装)」,value 保持干净)。
3860
+ // 固定展示顺序(Codex → Claude → OpenCode → Pi → Kimi → DSH),
3984
3861
  // 明确未安装的 agent 排到后面(getCachedAvailability === false)。
3985
3862
  const allAgents = sortAgentsForDisplay(listRegisteredAgents(), getCachedAvailability);
3986
3863
  const agentOptions = allAgents.map((kind) => {
3987
3864
  const available = getCachedAvailability(kind);
3988
3865
  const label = available === false ? `${agentDisplayName(kind)} ⚠️ (未安装)` : agentDisplayName(kind);
3989
- return { text: { tag: 'plain_text', content: label }, value: kind };
3866
+ return { text: label, value: kind };
3990
3867
  });
3991
3868
  const selectedAgent = displayConfig.defaultAgent ?? 'claude';
3992
- const agentSelector = {
3993
- tag: 'column_set',
3994
- flex_mode: 'none',
3995
- columns: [
3996
- {
3997
- tag: 'column',
3998
- width: 'weighted',
3999
- weight: 2,
4000
- vertical_align: 'center',
4001
- elements: [{ tag: 'div', text: { tag: 'lark_md', content: '默认 Agent' } }],
4002
- },
4003
- {
4004
- tag: 'column',
4005
- width: 'weighted',
4006
- weight: 3,
4007
- vertical_align: 'center',
4008
- elements: [
4009
- {
4010
- tag: 'select_static',
4011
- placeholder: { tag: 'plain_text', content: '请选择' },
4012
- options: agentOptions,
4013
- initial_option: selectedAgent,
4014
- behaviors: [{ type: 'callback', value: { cmd: 'config.set', key: 'defaultAgent' } }],
4015
- },
4016
- ],
4017
- },
4018
- ],
3869
+ const agentSelectorField = {
3870
+ key: 'defaultAgent',
3871
+ label: '默认 Agent',
3872
+ type: 'select',
3873
+ options: agentOptions,
3874
+ currentValue: selectedAgent,
4019
3875
  };
4020
- // Agent config fields — no longer include defaultAgent as a ConfigField
4021
- // (it's rendered inline above with text/value separation).
4022
- const agentConfigFieldsNoSelector = configBuilder.buildFields(displayConfig);
4023
3876
  const tabs = [
4024
3877
  {
4025
3878
  id: 'agent',
4026
3879
  label: `🤖 ${agentDisplayName(defaultAgent)}`,
4027
- fields: agentConfigFieldsNoSelector,
3880
+ fields: [agentSelectorField, ...configBuilder.buildFields(displayConfig)],
4028
3881
  },
4029
3882
  {
4030
3883
  id: 'idle',
@@ -4044,28 +3897,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
4044
3897
  ],
4045
3898
  },
4046
3899
  ];
4047
- // 使用共享渲染模块构建卡片
4048
- const card = buildConfigCardFromTabs(tabs, displayConfig);
4049
- // Inject the agent selector as the first element in the agent tab section,
4050
- // right after the "**🤖 ..." header, so text/value can differ for uninstalled agents.
4051
- // NOTE: findIndex relies on the agent tab header starting with "**🤖" — if the
4052
- // tab label format changes, this fallback will fire and the selector moves to top.
4053
- const body = card.body;
4054
- if (body && Array.isArray(body.elements)) {
4055
- const headerIdx = body.elements.findIndex((el) => typeof el === 'object' &&
4056
- el !== null &&
4057
- el.tag === 'div' &&
4058
- el.text?.tag === 'lark_md' &&
4059
- (el.text?.content ?? '').startsWith('**🤖'));
4060
- if (headerIdx >= 0) {
4061
- body.elements.splice(headerIdx + 1, 0, agentSelector);
4062
- }
4063
- else {
4064
- getLogger().warn('[buildConfigCard] agent tab header not found, falling back to unshift');
4065
- body.elements.unshift(agentSelector);
4066
- }
4067
- }
4068
- return { card };
3900
+ // 使用共享渲染模块构建卡片(Agent 选择器作为普通 select 字段参与渲染)
3901
+ return { card: buildConfigCardFromTabs(tabs, displayConfig) };
4069
3902
  }
4070
3903
  /** /order 实现(public:测试直接调用,替代 as unknown as)。 */
4071
3904
  cmdOrder(args, _ctx, offset = 0) {
@@ -4096,49 +3929,16 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
4096
3929
  // 右侧「删除指令」按钮。两列均 weighted 1:1,跨行对齐。
4097
3930
  const leftColumn = order.alias
4098
3931
  ? [
4099
- {
4100
- tag: 'button',
4101
- text: { tag: 'plain_text', content: `$${order.alias}` },
4102
- type: 'default',
4103
- size: 'small',
4104
- behaviors: [
4105
- {
4106
- type: 'callback',
4107
- value: { cmd: 'order.aliasEdit', orderId: order.id, offset: safeOffset },
4108
- },
4109
- ],
4110
- },
4111
- {
4112
- tag: 'button',
4113
- text: { tag: 'plain_text', content: '✕' },
4114
- type: 'default',
4115
- size: 'small',
4116
- behaviors: [
4117
- {
4118
- type: 'callback',
4119
- value: {
4120
- cmd: 'order.aliasRemove',
4121
- orderId: order.id,
4122
- aliasName: order.alias,
4123
- offset: safeOffset,
4124
- },
4125
- },
4126
- ],
4127
- },
3932
+ cardButton(`$${order.alias}`, { cmd: 'order.aliasEdit', orderId: order.id, offset: safeOffset }, { type: 'default', size: 'small' }),
3933
+ cardButton('✕', {
3934
+ cmd: 'order.aliasRemove',
3935
+ orderId: order.id,
3936
+ aliasName: order.alias,
3937
+ offset: safeOffset,
3938
+ }, { type: 'default', size: 'small' }),
4128
3939
  ]
4129
3940
  : [
4130
- {
4131
- tag: 'button',
4132
- text: { tag: 'plain_text', content: '+别名' },
4133
- type: 'default',
4134
- size: 'small',
4135
- behaviors: [
4136
- {
4137
- type: 'callback',
4138
- value: { cmd: 'order.aliasEdit', orderId: order.id, offset: safeOffset },
4139
- },
4140
- ],
4141
- },
3941
+ cardButton('+别名', { cmd: 'order.aliasEdit', orderId: order.id, offset: safeOffset }, { type: 'default', size: 'small' }),
4142
3942
  ];
4143
3943
  elements.push({
4144
3944
  tag: 'column_set',
@@ -4162,18 +3962,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
4162
3962
  tag: 'column',
4163
3963
  width: 'auto',
4164
3964
  elements: [
4165
- {
4166
- tag: 'button',
4167
- text: { tag: 'plain_text', content: '▶ 执行' },
4168
- type: 'primary',
4169
- size: 'small',
4170
- behaviors: [
4171
- {
4172
- type: 'callback',
4173
- value: { cmd: 'order.exec', orderId: order.id },
4174
- },
4175
- ],
4176
- },
3965
+ cardButton('▶ 执行', { cmd: 'order.exec', orderId: order.id }, { type: 'primary', size: 'small' }),
4177
3966
  ],
4178
3967
  },
4179
3968
  {
@@ -4183,44 +3972,14 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
4183
3972
  tag: 'column',
4184
3973
  width: 'auto',
4185
3974
  elements: [
4186
- {
4187
- tag: 'button',
4188
- text: { tag: 'plain_text', content: '编辑' },
4189
- type: 'default',
4190
- size: 'small',
4191
- behaviors: [
4192
- {
4193
- type: 'callback',
4194
- value: {
4195
- cmd: 'order.textEdit',
4196
- orderId: order.id,
4197
- offset: safeOffset,
4198
- },
4199
- },
4200
- ],
4201
- },
3975
+ cardButton('编辑', { cmd: 'order.textEdit', orderId: order.id, offset: safeOffset }, { type: 'default', size: 'small' }),
4202
3976
  ],
4203
3977
  },
4204
3978
  {
4205
3979
  tag: 'column',
4206
3980
  width: 'auto',
4207
3981
  elements: [
4208
- {
4209
- tag: 'button',
4210
- text: { tag: 'plain_text', content: '删除' },
4211
- type: 'danger',
4212
- size: 'small',
4213
- behaviors: [
4214
- {
4215
- type: 'callback',
4216
- value: {
4217
- cmd: 'order.delete',
4218
- orderId: order.id,
4219
- offset: safeOffset,
4220
- },
4221
- },
4222
- ],
4223
- },
3982
+ cardButton('删除', { cmd: 'order.delete', orderId: order.id, offset: safeOffset }, { type: 'danger', size: 'small' }),
4224
3983
  ],
4225
3984
  },
4226
3985
  ],