lark-remote 0.5.7 → 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 (47) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/bridge/index.d.ts +15 -0
  3. package/dist/bridge/index.js +61 -0
  4. package/dist/bridge/queue-manager.js +41 -58
  5. package/dist/card/approval-render.js +2 -1
  6. package/dist/card/bash-card-session.d.ts +0 -1
  7. package/dist/card/bash-card-session.js +0 -8
  8. package/dist/card/bash-renderer.js +2 -1
  9. package/dist/card/card-budget.js +15 -19
  10. package/dist/card/card-session.d.ts +3 -13
  11. package/dist/card/card-session.js +4 -18
  12. package/dist/card/card-shared.d.ts +11 -0
  13. package/dist/card/card-shared.js +19 -13
  14. package/dist/card/run-card-session.d.ts +0 -1
  15. package/dist/card/run-card-session.js +0 -6
  16. package/dist/card/run-renderer.d.ts +3 -9
  17. package/dist/card/run-renderer.js +99 -27
  18. package/dist/card/text-truncate.d.ts +3 -5
  19. package/dist/card/text-truncate.js +3 -5
  20. package/dist/card/tool-render.js +7 -21
  21. package/dist/common/tool-summary.d.ts +8 -0
  22. package/dist/common/tool-summary.js +38 -20
  23. package/dist/common/turn-failure.d.ts +13 -0
  24. package/dist/common/turn-failure.js +18 -0
  25. package/dist/index.js +5 -7
  26. package/dist/order/index.d.ts +2 -0
  27. package/dist/order/index.js +2 -0
  28. package/dist/router/card-helpers.js +23 -42
  29. package/dist/router/config/codex.js +2 -3
  30. package/dist/router/config/common/model-patch.d.ts +8 -0
  31. package/dist/router/config/common/model-patch.js +13 -0
  32. package/dist/router/config/common/render.js +30 -77
  33. package/dist/router/config/dsh.js +2 -1
  34. package/dist/router/config/kimi.js +3 -3
  35. package/dist/router/index.d.ts +0 -11
  36. package/dist/router/index.js +108 -349
  37. package/dist/router/order-exec-dispatch.js +2 -1
  38. package/dist/runner/common/defaults.d.ts +2 -0
  39. package/dist/runner/common/defaults.js +3 -0
  40. package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
  41. package/dist/runner/dsh/client.js +1 -2
  42. package/dist/runner/types.d.ts +19 -0
  43. package/dist/runner/types.js +9 -0
  44. package/dist/session/claude/session-index.d.ts +0 -4
  45. package/dist/session/claude/session-index.js +0 -8
  46. package/dist/session/dsh/sessions.js +57 -2
  47. 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.
@@ -1504,7 +1516,7 @@ export class CommandRouter {
1504
1516
  return { toast: { type: 'error', content: CARD_PAYLOAD_MISSING } };
1505
1517
  }
1506
1518
  if (!this.orderStore.has(orderId)) {
1507
- return { toast: { type: 'error', content: '指令不存在或已被删除' } };
1519
+ return { toast: { type: 'error', content: ORDER_NOT_FOUND_TEXT } };
1508
1520
  }
1509
1521
  // Delete the order
1510
1522
  this.orderStore.remove(orderId);
