lark-remote 0.5.19 → 0.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/bridge/approval-actions.d.ts +1 -1
- package/dist/bridge/approval-actions.js +1 -1
- package/dist/bridge/bash-run.d.ts +71 -0
- package/dist/bridge/bash-run.js +217 -0
- package/dist/bridge/compact.d.ts +155 -0
- package/dist/bridge/compact.js +450 -0
- package/dist/bridge/final-usage.js +4 -4
- package/dist/bridge/inbound-media.js +1 -1
- package/dist/bridge/index.d.ts +97 -153
- package/dist/bridge/index.js +182 -809
- package/dist/bridge/job-watch.d.ts +68 -0
- package/dist/bridge/job-watch.js +146 -0
- package/dist/bridge/queue-manager.d.ts +3 -3
- package/dist/bridge/queue-manager.js +10 -29
- package/dist/card/approval-render.d.ts +2 -2
- package/dist/card/approval-render.js +109 -97
- package/dist/card/card-budget.js +1 -1
- package/dist/card/card-helpers.d.ts +3 -3
- package/dist/card/card-helpers.js +6 -6
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +34 -93
- package/dist/card/run-state.d.ts +1 -1
- package/dist/card/run-state.js +28 -90
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +1 -1
- package/dist/card/usage-format.d.ts +12 -0
- package/dist/card/usage-format.js +8 -3
- package/dist/cli.js +15 -3
- package/dist/clone.js +2 -2
- package/dist/config/codex-config.js +130 -106
- package/dist/config/index.d.ts +16 -2
- package/dist/config/index.js +23 -8
- package/dist/connector/index.js +6 -6
- package/dist/error-classification.js +3 -2
- package/dist/inbound/placeholder.d.ts +1 -1
- package/dist/inbound/placeholder.js +1 -1
- package/dist/index.js +203 -133
- package/dist/logger/index.js +6 -6
- package/dist/message-handlers.d.ts +0 -5
- package/dist/message-handlers.js +1 -1
- package/dist/model-catalog.d.ts +2 -2
- package/dist/model-catalog.js +3 -8
- package/dist/platform/identity.js +1 -1
- package/dist/platform/path.d.ts +1 -1
- package/dist/platform/path.js +1 -1
- package/dist/platform/sleep-blocker.js +1 -1
- package/dist/platform/terminator-win32.js +2 -1
- package/dist/router/commands/resume.js +2 -20
- package/dist/router/config/dsh.d.ts +4 -8
- package/dist/router/config/dsh.js +3 -10
- package/dist/router/config/kimi.js +1 -1
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/config-diff.js +1 -1
- package/dist/router/index.d.ts +56 -11
- package/dist/router/index.js +219 -200
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +6 -4
- package/dist/runner/codex/app-server/runner.d.ts +9 -2
- package/dist/runner/codex/app-server/runner.js +30 -25
- package/dist/runner/codex/app-server/translator.d.ts +0 -5
- package/dist/runner/codex/app-server/translator.js +4 -17
- package/dist/runner/common/acp/acp-approval.d.ts +1 -1
- package/dist/runner/common/acp/acp-approval.js +1 -1
- package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
- package/dist/runner/common/acp/base-acp-runner.js +26 -65
- package/dist/runner/common/acp/base-translator.d.ts +12 -2
- package/dist/runner/common/acp/base-translator.js +31 -9
- package/dist/runner/common/acp/protocol-types.d.ts +1 -1
- package/dist/runner/common/acp/protocol-types.js +2 -2
- package/dist/runner/common/connection-based-runner.d.ts +32 -1
- package/dist/runner/common/connection-based-runner.js +32 -2
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
- package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
- package/dist/runner/common/spawning-runner.d.ts +1 -1
- package/dist/runner/common/spawning-runner.js +1 -1
- package/dist/runner/dsh/catalog-probe.d.ts +17 -0
- package/dist/runner/dsh/catalog-probe.js +10 -0
- package/dist/runner/dsh/client.d.ts +1 -3
- package/dist/runner/dsh/client.js +2 -4
- package/dist/runner/dsh/exit-cancel.d.ts +2 -2
- package/dist/runner/dsh/exit-cancel.js +2 -2
- package/dist/runner/dsh/inflight.d.ts +1 -1
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/runner.d.ts +40 -1
- package/dist/runner/dsh/runner.js +272 -199
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/dsh/translator.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +1 -2
- package/dist/runner/kimi/acp/runner.js +1 -2
- package/dist/runner/kimi/acp/translator.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +3 -4
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
- package/dist/runner/pi/rpc/protocol-types.js +1 -1
- package/dist/runner/pi/rpc/runner.d.ts +3 -0
- package/dist/runner/pi/rpc/runner.js +27 -42
- package/dist/runner/types.d.ts +2 -2
- package/dist/runner/types.js +1 -1
- package/dist/session/claude/sessions.d.ts +45 -0
- package/dist/session/claude/sessions.js +97 -71
- package/dist/session/codex/rollout-reader.js +4 -4
- package/dist/session/common/throw-session-list-error.d.ts +10 -0
- package/dist/session/common/throw-session-list-error.js +16 -0
- package/dist/session/dsh/sessions.js +11 -5
- package/dist/session/kimi/sessions.d.ts +40 -1
- package/dist/session/kimi/sessions.js +244 -192
- package/dist/session/opencode/sessions.d.ts +153 -1
- package/dist/session/opencode/sessions.js +354 -321
- package/dist/session/pi/sessions.js +4 -5
- package/dist/session/session-store.d.ts +1 -1
- package/dist/session/session-store.js +1 -1
- package/dist/update/version-check.js +2 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
package/dist/router/index.js
CHANGED
|
@@ -57,10 +57,10 @@ function findMergedCommandTail(message) {
|
|
|
57
57
|
/** 人类可读字节数(B/KB/MB/GB);/ls 列表与单文件卡共用。 */
|
|
58
58
|
/**
|
|
59
59
|
* /order 列表页大小;指令超过此数量时显示分页导航栏。
|
|
60
|
-
*
|
|
60
|
+
* 实测:每行 2 个元素(div + column_set,行间 hr),20 行 + 分页栏
|
|
61
61
|
* = 61 个 body 元素,触发飞书 ErrCode 11310 "element exceeds the limit"
|
|
62
62
|
* (21+ 条指令时第 1 页必炸)。改为 15 行:3*15 - 1 + 2 = 46 个元素,留足余量。
|
|
63
|
-
*
|
|
63
|
+
* 别名并卡后每行变 3 个元素(文本 div + 操作 column_set + hr),
|
|
64
64
|
* 从 15 降为 8:3*8 - 1 + 2 = 25 个元素,仍在余量内,同时保证手机窄屏
|
|
65
65
|
* 每条指令的操作控件(执行/别名/删除)不被挤压。
|
|
66
66
|
*/
|
|
@@ -148,7 +148,7 @@ const APPROVAL_ACTION_SPECS = {
|
|
|
148
148
|
}),
|
|
149
149
|
successToast: '审批已提交',
|
|
150
150
|
failureToast: (msg) => {
|
|
151
|
-
//
|
|
151
|
+
// 审批已过期:给用户明确反馈,不静默不误导(否则点了允许无任何反馈)。
|
|
152
152
|
const content = /state=expired/.test(msg)
|
|
153
153
|
? '⏰ 审批已过期,无法响应'
|
|
154
154
|
: `审批响应失败:${msg}`;
|
|
@@ -296,7 +296,7 @@ const IMMEDIATE_ACTION_CMDS = new Set([
|
|
|
296
296
|
'config.save',
|
|
297
297
|
// 审批响应/权限切换必须即时触达在途 run(同 stop 类控制动作)。若走串行
|
|
298
298
|
// 队列会排在等待审批的 run 之后形成死锁:run 不结束审批不执行,run 结束
|
|
299
|
-
// coordinator
|
|
299
|
+
// coordinator 已删响应空转(approval.respond 落进排队卡、审批永不生效)。
|
|
300
300
|
...APPROVAL_ACTION_CMDS,
|
|
301
301
|
]);
|
|
302
302
|
/**
|
|
@@ -304,7 +304,7 @@ const IMMEDIATE_ACTION_CMDS = new Set([
|
|
|
304
304
|
* sessionId / cwd / defaultAgent,必须与入站装配器的 700ms 静默窗口定序:
|
|
305
305
|
* 控制层(index.ts)在执行它们之前先 flush 该 user/chat 的装配窗口,保证窗口内
|
|
306
306
|
* 先到的文本消息在本动作之前 commit(绑定动作前的旧状态)。否则窗口内消息会
|
|
307
|
-
*
|
|
307
|
+
* 漂移到动作之后的新状态(输入「重新构建」后立即点「新会话」,
|
|
308
308
|
* 消息落到新会话)。
|
|
309
309
|
*
|
|
310
310
|
* 文本命令(/new、/cd、/resume、/config)已由 index.ts 命令分支先 flush,
|
|
@@ -321,7 +321,7 @@ export const SESSION_MUTATING_ACTION_CMDS = new Set([
|
|
|
321
321
|
* 会产生压缩工作的卡片动作(单源)。这类条目占用 lane 时必须自报身份:
|
|
322
322
|
* 排队卡写明「正在执行: 🗜 Compact」而不是「前面还有 0 条」,并且期间禁用
|
|
323
323
|
* 「立即执行」——该按钮走 interruptCurrentRun,会连带中断正在跑的压缩
|
|
324
|
-
* (
|
|
324
|
+
* (Compact 占 lane 期间新消息的排队卡自相矛盾,且按钮
|
|
325
325
|
* 一点就会杀掉压缩)。同时它们也不允许编辑(预览不是用户文本)。
|
|
326
326
|
*/
|
|
327
327
|
export const COMPACT_ACTION_CMDS = new Set(['compact', 'resume.compact']);
|
|
@@ -400,8 +400,8 @@ export class CommandRouter {
|
|
|
400
400
|
*
|
|
401
401
|
* 草稿(pendingConfig)是全局一份:/config 文本命令重开卡片时沿用同一份草稿
|
|
402
402
|
* (这是既有语义,不改)。但**视图**是每张卡一份 —— 只刷新被点击的那张,其余
|
|
403
|
-
*
|
|
404
|
-
*
|
|
403
|
+
* 卡片就停在旧视图上:用户在旧卡上看到的和点保存实际提交的是两套内容。
|
|
404
|
+
* 这里记住所有交互过的 config 卡,任何一次编辑/保存
|
|
405
405
|
* 后把草稿刷到每张卡,保证「看到的就是会保存的」。
|
|
406
406
|
*/
|
|
407
407
|
configCardIds = new Set();
|
|
@@ -412,7 +412,7 @@ export class CommandRouter {
|
|
|
412
412
|
* 条目的身份必须与「一次入队动作」1:1(queuedTasks 的定位、queueCardMessages
|
|
413
413
|
* 的卡片更新、taskIndex 的取消判据全部按这个 key 做 lookup)。复用卡片 id
|
|
414
414
|
* 会让同 key 的多条任务互相顶替:位置显示命中最早那条兄弟、第一条开始时删掉
|
|
415
|
-
*
|
|
415
|
+
* 索引导致其余兄弟被判「已撤销」静默丢弃(连点 Compact
|
|
416
416
|
* 三次 → 三张排队卡都显示「第 1 位」、两张永久停在「等待中」,只压缩一次)。
|
|
417
417
|
*
|
|
418
418
|
* Date.now() 不够(两次同步调用可落在同一毫秒);计数器与时间无关,恒唯一。
|
|
@@ -433,7 +433,7 @@ export class CommandRouter {
|
|
|
433
433
|
/** Session reader 注册中心(public:测试注入/读取,替代 as unknown as)。 */
|
|
434
434
|
sessionReaderRegistry;
|
|
435
435
|
/**
|
|
436
|
-
* 串行化 config.*
|
|
436
|
+
* 串行化 config.* 卡片回调。
|
|
437
437
|
* CardKit 2.0 input/button 回调经 `enqueueImmediate` 分发,**不进串行队列**,
|
|
438
438
|
* 多次快速点击下拉/文本框会并发读写 `pendingConfig` 并同时调
|
|
439
439
|
* `updateCardInPlace` → Feishu API 乱序到达,控件卡死在某个状态。
|
|
@@ -483,7 +483,7 @@ export class CommandRouter {
|
|
|
483
483
|
/**
|
|
484
484
|
* Route a message: if it starts with /, handle as command; if it starts with !, execute as bash; otherwise forward to Claude.
|
|
485
485
|
*
|
|
486
|
-
*
|
|
486
|
+
* 命令识别前置条件(`inbound-unified-input-design.md` §5.6):
|
|
487
487
|
* 裸前缀判定会被结构占位符命中 —— SDK 把富文本里的图片渲染成 ``,
|
|
488
488
|
* 首字符恰好是 `!`,整段用户消息被当 shell 命令执行(`executeBash "[image](img_v3_…"`)。
|
|
489
489
|
* 因此命令前缀只在「调用方声明纯文本 + 剥离占位符后非空且无占位符 + 首字符是 / 或 !」
|
|
@@ -587,40 +587,15 @@ export class CommandRouter {
|
|
|
587
587
|
return await this.handleWsSort(value, ctx);
|
|
588
588
|
case 'ws.remove':
|
|
589
589
|
return await this.handleWsRemove(value, ctx);
|
|
590
|
-
case 'resume.use':
|
|
591
|
-
|
|
592
|
-
// (both active and completed cards include the agent field).
|
|
593
|
-
const resolvedAgent = resolveAgentKind(value.agent, this.config.defaultAgent);
|
|
594
|
-
const entry = this.sessionStore.get(ctx.userId);
|
|
595
|
-
const cwd = entry?.cwd;
|
|
596
|
-
if (!cwd) {
|
|
597
|
-
await this.bridge.sendResult({ text: '请先 /cd 设置工作目录' }, ctx);
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
// 校验前不得写入 sessionId。旧实现先无条件 setSessionId 再调
|
|
601
|
-
// cmdResume 校验,过期卡片(session 不在当前 cwd)校验失败时 store 已被
|
|
602
|
-
// 污染;value.sessionId 缺失时还会静默清空已有绑定。现在统一走 cmdResume
|
|
603
|
-
// 的已验证路径(:2352 校验通过才写入),sessionId 缺失直接报错。
|
|
604
|
-
if (!value.sessionId) {
|
|
605
|
-
await this.bridge.sendResult({ text: '缺少 sessionId,请重新从 /resume 列表选择会话' }, ctx);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
await this.bridge.sendResult(this.cmdResume([resolvedAgent, value.sessionId], ctx), ctx);
|
|
590
|
+
case 'resume.use':
|
|
591
|
+
await this.handleResumeUse(value, ctx);
|
|
609
592
|
return;
|
|
610
|
-
}
|
|
611
593
|
case 'resume.page':
|
|
612
594
|
return this.handleResumePage(value, ctx);
|
|
613
595
|
case 'active.page':
|
|
614
596
|
return this.handleActivePage(value, ctx);
|
|
615
597
|
case 'new-session':
|
|
616
|
-
this.
|
|
617
|
-
clearSessionCwd: true,
|
|
618
|
-
});
|
|
619
|
-
{
|
|
620
|
-
const cwd = this.sessionStore.getCwd(ctx.userId) ?? '(未设置)';
|
|
621
|
-
const agentName = agentDisplayName(this.config.defaultAgent);
|
|
622
|
-
await this.bridge.sendResult({ text: `已创建新 ${agentName} 会话,下一条消息将开始全新对话\n📁 ${cwd}` }, ctx);
|
|
623
|
-
}
|
|
598
|
+
await this.handleNewSession(ctx);
|
|
624
599
|
return;
|
|
625
600
|
case 'stop':
|
|
626
601
|
await this.handleCardStop(value, ctx);
|
|
@@ -664,43 +639,93 @@ export class CommandRouter {
|
|
|
664
639
|
case 'config.set':
|
|
665
640
|
case 'config.input':
|
|
666
641
|
case 'config.save':
|
|
667
|
-
//
|
|
642
|
+
// 串行化所有 config.* 动作。CardKit 2.0 回调经 enqueueImmediate
|
|
668
643
|
// 不进串行队列,多次快速点击下拉/文本框会并发读写 pendingConfig 并同时
|
|
669
644
|
// 调 updateCardInPlace → Feishu API 乱序到达导致控件卡死。
|
|
670
|
-
//
|
|
645
|
+
// 返回 enqueueConfigAction 的结果以支持 toast 响应。
|
|
671
646
|
return this.enqueueConfigAction(value, ctx);
|
|
672
647
|
case 'compact':
|
|
673
|
-
await this.
|
|
648
|
+
await this.handleCompact(value, ctx);
|
|
674
649
|
return;
|
|
675
650
|
case 'resume.compact':
|
|
676
|
-
await this.
|
|
651
|
+
await this.handleResumeCompact(value, ctx);
|
|
677
652
|
return;
|
|
678
653
|
default: {
|
|
679
|
-
// Help card buttons: help.<cmd> → execute /<cmd>
|
|
680
654
|
if (value.cmd.startsWith('help.')) {
|
|
681
|
-
|
|
682
|
-
const result = await this.executeCommand(`/${subCmd}`, ctx);
|
|
683
|
-
if (result) {
|
|
684
|
-
await this.bridge.sendResult(result, ctx);
|
|
685
|
-
}
|
|
686
|
-
// /exit、/restart 等命令通过 pendingExit 表达"回复送达后退出"。
|
|
687
|
-
// handle() 在 sendResult 后消费它;help.* 按钮点击走 handleCardAction,
|
|
688
|
-
// 必须同样消费,否则点击 /restart 按钮 spawn 成功后旧进程不退出,
|
|
689
|
-
// 新进程撞单例锁退出 → 重启两头落空(2026-08-01 红绿 anchor 锁定)。
|
|
690
|
-
if (this.pendingExit) {
|
|
691
|
-
await this.flushInbound?.();
|
|
692
|
-
this.exitHandler();
|
|
693
|
-
}
|
|
655
|
+
await this.handleHelpAction(value, ctx);
|
|
694
656
|
return;
|
|
695
657
|
}
|
|
696
|
-
|
|
697
|
-
// don't silently swallow unknown card actions.
|
|
698
|
-
getLogger().warn(`[router] unknown card action: ${value.cmd}`);
|
|
699
|
-
await this.bridge.sendResult({ text: `⚠️ 未知的卡片操作: ${value.cmd}` }, ctx);
|
|
658
|
+
await this.handleUnknownCardAction(value, ctx);
|
|
700
659
|
return;
|
|
701
660
|
}
|
|
702
661
|
}
|
|
703
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* resume.use: 从 /resume 列表卡切换到所选会话。
|
|
665
|
+
*/
|
|
666
|
+
async handleResumeUse(value, ctx) {
|
|
667
|
+
// resume.use carries an agent field from /resume [agent] list cards
|
|
668
|
+
// (both active and completed cards include the agent field).
|
|
669
|
+
const resolvedAgent = resolveAgentKind(value.agent, this.config.defaultAgent);
|
|
670
|
+
const entry = this.sessionStore.get(ctx.userId);
|
|
671
|
+
const cwd = entry?.cwd;
|
|
672
|
+
if (!cwd) {
|
|
673
|
+
await this.bridge.sendResult({ text: '请先 /cd 设置工作目录' }, ctx);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
// 校验前不得写入 sessionId。旧实现先无条件 setSessionId 再调
|
|
677
|
+
// cmdResume 校验,过期卡片(session 不在当前 cwd)校验失败时 store 已被
|
|
678
|
+
// 污染;value.sessionId 缺失时还会静默清空已有绑定。现在统一走 cmdResume
|
|
679
|
+
// 的已验证路径(:2352 校验通过才写入),sessionId 缺失直接报错。
|
|
680
|
+
if (!value.sessionId) {
|
|
681
|
+
await this.bridge.sendResult({ text: '缺少 sessionId,请重新从 /resume 列表选择会话' }, ctx);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
await this.bridge.sendResult(this.cmdResume([resolvedAgent, value.sessionId], ctx), ctx);
|
|
685
|
+
}
|
|
686
|
+
/** new-session: 清空当前会话绑定,下一条消息开始全新对话。 */
|
|
687
|
+
async handleNewSession(ctx) {
|
|
688
|
+
this.sessionStore.clearSessionId(ctx.userId, this.config.defaultAgent, {
|
|
689
|
+
clearSessionCwd: true,
|
|
690
|
+
});
|
|
691
|
+
const cwd = this.sessionStore.getCwd(ctx.userId) ?? '(未设置)';
|
|
692
|
+
const agentName = agentDisplayName(this.config.defaultAgent);
|
|
693
|
+
await this.bridge.sendResult({ text: `已创建新 ${agentName} 会话,下一条消息将开始全新对话\n📁 ${cwd}` }, ctx);
|
|
694
|
+
}
|
|
695
|
+
/** compact: 对当前会话发起压缩(bridge 收口)。 */
|
|
696
|
+
async handleCompact(value, ctx) {
|
|
697
|
+
await this.bridge.handleCompactAction(value, ctx);
|
|
698
|
+
}
|
|
699
|
+
/** resume.compact: 对 /resume 选中的历史会话发起压缩(bridge 收口)。 */
|
|
700
|
+
async handleResumeCompact(value, ctx) {
|
|
701
|
+
await this.bridge.handleResumeCompact(value, ctx);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Help card buttons: help.<cmd> → execute /<cmd>.
|
|
705
|
+
* /exit、/restart 等命令通过 pendingExit 表达"回复送达后退出"。
|
|
706
|
+
* handle() 在 sendResult 后消费它;help.* 按钮点击走 handleCardAction,
|
|
707
|
+
* 必须同样消费,否则点击 /restart 按钮 spawn 成功后旧进程不退出,
|
|
708
|
+
* 新进程撞单例锁退出 → 重启两头落空(2026-08-01 红绿 anchor 锁定)。
|
|
709
|
+
*/
|
|
710
|
+
async handleHelpAction(value, ctx) {
|
|
711
|
+
const subCmd = value.cmd.slice(5); // e.g. "status", "ps", "stop"
|
|
712
|
+
const result = await this.executeCommand(`/${subCmd}`, ctx);
|
|
713
|
+
if (result) {
|
|
714
|
+
await this.bridge.sendResult(result, ctx);
|
|
715
|
+
}
|
|
716
|
+
if (this.pendingExit) {
|
|
717
|
+
await this.flushInbound?.();
|
|
718
|
+
this.exitHandler();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Design constraint: miss paths must reply via bridge.sendResult —
|
|
723
|
+
* don't silently swallow unknown card actions.
|
|
724
|
+
*/
|
|
725
|
+
async handleUnknownCardAction(value, ctx) {
|
|
726
|
+
getLogger().warn(`[router] unknown card action: ${value.cmd}`);
|
|
727
|
+
await this.bridge.sendResult({ text: `⚠️ 未知的卡片操作: ${value.cmd}` }, ctx);
|
|
728
|
+
}
|
|
704
729
|
/**
|
|
705
730
|
* Handle approval-related card actions.
|
|
706
731
|
* Routes to the appropriate bridge method based on value.cmd.
|
|
@@ -1330,7 +1355,7 @@ export class CommandRouter {
|
|
|
1330
1355
|
cardMessageId: ctx.messageId,
|
|
1331
1356
|
});
|
|
1332
1357
|
const stopped = interrupt.stopped;
|
|
1333
|
-
//
|
|
1358
|
+
// card stop button silently no-op'd when the
|
|
1334
1359
|
// run had already exited. Give the user a visible result.
|
|
1335
1360
|
if (!stopped) {
|
|
1336
1361
|
await this.bridge.sendResult({ text: '该任务已结束,无需终止' }, ctx);
|
|
@@ -1810,8 +1835,6 @@ export class CommandRouter {
|
|
|
1810
1835
|
this.configCardIds.add(messageId);
|
|
1811
1836
|
while (this.configCardIds.size > MAX_CONFIG_CARDS) {
|
|
1812
1837
|
const oldest = this.configCardIds.values().next().value;
|
|
1813
|
-
if (oldest === undefined)
|
|
1814
|
-
break;
|
|
1815
1838
|
this.configCardIds.delete(oldest);
|
|
1816
1839
|
}
|
|
1817
1840
|
}
|
|
@@ -1858,7 +1881,7 @@ export class CommandRouter {
|
|
|
1858
1881
|
}
|
|
1859
1882
|
}
|
|
1860
1883
|
/**
|
|
1861
|
-
* 串行化 config.*
|
|
1884
|
+
* 串行化 config.* 卡片回调(修复连击卡死 bug)。
|
|
1862
1885
|
*
|
|
1863
1886
|
* CardKit 2.0 input/button 回调经 `enqueueImmediate` 分发,不进 bridge 的
|
|
1864
1887
|
* 串行队列;多次快速点击下拉/文本框会并发读写 `pendingConfig` 并同时调
|
|
@@ -1899,7 +1922,7 @@ export class CommandRouter {
|
|
|
1899
1922
|
}
|
|
1900
1923
|
case 'config.input': {
|
|
1901
1924
|
// Handle input field: value.key is the field key.
|
|
1902
|
-
//
|
|
1925
|
+
// CardKit 2.0 input 提交(回车/完成键)触发 callback,回传 input_value。
|
|
1903
1926
|
// Fall back to formValue[key]: the SDK currently reports input values
|
|
1904
1927
|
// via action.formValue for form-wrapped inputs.
|
|
1905
1928
|
const key = value.key;
|
|
@@ -1934,8 +1957,8 @@ export class CommandRouter {
|
|
|
1934
1957
|
// defaultAgent 切换的 session 处理)抽到共享方法,与文本直写路径共用。
|
|
1935
1958
|
const switchResult = this.propagateConfigSave(oldDefaultAgent, updates, ctx);
|
|
1936
1959
|
this.pendingConfig = null;
|
|
1937
|
-
//
|
|
1938
|
-
//
|
|
1960
|
+
// 原地更新卡片:保存后卡片刷新为已保存状态。
|
|
1961
|
+
// 卡片刷新失败不吞切换通知——保存已成功,仅记录日志,
|
|
1939
1962
|
// 后续切换消息照常发送;真正写盘/传播阶段的失败才走外层 catch 报「保存失败」。
|
|
1940
1963
|
try {
|
|
1941
1964
|
// host 可能已变更 → 重新预取 DSH 目录(内部按 host 缓存,未变不重复拉)
|
|
@@ -1945,7 +1968,7 @@ export class CommandRouter {
|
|
|
1945
1968
|
catch (refreshErr) {
|
|
1946
1969
|
getLogger().warn(`[router] config.save 卡片刷新失败(保存已成功): ${refreshErr.message}`);
|
|
1947
1970
|
}
|
|
1948
|
-
//
|
|
1971
|
+
// agent 切换时发送 Resume 卡片(替代纯文本通知),
|
|
1949
1972
|
// 让用户直观看到新 agent 的会话状态和历史。
|
|
1950
1973
|
// 发送失败(resolve false)时兜底回退 toast 即时反馈。
|
|
1951
1974
|
if (switchResult.notice) {
|
|
@@ -1996,7 +2019,7 @@ export class CommandRouter {
|
|
|
1996
2019
|
// idle.watchdogMinutes 改动需同步到 bridge 的 idleTimeoutMs
|
|
1997
2020
|
this.idleTimeoutMs = this.config.idle.watchdogMinutes * 60_000;
|
|
1998
2021
|
this.bridge.setIdleTimeout(this.idleTimeoutMs);
|
|
1999
|
-
//
|
|
2022
|
+
// 检测是否有任何 agent 配置变更或 defaultAgent 切换
|
|
2000
2023
|
const hasDefaultAgentChange = oldDefaultAgent !== this.config.defaultAgent;
|
|
2001
2024
|
const changedKeys = Object.keys(updates);
|
|
2002
2025
|
const hasAgentConfigChange = changedKeys.includes('defaultAgent') ||
|
|
@@ -2070,7 +2093,7 @@ export class CommandRouter {
|
|
|
2070
2093
|
};
|
|
2071
2094
|
parkDshSessionBoundary(changedKeys.includes('agents.dsh.agentPreset'), 'preset');
|
|
2072
2095
|
parkDshSessionBoundary(changedKeys.includes('agents.dsh.host'), 'host');
|
|
2073
|
-
//
|
|
2096
|
+
// 同步 agent 配置到 agentChoices(用于切换 agent 时恢复配置)
|
|
2074
2097
|
const currentAgent = this.config.defaultAgent;
|
|
2075
2098
|
if (touchesAgentsSubtree(changedKeys, currentAgent)) {
|
|
2076
2099
|
this.config = syncAgentChoices(this.config, currentAgent);
|
|
@@ -2089,7 +2112,7 @@ export class CommandRouter {
|
|
|
2089
2112
|
if (hasApprovalConfigChange) {
|
|
2090
2113
|
this.bridge.syncActiveApprovalModes();
|
|
2091
2114
|
}
|
|
2092
|
-
//
|
|
2115
|
+
// 任何 agent 配置变更或 defaultAgent 切换,都清除 runner 缓存
|
|
2093
2116
|
if (hasDefaultAgentChange || hasAgentConfigChange) {
|
|
2094
2117
|
this.bridge.clearRunners();
|
|
2095
2118
|
}
|
|
@@ -2161,8 +2184,8 @@ export class CommandRouter {
|
|
|
2161
2184
|
// --- Command implementations ---
|
|
2162
2185
|
cmdHelp() {
|
|
2163
2186
|
// 按钮组(可直接点击触发;按钮 label 只保留子命令,不含参数)
|
|
2164
|
-
//
|
|
2165
|
-
//
|
|
2187
|
+
// /ws 子命令 (save|use|remove) 移到右侧文本说明,按钮只显示 /ws
|
|
2188
|
+
// 按钮 label 按长度升序排列(短在上,长在下),视觉更整齐
|
|
2166
2189
|
const buttonCommands = [
|
|
2167
2190
|
{ cmd: 'help', label: '/help /h', desc: '显示此帮助' },
|
|
2168
2191
|
{ cmd: 'ps', label: '/ps', desc: '查看是否有 agent 进程在跑' },
|
|
@@ -2203,11 +2226,11 @@ export class CommandRouter {
|
|
|
2203
2226
|
];
|
|
2204
2227
|
const bodyElements = [];
|
|
2205
2228
|
// 按钮组 — CardKit 2.0: column_set + column + button with behaviors
|
|
2206
|
-
//
|
|
2229
|
+
// 对齐修复(方案 A):两列都用 width:'weighted' + 固定 weight。
|
|
2207
2230
|
// 根因:每行独立 column_set + width:'auto' → 列宽随按钮/文本内容变化 →
|
|
2208
2231
|
// 跨行不对齐(按钮列右边界参差)。改用 weighted 后,每行 column_set
|
|
2209
2232
|
// 总宽相同(同级 body.elements),weight 比例一致,列宽跨行恒等。
|
|
2210
|
-
//
|
|
2233
|
+
// 手机排版修复:weight 1:3 → 2:3。1:3 时按钮列只占 25%,
|
|
2211
2234
|
// 手机窄屏(~330px 可用)下约 82px,容不下 `/config /c`(10 字符 ≈ 100px),
|
|
2212
2235
|
// 按钮文字被截断成 "..."。改 2:3 后按钮列占 40% ≈ 132px,足够显示
|
|
2213
2236
|
// 最长的 /config /c,文本列仍占 60% 足以放下描述。
|
|
@@ -2460,8 +2483,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2460
2483
|
}
|
|
2461
2484
|
cmdNew(ctx) {
|
|
2462
2485
|
// 只清 sessionId 保留 cwd — 否则 /new 之后 /resume 会提示"请先 /cd
|
|
2463
|
-
// 设置工作目录",但用户的 cwd
|
|
2464
|
-
//
|
|
2486
|
+
// 设置工作目录",但用户的 cwd 还在,用户 /new → /resume 后会被误导
|
|
2487
|
+
// 以为需要重新 /cd。
|
|
2465
2488
|
const agentName = agentDisplayName(this.config.defaultAgent);
|
|
2466
2489
|
this.sessionStore.clearSessionId(ctx.userId, this.config.defaultAgent, {
|
|
2467
2490
|
clearSessionCwd: true,
|
|
@@ -2546,38 +2569,66 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2546
2569
|
}
|
|
2547
2570
|
/** Build card for auto-resumed session with new session button */
|
|
2548
2571
|
buildAutoResumeCard(cwd, session, _ctx) {
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2572
|
+
const agentKind = this.config.defaultAgent;
|
|
2573
|
+
return this.buildResumeLikeCard({
|
|
2574
|
+
cwd,
|
|
2575
|
+
sessionId: session.sessionId,
|
|
2576
|
+
agentKind,
|
|
2577
|
+
// 仅 auto-resume 入口带 Compact 按钮(runner 支持压缩时);
|
|
2578
|
+
// config-switch 卡不带。
|
|
2579
|
+
compact: this.canCompactSession(agentKind, cwd)
|
|
2580
|
+
? { sessionId: session.sessionId, agentKind }
|
|
2581
|
+
: undefined,
|
|
2582
|
+
titleBase: '自动恢复会话',
|
|
2583
|
+
idleIcon: '🔁',
|
|
2584
|
+
resumedLine: `已恢复最近会话: **${session.sessionId}**`,
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Shared body of the two "resume-like" cards(cwd 切换后的 auto-resume 与
|
|
2589
|
+
* config.save 换 agent):两者都是同一序列 readSessionDisplayState →
|
|
2590
|
+
* buildResumeActionButtons → buildSessionHistoryCard,差异仅 4 处——
|
|
2591
|
+
* Compact 按钮(仅 auto-resume,canCompactSession 门控)、标题文案、
|
|
2592
|
+
* headerText 的「已恢复…会话」行与可选尾部行、空会话占位——全部参数化,
|
|
2593
|
+
* 共享序列只留一份(Clean Code DRY)。
|
|
2594
|
+
*/
|
|
2595
|
+
buildResumeLikeCard(opts) {
|
|
2596
|
+
const agentName = agentDisplayName(opts.agentKind);
|
|
2597
|
+
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(opts.sessionId, opts.cwd, {
|
|
2598
|
+
// 默认 agent 时 readSessionDisplayState 内部仍走 this.sessionReader
|
|
2599
|
+
//(agentKind === defaultAgent 不触发 registry 查找),与旧的不传等价。
|
|
2600
|
+
agentKind: opts.agentKind,
|
|
2601
|
+
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
2602
|
+
});
|
|
2551
2603
|
const actions = this.buildResumeActionButtons({
|
|
2552
2604
|
isActive,
|
|
2553
2605
|
activeRunRunId,
|
|
2554
|
-
cwd,
|
|
2555
|
-
compact:
|
|
2556
|
-
? { sessionId: session.sessionId, agentKind: this.config.defaultAgent }
|
|
2557
|
-
: undefined,
|
|
2606
|
+
cwd: opts.cwd,
|
|
2607
|
+
compact: opts.compact,
|
|
2558
2608
|
newSession: true,
|
|
2559
2609
|
});
|
|
2560
2610
|
const card = buildSessionHistoryCard({
|
|
2561
|
-
sessionId:
|
|
2562
|
-
cwd,
|
|
2611
|
+
sessionId: opts.sessionId,
|
|
2612
|
+
cwd: opts.cwd,
|
|
2563
2613
|
displayTitle,
|
|
2564
2614
|
aiTitle,
|
|
2565
2615
|
recap,
|
|
2566
2616
|
events: content,
|
|
2567
2617
|
usage,
|
|
2568
2618
|
}, {
|
|
2569
|
-
agentKind:
|
|
2570
|
-
headerText: `📂 \`${cwd}\`\n
|
|
2571
|
-
title: `${isActive ?
|
|
2619
|
+
agentKind: opts.agentKind,
|
|
2620
|
+
headerText: `📂 \`${opts.cwd}\`\n${opts.resumedLine}${opts.headerTail ? `\n${opts.headerTail}` : ''}`,
|
|
2621
|
+
title: `${isActive ? `⏳ ${opts.titleBase}(完成中)` : `${opts.idleIcon} ${opts.titleBase}`} · ${agentName}`,
|
|
2572
2622
|
usageResult: 'success',
|
|
2623
|
+
...(opts.emptyPlaceholder !== undefined ? { emptyPlaceholder: opts.emptyPlaceholder } : {}),
|
|
2573
2624
|
actions,
|
|
2574
2625
|
});
|
|
2575
2626
|
return { card };
|
|
2576
2627
|
}
|
|
2577
2628
|
/**
|
|
2578
2629
|
* Build card for config.save agent switch.
|
|
2579
|
-
* When the new agent has a restored session, renders a Resume card (
|
|
2580
|
-
*
|
|
2630
|
+
* When the new agent has a restored session, renders a Resume card (shared
|
|
2631
|
+
* body via buildResumeLikeCard) with session history + stop/new buttons.
|
|
2581
2632
|
* When the session was cleared, renders a compact notice card instead.
|
|
2582
2633
|
*/
|
|
2583
2634
|
buildConfigSwitchCard(switchResult, ctx) {
|
|
@@ -2601,62 +2652,25 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2601
2652
|
},
|
|
2602
2653
|
};
|
|
2603
2654
|
}
|
|
2604
|
-
// Has session → Resume card (
|
|
2605
|
-
//
|
|
2606
|
-
//
|
|
2607
|
-
|
|
2608
|
-
? this.sessionReader
|
|
2609
|
-
: this.sessionReaderRegistry.get(newAgent);
|
|
2610
|
-
if (!readerForNewAgent) {
|
|
2611
|
-
// Fallback: compact notice card with the notice text
|
|
2612
|
-
const elements = [markdownDiv(switchResult.notice)];
|
|
2613
|
-
elements.push({
|
|
2614
|
-
tag: 'column_set',
|
|
2615
|
-
columns: [
|
|
2616
|
-
{
|
|
2617
|
-
tag: 'column',
|
|
2618
|
-
width: 'auto',
|
|
2619
|
-
elements: [newSessionButton()],
|
|
2620
|
-
},
|
|
2621
|
-
],
|
|
2622
|
-
});
|
|
2623
|
-
return {
|
|
2624
|
-
card: {
|
|
2625
|
-
schema: '2.0',
|
|
2626
|
-
config: { wide_screen_mode: true },
|
|
2627
|
-
header: { title: { tag: 'plain_text', content: `🔀 切换到 ${agentName}` } },
|
|
2628
|
-
body: { elements },
|
|
2629
|
-
},
|
|
2630
|
-
};
|
|
2631
|
-
}
|
|
2632
|
-
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(sessionId, cwd, {
|
|
2633
|
-
agentKind: newAgent,
|
|
2634
|
-
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
2635
|
-
});
|
|
2636
|
-
// Action buttons: stop (active) + new session
|
|
2637
|
-
const actions = this.buildResumeActionButtons({
|
|
2638
|
-
isActive,
|
|
2639
|
-
activeRunRunId,
|
|
2655
|
+
// Has session → Resume card (shared body with buildAutoResumeCard)
|
|
2656
|
+
// SessionReaderRegistry.get() throws when the agent is not registered —
|
|
2657
|
+
// a null-reader fallback branch here is unreachable either way.
|
|
2658
|
+
return this.buildResumeLikeCard({
|
|
2640
2659
|
cwd,
|
|
2641
|
-
newSession: true,
|
|
2642
|
-
});
|
|
2643
|
-
const card = buildSessionHistoryCard({
|
|
2644
2660
|
sessionId,
|
|
2645
|
-
cwd,
|
|
2646
|
-
displayTitle,
|
|
2647
|
-
aiTitle,
|
|
2648
|
-
recap,
|
|
2649
|
-
events: content,
|
|
2650
|
-
usage,
|
|
2651
|
-
}, {
|
|
2652
2661
|
agentKind: newAgent,
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2662
|
+
titleBase: '切换 Agent',
|
|
2663
|
+
idleIcon: '🔀',
|
|
2664
|
+
// 切换语义(已使用所选 / 将继续之前的 / 已清空)必须随卡可见:
|
|
2665
|
+
// 2026-08-13 换 Resume 卡时这条信息一度只在「无 reader」回退分支
|
|
2666
|
+
// 渲染,生产路径(registry 恒有 reader)上不可见——卡上只剩
|
|
2667
|
+
// sessionId,用户无法区分显式选择、停车恢复与清空。notice 含
|
|
2668
|
+
// sessionId,与「已恢复会话」行并存(agent-switch-entry-points 与
|
|
2669
|
+
// arrival-baseline 两族 anchor 分别钉住两行)。
|
|
2670
|
+
resumedLine: `已恢复会话: **${sessionId}**`,
|
|
2671
|
+
headerTail: switchResult.notice,
|
|
2656
2672
|
emptyPlaceholder: '_该会话暂无新消息可显示(最后一条为用户输入)_',
|
|
2657
|
-
actions,
|
|
2658
2673
|
});
|
|
2659
|
-
return { card };
|
|
2660
2674
|
}
|
|
2661
2675
|
/**
|
|
2662
2676
|
* Whether a session of the given agent kind can be compacted from a resume
|
|
@@ -2701,7 +2715,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2701
2715
|
* @param offset 分页起点(条目数)
|
|
2702
2716
|
* @param rootDir 浏览起点:卡片「返回」按钮回到这里。省略时默认 = 本次列出的
|
|
2703
2717
|
* 目录(`/ls <dir>` 的起点即该目录),因此「返回」不会把用户
|
|
2704
|
-
* 丢回
|
|
2718
|
+
* 丢回 工作目录。
|
|
2705
2719
|
* @param q 关键词筛选:只匹配**当前层**条目名(不递归),大小写不敏感。
|
|
2706
2720
|
* 省略/空 = 无筛选。
|
|
2707
2721
|
*/
|
|
@@ -2791,7 +2805,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2791
2805
|
}
|
|
2792
2806
|
cmdActive(_args, _ctx, offset = 0) {
|
|
2793
2807
|
// List all active runs from lark-remote memory (not from file system scan)
|
|
2794
|
-
//
|
|
2808
|
+
// (only shows runs started by THIS lark-remote process)
|
|
2795
2809
|
const activeRuns = this.bridge.getActiveRuns();
|
|
2796
2810
|
const activeBashRuns = this.bridge.getActiveBashRuns();
|
|
2797
2811
|
if (activeRuns.length === 0 && activeBashRuns.length === 0) {
|
|
@@ -2815,54 +2829,31 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2815
2829
|
const elements = [];
|
|
2816
2830
|
let remaining = pageSize;
|
|
2817
2831
|
let skipped = safeOffset;
|
|
2818
|
-
// Agent runs section
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
tag: '
|
|
2828
|
-
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
else {
|
|
2844
|
-
skipped -= activeRuns.length;
|
|
2845
|
-
}
|
|
2846
|
-
// Bash runs section
|
|
2847
|
-
if (activeBashRuns.length > 0 && skipped < activeBashRuns.length && remaining > 0) {
|
|
2848
|
-
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '## 💻 Bash 命令' } });
|
|
2849
|
-
const start = skipped;
|
|
2850
|
-
const end = Math.min(start + remaining, activeBashRuns.length);
|
|
2851
|
-
for (let i = start; i < end; i++) {
|
|
2852
|
-
const run = activeBashRuns[i];
|
|
2853
|
-
const statusLabel = run.terminal === 'running' ? '运行中' : '处理中';
|
|
2854
|
-
elements.push({
|
|
2855
|
-
tag: 'div',
|
|
2856
|
-
text: { tag: 'lark_md', content: `**\`${run.command}\`**` },
|
|
2857
|
-
});
|
|
2858
|
-
elements.push({
|
|
2859
|
-
tag: 'div',
|
|
2860
|
-
text: { tag: 'lark_md', content: `> 目录: ${run.cwd} \n> 状态: ${statusLabel}` },
|
|
2861
|
-
});
|
|
2862
|
-
elements.push(cardButton('⏹ 停止', { cmd: 'stop', runId: run.runId, cwd: run.cwd }, { type: 'danger', size: 'small' }));
|
|
2863
|
-
elements.push({ tag: 'hr' });
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2832
|
+
// Agent runs section(跨段 offset 分配:agent 段吃满 remaining 才轮到 bash 段)
|
|
2833
|
+
({ remaining, skipped } = this.appendActiveCardSection(elements, activeRuns, '## 🤖 Agent 任务', (run, statusLabel) => [
|
|
2834
|
+
{
|
|
2835
|
+
tag: 'div',
|
|
2836
|
+
text: { tag: 'lark_md', content: `**📂 ${displayName(run.cwd)}**` },
|
|
2837
|
+
},
|
|
2838
|
+
{
|
|
2839
|
+
tag: 'div',
|
|
2840
|
+
text: {
|
|
2841
|
+
tag: 'lark_md',
|
|
2842
|
+
content: `> session: ${run.sessionId.slice(0, 12)}... \n> 状态: ${statusLabel}`,
|
|
2843
|
+
},
|
|
2844
|
+
},
|
|
2845
|
+
], remaining, skipped));
|
|
2846
|
+
// Bash runs section(末段:不再使用返回的记账值)
|
|
2847
|
+
this.appendActiveCardSection(elements, activeBashRuns, '## 💻 Bash 命令', (run, statusLabel) => [
|
|
2848
|
+
{
|
|
2849
|
+
tag: 'div',
|
|
2850
|
+
text: { tag: 'lark_md', content: `**\`${run.command}\`**` },
|
|
2851
|
+
},
|
|
2852
|
+
{
|
|
2853
|
+
tag: 'div',
|
|
2854
|
+
text: { tag: 'lark_md', content: `> 目录: ${run.cwd} \n> 状态: ${statusLabel}` },
|
|
2855
|
+
},
|
|
2856
|
+
], remaining, skipped);
|
|
2866
2857
|
// Pagination bar(复用 paginationBar:safeOffset 已对齐页边界,
|
|
2867
2858
|
// offset±pageSize 与原先 currentPage±1 的页码换算等价)
|
|
2868
2859
|
if (totalPages > 1) {
|
|
@@ -2887,6 +2878,34 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2887
2878
|
};
|
|
2888
2879
|
return { card };
|
|
2889
2880
|
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Render one section of the /active(/ps)card — the agent-runs and
|
|
2883
|
+
* bash-runs sections are the same loop (title line → per-entry rows →
|
|
2884
|
+
* stop button → trailing hr) and differ only in the title and the two
|
|
2885
|
+
* per-entry content rows, which the caller supplies via `renderRows`.
|
|
2886
|
+
*
|
|
2887
|
+
* Cross-section offset accounting (caller threads `remaining`/`skipped`
|
|
2888
|
+
* through the returned value): entries rendered decrement `remaining` and
|
|
2889
|
+
* consume the offset (`skipped` → 0, so the next section renders from its
|
|
2890
|
+
* own head); a section the offset/page has moved past deducts its full size
|
|
2891
|
+
* from `skipped` instead, so agent runs eat the offset before bash runs do.
|
|
2892
|
+
*/
|
|
2893
|
+
appendActiveCardSection(elements, runs, title, renderRows, remaining, skipped) {
|
|
2894
|
+
if (runs.length > 0 && skipped < runs.length && remaining > 0) {
|
|
2895
|
+
elements.push({ tag: 'div', text: { tag: 'lark_md', content: title } });
|
|
2896
|
+
const start = skipped;
|
|
2897
|
+
const end = Math.min(start + remaining, runs.length);
|
|
2898
|
+
for (let i = start; i < end; i++) {
|
|
2899
|
+
const run = runs[i];
|
|
2900
|
+
const statusLabel = run.terminal === 'running' ? '运行中' : '处理中';
|
|
2901
|
+
elements.push(...renderRows(run, statusLabel));
|
|
2902
|
+
elements.push(cardButton('⏹ 停止', { cmd: 'stop', runId: run.runId, cwd: run.cwd }, { type: 'danger', size: 'small' }));
|
|
2903
|
+
elements.push({ tag: 'hr' });
|
|
2904
|
+
}
|
|
2905
|
+
return { remaining: remaining - (end - start), skipped: 0 };
|
|
2906
|
+
}
|
|
2907
|
+
return { remaining, skipped: skipped - runs.length };
|
|
2908
|
+
}
|
|
2890
2909
|
async cmdConfig(args, ctx) {
|
|
2891
2910
|
// Ensure probe cache is populated before building the card.
|
|
2892
2911
|
await probeAllAgents();
|
|
@@ -2913,21 +2932,21 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2913
2932
|
// defaultAgent 不再漏判。
|
|
2914
2933
|
const switchResult = this.propagateConfigSave(oldDefaultAgent, { [mapAgentKey(key)]: value }, ctx);
|
|
2915
2934
|
this.pendingConfig = null; // 命令式写盘后清空暂存,避免不一致
|
|
2916
|
-
//
|
|
2935
|
+
// agent 切换时额外发送 Resume 卡片
|
|
2917
2936
|
if (switchResult.notice) {
|
|
2918
2937
|
const switchCard = this.buildConfigSwitchCard(switchResult, ctx);
|
|
2919
2938
|
// 文本路径返回 config card,Resume card 通过 bridge 异步发送
|
|
2920
2939
|
void this.bridge.sendResult(switchCard, ctx);
|
|
2921
2940
|
}
|
|
2922
2941
|
const card = this.buildConfigCard();
|
|
2923
|
-
if (discarded > 0
|
|
2942
|
+
if (discarded > 0) {
|
|
2943
|
+
// buildConfigCardFromTabs 恒返回 body.elements(至少含保存按钮),
|
|
2944
|
+
// 这里直接收窄插入提示条——同模块自己刚构建的值无需逐层探测。
|
|
2924
2945
|
const body = card.card.body;
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
});
|
|
2930
|
-
}
|
|
2946
|
+
body.elements.unshift({
|
|
2947
|
+
tag: 'div',
|
|
2948
|
+
text: { tag: 'lark_md', content: `⚠️ 已丢弃 ${discarded} 项未保存修改` },
|
|
2949
|
+
});
|
|
2931
2950
|
}
|
|
2932
2951
|
return card;
|
|
2933
2952
|
}
|