lark-remote 0.5.18 → 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 +32 -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 +120 -119
- package/dist/bridge/index.js +257 -695
- 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/bash-card-session.js +2 -0
- 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/card-session.d.ts +17 -0
- package/dist/card/card-session.js +35 -0
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-card-session.js +2 -0
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +77 -102
- package/dist/card/run-state.d.ts +35 -8
- package/dist/card/run-state.js +117 -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 +4 -2
- 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 +222 -202
- 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 +13 -8
- package/dist/runner/dsh/client.js +23 -15
- 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 +48 -2
- package/dist/runner/dsh/runner.js +288 -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 +25 -8
- 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.
|
|
@@ -1319,17 +1344,18 @@ export class CommandRouter {
|
|
|
1319
1344
|
* Handle stop card action: interrupt current run.
|
|
1320
1345
|
*/
|
|
1321
1346
|
async handleCardStop(value, ctx) {
|
|
1322
|
-
if (!
|
|
1347
|
+
if (!ctx.userId || !ctx.chatId || (!value.runId && !ctx.messageId)) {
|
|
1323
1348
|
await this.bridge.sendResult({ text: '⚠️ 无效的停止请求,缺少必要信息' }, ctx);
|
|
1324
1349
|
return;
|
|
1325
1350
|
}
|
|
1326
1351
|
const interrupt = await this.bridge.interruptCurrentRunDetailed({
|
|
1327
1352
|
userId: ctx.userId,
|
|
1328
1353
|
chatId: ctx.chatId,
|
|
1329
|
-
runId: value.runId,
|
|
1354
|
+
...(value.runId ? { runId: value.runId } : {}),
|
|
1355
|
+
cardMessageId: ctx.messageId,
|
|
1330
1356
|
});
|
|
1331
1357
|
const stopped = interrupt.stopped;
|
|
1332
|
-
//
|
|
1358
|
+
// card stop button silently no-op'd when the
|
|
1333
1359
|
// run had already exited. Give the user a visible result.
|
|
1334
1360
|
if (!stopped) {
|
|
1335
1361
|
await this.bridge.sendResult({ text: '该任务已结束,无需终止' }, ctx);
|
|
@@ -1809,8 +1835,6 @@ export class CommandRouter {
|
|
|
1809
1835
|
this.configCardIds.add(messageId);
|
|
1810
1836
|
while (this.configCardIds.size > MAX_CONFIG_CARDS) {
|
|
1811
1837
|
const oldest = this.configCardIds.values().next().value;
|
|
1812
|
-
if (oldest === undefined)
|
|
1813
|
-
break;
|
|
1814
1838
|
this.configCardIds.delete(oldest);
|
|
1815
1839
|
}
|
|
1816
1840
|
}
|
|
@@ -1857,7 +1881,7 @@ export class CommandRouter {
|
|
|
1857
1881
|
}
|
|
1858
1882
|
}
|
|
1859
1883
|
/**
|
|
1860
|
-
* 串行化 config.*
|
|
1884
|
+
* 串行化 config.* 卡片回调(修复连击卡死 bug)。
|
|
1861
1885
|
*
|
|
1862
1886
|
* CardKit 2.0 input/button 回调经 `enqueueImmediate` 分发,不进 bridge 的
|
|
1863
1887
|
* 串行队列;多次快速点击下拉/文本框会并发读写 `pendingConfig` 并同时调
|
|
@@ -1898,7 +1922,7 @@ export class CommandRouter {
|
|
|
1898
1922
|
}
|
|
1899
1923
|
case 'config.input': {
|
|
1900
1924
|
// Handle input field: value.key is the field key.
|
|
1901
|
-
//
|
|
1925
|
+
// CardKit 2.0 input 提交(回车/完成键)触发 callback,回传 input_value。
|
|
1902
1926
|
// Fall back to formValue[key]: the SDK currently reports input values
|
|
1903
1927
|
// via action.formValue for form-wrapped inputs.
|
|
1904
1928
|
const key = value.key;
|
|
@@ -1933,8 +1957,8 @@ export class CommandRouter {
|
|
|
1933
1957
|
// defaultAgent 切换的 session 处理)抽到共享方法,与文本直写路径共用。
|
|
1934
1958
|
const switchResult = this.propagateConfigSave(oldDefaultAgent, updates, ctx);
|
|
1935
1959
|
this.pendingConfig = null;
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1960
|
+
// 原地更新卡片:保存后卡片刷新为已保存状态。
|
|
1961
|
+
// 卡片刷新失败不吞切换通知——保存已成功,仅记录日志,
|
|
1938
1962
|
// 后续切换消息照常发送;真正写盘/传播阶段的失败才走外层 catch 报「保存失败」。
|
|
1939
1963
|
try {
|
|
1940
1964
|
// host 可能已变更 → 重新预取 DSH 目录(内部按 host 缓存,未变不重复拉)
|
|
@@ -1944,7 +1968,7 @@ export class CommandRouter {
|
|
|
1944
1968
|
catch (refreshErr) {
|
|
1945
1969
|
getLogger().warn(`[router] config.save 卡片刷新失败(保存已成功): ${refreshErr.message}`);
|
|
1946
1970
|
}
|
|
1947
|
-
//
|
|
1971
|
+
// agent 切换时发送 Resume 卡片(替代纯文本通知),
|
|
1948
1972
|
// 让用户直观看到新 agent 的会话状态和历史。
|
|
1949
1973
|
// 发送失败(resolve false)时兜底回退 toast 即时反馈。
|
|
1950
1974
|
if (switchResult.notice) {
|
|
@@ -1995,7 +2019,7 @@ export class CommandRouter {
|
|
|
1995
2019
|
// idle.watchdogMinutes 改动需同步到 bridge 的 idleTimeoutMs
|
|
1996
2020
|
this.idleTimeoutMs = this.config.idle.watchdogMinutes * 60_000;
|
|
1997
2021
|
this.bridge.setIdleTimeout(this.idleTimeoutMs);
|
|
1998
|
-
//
|
|
2022
|
+
// 检测是否有任何 agent 配置变更或 defaultAgent 切换
|
|
1999
2023
|
const hasDefaultAgentChange = oldDefaultAgent !== this.config.defaultAgent;
|
|
2000
2024
|
const changedKeys = Object.keys(updates);
|
|
2001
2025
|
const hasAgentConfigChange = changedKeys.includes('defaultAgent') ||
|
|
@@ -2069,7 +2093,7 @@ export class CommandRouter {
|
|
|
2069
2093
|
};
|
|
2070
2094
|
parkDshSessionBoundary(changedKeys.includes('agents.dsh.agentPreset'), 'preset');
|
|
2071
2095
|
parkDshSessionBoundary(changedKeys.includes('agents.dsh.host'), 'host');
|
|
2072
|
-
//
|
|
2096
|
+
// 同步 agent 配置到 agentChoices(用于切换 agent 时恢复配置)
|
|
2073
2097
|
const currentAgent = this.config.defaultAgent;
|
|
2074
2098
|
if (touchesAgentsSubtree(changedKeys, currentAgent)) {
|
|
2075
2099
|
this.config = syncAgentChoices(this.config, currentAgent);
|
|
@@ -2088,7 +2112,7 @@ export class CommandRouter {
|
|
|
2088
2112
|
if (hasApprovalConfigChange) {
|
|
2089
2113
|
this.bridge.syncActiveApprovalModes();
|
|
2090
2114
|
}
|
|
2091
|
-
//
|
|
2115
|
+
// 任何 agent 配置变更或 defaultAgent 切换,都清除 runner 缓存
|
|
2092
2116
|
if (hasDefaultAgentChange || hasAgentConfigChange) {
|
|
2093
2117
|
this.bridge.clearRunners();
|
|
2094
2118
|
}
|
|
@@ -2160,8 +2184,8 @@ export class CommandRouter {
|
|
|
2160
2184
|
// --- Command implementations ---
|
|
2161
2185
|
cmdHelp() {
|
|
2162
2186
|
// 按钮组(可直接点击触发;按钮 label 只保留子命令,不含参数)
|
|
2163
|
-
//
|
|
2164
|
-
//
|
|
2187
|
+
// /ws 子命令 (save|use|remove) 移到右侧文本说明,按钮只显示 /ws
|
|
2188
|
+
// 按钮 label 按长度升序排列(短在上,长在下),视觉更整齐
|
|
2165
2189
|
const buttonCommands = [
|
|
2166
2190
|
{ cmd: 'help', label: '/help /h', desc: '显示此帮助' },
|
|
2167
2191
|
{ cmd: 'ps', label: '/ps', desc: '查看是否有 agent 进程在跑' },
|
|
@@ -2202,11 +2226,11 @@ export class CommandRouter {
|
|
|
2202
2226
|
];
|
|
2203
2227
|
const bodyElements = [];
|
|
2204
2228
|
// 按钮组 — CardKit 2.0: column_set + column + button with behaviors
|
|
2205
|
-
//
|
|
2229
|
+
// 对齐修复(方案 A):两列都用 width:'weighted' + 固定 weight。
|
|
2206
2230
|
// 根因:每行独立 column_set + width:'auto' → 列宽随按钮/文本内容变化 →
|
|
2207
2231
|
// 跨行不对齐(按钮列右边界参差)。改用 weighted 后,每行 column_set
|
|
2208
2232
|
// 总宽相同(同级 body.elements),weight 比例一致,列宽跨行恒等。
|
|
2209
|
-
//
|
|
2233
|
+
// 手机排版修复:weight 1:3 → 2:3。1:3 时按钮列只占 25%,
|
|
2210
2234
|
// 手机窄屏(~330px 可用)下约 82px,容不下 `/config /c`(10 字符 ≈ 100px),
|
|
2211
2235
|
// 按钮文字被截断成 "..."。改 2:3 后按钮列占 40% ≈ 132px,足够显示
|
|
2212
2236
|
// 最长的 /config /c,文本列仍占 60% 足以放下描述。
|
|
@@ -2459,8 +2483,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2459
2483
|
}
|
|
2460
2484
|
cmdNew(ctx) {
|
|
2461
2485
|
// 只清 sessionId 保留 cwd — 否则 /new 之后 /resume 会提示"请先 /cd
|
|
2462
|
-
// 设置工作目录",但用户的 cwd
|
|
2463
|
-
//
|
|
2486
|
+
// 设置工作目录",但用户的 cwd 还在,用户 /new → /resume 后会被误导
|
|
2487
|
+
// 以为需要重新 /cd。
|
|
2464
2488
|
const agentName = agentDisplayName(this.config.defaultAgent);
|
|
2465
2489
|
this.sessionStore.clearSessionId(ctx.userId, this.config.defaultAgent, {
|
|
2466
2490
|
clearSessionCwd: true,
|
|
@@ -2545,38 +2569,66 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2545
2569
|
}
|
|
2546
2570
|
/** Build card for auto-resumed session with new session button */
|
|
2547
2571
|
buildAutoResumeCard(cwd, session, _ctx) {
|
|
2548
|
-
const
|
|
2549
|
-
|
|
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
|
+
});
|
|
2550
2603
|
const actions = this.buildResumeActionButtons({
|
|
2551
2604
|
isActive,
|
|
2552
2605
|
activeRunRunId,
|
|
2553
|
-
cwd,
|
|
2554
|
-
compact:
|
|
2555
|
-
? { sessionId: session.sessionId, agentKind: this.config.defaultAgent }
|
|
2556
|
-
: undefined,
|
|
2606
|
+
cwd: opts.cwd,
|
|
2607
|
+
compact: opts.compact,
|
|
2557
2608
|
newSession: true,
|
|
2558
2609
|
});
|
|
2559
2610
|
const card = buildSessionHistoryCard({
|
|
2560
|
-
sessionId:
|
|
2561
|
-
cwd,
|
|
2611
|
+
sessionId: opts.sessionId,
|
|
2612
|
+
cwd: opts.cwd,
|
|
2562
2613
|
displayTitle,
|
|
2563
2614
|
aiTitle,
|
|
2564
2615
|
recap,
|
|
2565
2616
|
events: content,
|
|
2566
2617
|
usage,
|
|
2567
2618
|
}, {
|
|
2568
|
-
agentKind:
|
|
2569
|
-
headerText: `📂 \`${cwd}\`\n
|
|
2570
|
-
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}`,
|
|
2571
2622
|
usageResult: 'success',
|
|
2623
|
+
...(opts.emptyPlaceholder !== undefined ? { emptyPlaceholder: opts.emptyPlaceholder } : {}),
|
|
2572
2624
|
actions,
|
|
2573
2625
|
});
|
|
2574
2626
|
return { card };
|
|
2575
2627
|
}
|
|
2576
2628
|
/**
|
|
2577
2629
|
* Build card for config.save agent switch.
|
|
2578
|
-
* When the new agent has a restored session, renders a Resume card (
|
|
2579
|
-
*
|
|
2630
|
+
* When the new agent has a restored session, renders a Resume card (shared
|
|
2631
|
+
* body via buildResumeLikeCard) with session history + stop/new buttons.
|
|
2580
2632
|
* When the session was cleared, renders a compact notice card instead.
|
|
2581
2633
|
*/
|
|
2582
2634
|
buildConfigSwitchCard(switchResult, ctx) {
|
|
@@ -2600,62 +2652,25 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2600
2652
|
},
|
|
2601
2653
|
};
|
|
2602
2654
|
}
|
|
2603
|
-
// Has session → Resume card (
|
|
2604
|
-
//
|
|
2605
|
-
//
|
|
2606
|
-
|
|
2607
|
-
? this.sessionReader
|
|
2608
|
-
: this.sessionReaderRegistry.get(newAgent);
|
|
2609
|
-
if (!readerForNewAgent) {
|
|
2610
|
-
// Fallback: compact notice card with the notice text
|
|
2611
|
-
const elements = [markdownDiv(switchResult.notice)];
|
|
2612
|
-
elements.push({
|
|
2613
|
-
tag: 'column_set',
|
|
2614
|
-
columns: [
|
|
2615
|
-
{
|
|
2616
|
-
tag: 'column',
|
|
2617
|
-
width: 'auto',
|
|
2618
|
-
elements: [newSessionButton()],
|
|
2619
|
-
},
|
|
2620
|
-
],
|
|
2621
|
-
});
|
|
2622
|
-
return {
|
|
2623
|
-
card: {
|
|
2624
|
-
schema: '2.0',
|
|
2625
|
-
config: { wide_screen_mode: true },
|
|
2626
|
-
header: { title: { tag: 'plain_text', content: `🔀 切换到 ${agentName}` } },
|
|
2627
|
-
body: { elements },
|
|
2628
|
-
},
|
|
2629
|
-
};
|
|
2630
|
-
}
|
|
2631
|
-
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(sessionId, cwd, {
|
|
2632
|
-
agentKind: newAgent,
|
|
2633
|
-
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
2634
|
-
});
|
|
2635
|
-
// Action buttons: stop (active) + new session
|
|
2636
|
-
const actions = this.buildResumeActionButtons({
|
|
2637
|
-
isActive,
|
|
2638
|
-
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({
|
|
2639
2659
|
cwd,
|
|
2640
|
-
newSession: true,
|
|
2641
|
-
});
|
|
2642
|
-
const card = buildSessionHistoryCard({
|
|
2643
2660
|
sessionId,
|
|
2644
|
-
cwd,
|
|
2645
|
-
displayTitle,
|
|
2646
|
-
aiTitle,
|
|
2647
|
-
recap,
|
|
2648
|
-
events: content,
|
|
2649
|
-
usage,
|
|
2650
|
-
}, {
|
|
2651
2661
|
agentKind: newAgent,
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
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,
|
|
2655
2672
|
emptyPlaceholder: '_该会话暂无新消息可显示(最后一条为用户输入)_',
|
|
2656
|
-
actions,
|
|
2657
2673
|
});
|
|
2658
|
-
return { card };
|
|
2659
2674
|
}
|
|
2660
2675
|
/**
|
|
2661
2676
|
* Whether a session of the given agent kind can be compacted from a resume
|
|
@@ -2700,7 +2715,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2700
2715
|
* @param offset 分页起点(条目数)
|
|
2701
2716
|
* @param rootDir 浏览起点:卡片「返回」按钮回到这里。省略时默认 = 本次列出的
|
|
2702
2717
|
* 目录(`/ls <dir>` 的起点即该目录),因此「返回」不会把用户
|
|
2703
|
-
* 丢回
|
|
2718
|
+
* 丢回 工作目录。
|
|
2704
2719
|
* @param q 关键词筛选:只匹配**当前层**条目名(不递归),大小写不敏感。
|
|
2705
2720
|
* 省略/空 = 无筛选。
|
|
2706
2721
|
*/
|
|
@@ -2790,7 +2805,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2790
2805
|
}
|
|
2791
2806
|
cmdActive(_args, _ctx, offset = 0) {
|
|
2792
2807
|
// List all active runs from lark-remote memory (not from file system scan)
|
|
2793
|
-
//
|
|
2808
|
+
// (only shows runs started by THIS lark-remote process)
|
|
2794
2809
|
const activeRuns = this.bridge.getActiveRuns();
|
|
2795
2810
|
const activeBashRuns = this.bridge.getActiveBashRuns();
|
|
2796
2811
|
if (activeRuns.length === 0 && activeBashRuns.length === 0) {
|
|
@@ -2814,54 +2829,31 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2814
2829
|
const elements = [];
|
|
2815
2830
|
let remaining = pageSize;
|
|
2816
2831
|
let skipped = safeOffset;
|
|
2817
|
-
// Agent runs section
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
tag: '
|
|
2827
|
-
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
else {
|
|
2843
|
-
skipped -= activeRuns.length;
|
|
2844
|
-
}
|
|
2845
|
-
// Bash runs section
|
|
2846
|
-
if (activeBashRuns.length > 0 && skipped < activeBashRuns.length && remaining > 0) {
|
|
2847
|
-
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '## 💻 Bash 命令' } });
|
|
2848
|
-
const start = skipped;
|
|
2849
|
-
const end = Math.min(start + remaining, activeBashRuns.length);
|
|
2850
|
-
for (let i = start; i < end; i++) {
|
|
2851
|
-
const run = activeBashRuns[i];
|
|
2852
|
-
const statusLabel = run.terminal === 'running' ? '运行中' : '处理中';
|
|
2853
|
-
elements.push({
|
|
2854
|
-
tag: 'div',
|
|
2855
|
-
text: { tag: 'lark_md', content: `**\`${run.command}\`**` },
|
|
2856
|
-
});
|
|
2857
|
-
elements.push({
|
|
2858
|
-
tag: 'div',
|
|
2859
|
-
text: { tag: 'lark_md', content: `> 目录: ${run.cwd} \n> 状态: ${statusLabel}` },
|
|
2860
|
-
});
|
|
2861
|
-
elements.push(cardButton('⏹ 停止', { cmd: 'stop', runId: run.runId, cwd: run.cwd }, { type: 'danger', size: 'small' }));
|
|
2862
|
-
elements.push({ tag: 'hr' });
|
|
2863
|
-
}
|
|
2864
|
-
}
|
|
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);
|
|
2865
2857
|
// Pagination bar(复用 paginationBar:safeOffset 已对齐页边界,
|
|
2866
2858
|
// offset±pageSize 与原先 currentPage±1 的页码换算等价)
|
|
2867
2859
|
if (totalPages > 1) {
|
|
@@ -2886,6 +2878,34 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2886
2878
|
};
|
|
2887
2879
|
return { card };
|
|
2888
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
|
+
}
|
|
2889
2909
|
async cmdConfig(args, ctx) {
|
|
2890
2910
|
// Ensure probe cache is populated before building the card.
|
|
2891
2911
|
await probeAllAgents();
|
|
@@ -2912,21 +2932,21 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2912
2932
|
// defaultAgent 不再漏判。
|
|
2913
2933
|
const switchResult = this.propagateConfigSave(oldDefaultAgent, { [mapAgentKey(key)]: value }, ctx);
|
|
2914
2934
|
this.pendingConfig = null; // 命令式写盘后清空暂存,避免不一致
|
|
2915
|
-
//
|
|
2935
|
+
// agent 切换时额外发送 Resume 卡片
|
|
2916
2936
|
if (switchResult.notice) {
|
|
2917
2937
|
const switchCard = this.buildConfigSwitchCard(switchResult, ctx);
|
|
2918
2938
|
// 文本路径返回 config card,Resume card 通过 bridge 异步发送
|
|
2919
2939
|
void this.bridge.sendResult(switchCard, ctx);
|
|
2920
2940
|
}
|
|
2921
2941
|
const card = this.buildConfigCard();
|
|
2922
|
-
if (discarded > 0
|
|
2942
|
+
if (discarded > 0) {
|
|
2943
|
+
// buildConfigCardFromTabs 恒返回 body.elements(至少含保存按钮),
|
|
2944
|
+
// 这里直接收窄插入提示条——同模块自己刚构建的值无需逐层探测。
|
|
2923
2945
|
const body = card.card.body;
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
});
|
|
2929
|
-
}
|
|
2946
|
+
body.elements.unshift({
|
|
2947
|
+
tag: 'div',
|
|
2948
|
+
text: { tag: 'lark_md', content: `⚠️ 已丢弃 ${discarded} 项未保存修改` },
|
|
2949
|
+
});
|
|
2930
2950
|
}
|
|
2931
2951
|
return card;
|
|
2932
2952
|
}
|