@@ -1542,7 +1554,7 @@ export class CommandRouter {
1542
1554
  this.orderStore.reload();
1543
1555
  const order = this.orderStore.get().find((o) => o.id === orderId);
1544
1556
  if (!order) {
1545
- return { toast: { type: 'error', content: '指令不存在或已被删除' } };
1557
+ return { toast: { type: 'error', content: ORDER_NOT_FOUND_TEXT } };
1546
1558
  }
1547
1559
  // 列表卡显示的是截断版(≤100 字符 + ...),编辑卡预览保持一致
1548
1560
  const displayText = order.text.length > 100 ? order.text.slice(0, 97) + '...' : order.text;
@@ -1614,7 +1626,7 @@ export class CommandRouter {
1614
1626
  */
1615
1627
  async handleOrderAliasInput(value, ctx) {
1616
1628
  const name = value.inputValue ?? value.formValue?.['aliasName'];
1617
- return this.mutateOrderAndRefreshCard(value, ctx, { missingToast: '指令不存在或已被删除' }, (order) => {
1629
+ return this.mutateOrderAndRefreshCard(value, ctx, { missingToast: ORDER_NOT_FOUND_TEXT }, (order) => {
1618
1630
  const trimmed = name?.trim() ?? '';
1619
1631
  // 变更前快照旧别名:setAlias 会原地变异 order 对象(delete entry.alias),
1620
1632
  // 删除后 order.alias 已为 undefined,直接用会让成功文案显示 `$undefined`。
@@ -1676,7 +1688,7 @@ export class CommandRouter {
1676
1688
  */
1677
1689
  async handleOrderTextInput(value, ctx) {
1678
1690
  const raw = value.inputValue ?? value.formValue?.['text'];
1679
- return this.mutateOrderAndRefreshCard(value, ctx, { missingToast: '指令不存在或已被删除' }, (order) => {
1691
+ return this.mutateOrderAndRefreshCard(value, ctx, { missingToast: ORDER_NOT_FOUND_TEXT }, (order) => {
1680
1692
  let updated;
1681
1693
  try {
1682
1694
  updated = this.orderStore.updateText(order.id, raw ?? '');
@@ -1686,7 +1698,7 @@ export class CommandRouter {
1686
1698
  }
1687
1699
  if (!updated) {
1688
1700
  // reload + find 后 updateText 不应返回 undefined;防御性兜底(如并发删除)。
1689
- return { ok: false, error: '指令不存在或已被删除' };
1701
+ return { ok: false, error: ORDER_NOT_FOUND_TEXT };
1690
1702
  }
1691
1703
  const preview = updated.text.length > 50 ? updated.text.slice(0, 50) + '...' : updated.text;
1692
1704
  return { ok: true, toast: `✅ 已更新指令: ${preview}` };
@@ -2322,13 +2334,7 @@ export class CommandRouter {
2322
2334
  width: 'weighted',
2323
2335
  weight: 2,
2324
2336
  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
- },
2337
+ cardButton(c.label, { cmd: `help.${c.cmd}` }, { type: 'default', size: 'small' }),
2332
2338
  ],
2333
2339
  },
2334
2340
  {
@@ -2804,17 +2810,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2804
2810
  buildResumeActionButtons(opts) {
2805
2811
  const actions = [];
2806
2812
  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
- });
2813
+ actions.push(cardButton('⏹ 终止', { cmd: 'stop', runId: opts.activeRunRunId, cwd: opts.cwd }, { type: 'danger' }));
2818
2814
  }
2819
2815
  if (opts.compact && !opts.isActive) {
2820
2816
  actions.push(resumeCompactButton(opts.compact.sessionId, opts.compact.agentKind));
@@ -2930,88 +2926,32 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2930
2926
  // Build header with parent button
2931
2927
  const headerElements = [];
2932
2928
  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
- });
2929
+ headerElements.push(cardButton('上级', { cmd: 'ls.browse', path: parentDir, root: browseRoot }, { type: 'default', size: 'small' }));
2942
2930
  }
2943
2931
  // Show "返回" button when browsing away from the /ls root (回到 /ls 指定的目录)
2944
2932
  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
- });
2933
+ headerElements.push(cardButton('返回', { cmd: 'ls.browse', path: browseRoot, root: browseRoot }, { type: 'default', size: 'small' }));
2954
2934
  }
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
- });
2935
+ headerElements.push(cardButton('刷新', {
2936
+ cmd: 'ls.refresh',
2937
+ path: targetDir,
2938
+ offset: safeOffset,
2939
+ root: browseRoot,
2940
+ ...(filter ? { q: filter } : {}),
2941
+ }, { type: 'default', size: 'small' }));
2973
2942
  // Show "切换" button when viewing a subdirectory (to switch cwd to this directory)
2974
2943
  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
- });
2944
+ headerElements.push(
2945
+ // 切换只改 cwd,**不重置浏览起点**:root 一路带到切换后的卡片,
2946
+ // 否则「返回」会退化成「回到刚切换到的目录」(用户反馈的缺陷)。
2947
+ cardButton('切换', { cmd: 'ls.switch', path: targetDir, root: browseRoot }, { type: 'primary', size: 'small' }));
2986
2948
  }
2987
2949
  // 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
- });
2950
+ const dirButton = (name) => cardButton(`📁 ${name}`, { cmd: 'ls.browse', path: path.join(targetDir, name), root: browseRoot }, { type: 'default' });
3003
2951
  // Helper to create file button with size - use targetDir
3004
2952
  const fileButton = (name, size) => {
3005
2953
  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
- };
2954
+ return cardButton(`📄 ${name}${sizeStr}`, { cmd: 'ls.file', path: path.join(targetDir, name) }, { type: 'default', size: 'small' });
3015
2955
  };
3016
2956
  // Build each section's elements (no tabs - use section headers)
3017
2957
  // Note: div+elements is not supported in CardKit 2.0, use column_set instead
@@ -3176,25 +3116,12 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3176
3116
  const mtimeText = stat ? stat.mtime.toLocaleString('zh-CN', { hour12: false }) : '未知';
3177
3117
  const parentDir = path.dirname(filePath);
3178
3118
  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
- },
3119
+ cardButton('📎 下载', { cmd: 'ls.file', path: filePath }, { type: 'primary', size: 'small' }),
3186
3120
  ];
3187
3121
  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
- });
3122
+ buttons.push(
3123
+ // 浏览起点 = 文件所在目录:进入后「返回」回到这里,不回 工作目录
3124
+ cardButton('上级', { cmd: 'ls.browse', path: parentDir, root: parentDir }, { type: 'default', size: 'small' }));
3198
3125
  }
3199
3126
  return {
3200
3127
  card: {
@@ -3373,18 +3300,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3373
3300
  tag: 'column',
3374
3301
  width: 'auto',
3375
3302
  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
- },
3303
+ cardButton(`切换为 ${switchLabel}`, { cmd: 'ws.sort', ...(filter ? { q: filter } : {}) }, { type: 'default', size: 'small' }),
3388
3304
  ],
3389
3305
  },
3390
3306
  ],
@@ -3403,46 +3319,24 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3403
3319
  tag: 'column',
3404
3320
  width: 'auto',
3405
3321
  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
- },
3322
+ cardButton('切换', {
3323
+ cmd: 'ws.use',
3324
+ name: entry.name,
3325
+ offset: safeOffset,
3326
+ ...(filter ? { q: filter } : {}),
3327
+ }, { type: 'primary', size: 'small' }),
3423
3328
  ],
3424
3329
  },
3425
3330
  {
3426
3331
  tag: 'column',
3427
3332
  width: 'auto',
3428
3333
  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
- },
3334
+ cardButton('删除', {
3335
+ cmd: 'ws.remove',
3336
+ name: entry.name,
3337
+ offset: safeOffset,
3338
+ ...(filter ? { q: filter } : {}),
3339
+ }, { type: 'danger', size: 'small' }),
3446
3340
  ],
3447
3341
  },
3448
3342
  ],
@@ -3477,17 +3371,6 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3477
3371
  }
3478
3372
  }
3479
3373
  }
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
3374
  /**
3492
3375
  * DSH /resume preset 一致性校验:返回 mismatch 详情,或 undefined(一致/不可判定)。
3493
3376
  *
@@ -3654,6 +3537,12 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3654
3537
  const sessionMeta = new Map();
3655
3538
  const prefetchCount = Math.min(RESUME_CONTENT_PREFETCH, allSessions.length);
3656
3539
  for (const s of allSessions.slice(0, prefetchCount)) {
3540
+ // 行上已经带了服务端投影预览(DSH:title + turnOutline 末条)→ 跳过预取。
3541
+ // DSH 的 readSessionContent 是 3 个 curl 加一次 maxMessages=10000 的
3542
+ // session/page;一页 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
  ],