lark-remote 0.4.4 → 0.5.0
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 +28 -0
- package/dist/binder.d.ts +11 -10
- package/dist/binder.js +17 -16
- package/dist/bridge/approval-coordinator.d.ts +1 -1
- package/dist/bridge/approval-coordinator.js +3 -3
- package/dist/bridge/inbound-media.d.ts +1 -27
- package/dist/bridge/inbound-media.js +23 -21
- package/dist/bridge/index.d.ts +57 -57
- package/dist/bridge/index.js +136 -129
- package/dist/bridge/queue-manager.d.ts +45 -38
- package/dist/bridge/queue-manager.js +136 -136
- package/dist/card/approval-render.d.ts +1 -1
- package/dist/card/approval-render.js +6 -6
- package/dist/card/bash-card-session.d.ts +3 -3
- package/dist/card/bash-card-session.js +3 -3
- package/dist/card/bash-renderer.js +5 -5
- package/dist/card/card-session.d.ts +4 -4
- package/dist/card/card-session.js +6 -6
- package/dist/card/card-shared.d.ts +1 -1
- package/dist/card/card-shared.js +2 -2
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.d.ts +1 -1
- package/dist/card/run-renderer.js +22 -22
- package/dist/card/run-state.d.ts +2 -2
- package/dist/card/run-state.js +3 -3
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +6 -6
- package/dist/cli.js +5 -6
- package/dist/clone.d.ts +6 -9
- package/dist/clone.js +12 -8
- package/dist/common/clamp.d.ts +2 -0
- package/dist/common/clamp.js +5 -0
- package/dist/common/fs.d.ts +4 -3
- package/dist/common/fs.js +5 -4
- package/dist/common/truncate.d.ts +1 -1
- package/dist/common/truncate.js +2 -2
- package/dist/common/ttl-cache.d.ts +2 -2
- package/dist/common/ttl-cache.js +2 -2
- package/dist/config/codex-config.d.ts +9 -9
- package/dist/config/codex-config.js +18 -18
- package/dist/config/dir.d.ts +4 -5
- package/dist/config/dir.js +12 -10
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +6 -6
- package/dist/config/opencode-config.js +3 -3
- package/dist/config/wizard.js +2 -2
- package/dist/connector/index.d.ts +5 -7
- package/dist/connector/index.js +29 -16
- package/dist/inbound/event.d.ts +1 -1
- package/dist/inbound/event.js +1 -1
- package/dist/inbound/placeholder.d.ts +2 -2
- package/dist/inbound/placeholder.js +2 -2
- package/dist/inbound/receipt.d.ts +1 -3
- package/dist/inbound/receipt.js +2 -2
- package/dist/inbound/turn-assembler.d.ts +1 -3
- package/dist/inbound/turn-assembler.js +2 -2
- package/dist/index.js +38 -30
- package/dist/instance-lock.js +8 -7
- package/dist/logger/index.d.ts +1 -1
- package/dist/logger/index.js +4 -4
- package/dist/persistence/atomic-write.d.ts +1 -1
- package/dist/persistence/atomic-write.js +9 -5
- package/dist/platform/agent-stopper.d.ts +2 -2
- package/dist/platform/identity.d.ts +7 -1
- package/dist/platform/identity.js +2 -2
- package/dist/platform/path.d.ts +6 -0
- package/dist/platform/path.js +14 -8
- package/dist/platform/select.d.ts +1 -1
- package/dist/platform/select.js +1 -1
- package/dist/platform/shell.d.ts +2 -3
- package/dist/platform/spawn.d.ts +2 -2
- package/dist/platform/spawn.js +2 -2
- package/dist/platform/terminator-posix.js +6 -6
- package/dist/platform/terminator-win32.js +3 -3
- package/dist/platform/terminator.d.ts +3 -3
- package/dist/platform/terminator.js +2 -2
- package/dist/platform/types.d.ts +1 -1
- package/dist/restart.d.ts +1 -1
- package/dist/restart.js +5 -3
- package/dist/router/card-helpers.d.ts +1 -3
- package/dist/router/card-helpers.js +2 -2
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +3 -3
- package/dist/router/config/common/model-patch.d.ts +1 -2
- package/dist/router/config/common/model-patch.js +1 -2
- package/dist/router/config/opencode.js +2 -2
- package/dist/router/config/pi.js +1 -1
- package/dist/router/index.d.ts +30 -30
- package/dist/router/index.js +103 -84
- package/dist/router/order-exec-dispatch.d.ts +3 -10
- package/dist/router/order-exec-dispatch.js +5 -5
- package/dist/router/{utils.d.ts → usage-format.d.ts} +2 -4
- package/dist/router/{utils.js → usage-format.js} +3 -8
- package/dist/runner/bash/runner.d.ts +2 -2
- package/dist/runner/bash/runner.js +6 -6
- package/dist/runner/claude/runner.d.ts +1 -1
- package/dist/runner/claude/runner.js +2 -3
- package/dist/runner/claude/session.d.ts +4 -6
- package/dist/runner/claude/session.js +21 -23
- package/dist/runner/codex/app-server/runner.js +3 -3
- package/dist/runner/codex/app-server/translator.d.ts +2 -2
- package/dist/runner/codex/app-server/translator.js +5 -5
- package/dist/runner/common/acp/{protocol-helpers.d.ts → acp-approval.d.ts} +4 -10
- package/dist/runner/common/acp/{protocol-helpers.js → acp-approval.js} +7 -6
- package/dist/runner/common/acp/base-acp-runner.d.ts +1 -1
- package/dist/runner/common/acp/base-acp-runner.js +2 -2
- package/dist/runner/common/acp/base-translator.d.ts +3 -3
- package/dist/runner/common/acp/base-translator.js +4 -4
- package/dist/runner/common/acp/protocol-types.js +1 -1
- package/dist/runner/common/connection-based-runner.d.ts +5 -5
- package/dist/runner/common/connection-based-runner.js +5 -5
- package/dist/runner/common/jsonl-stream.d.ts +2 -2
- package/dist/runner/common/jsonl-stream.js +12 -12
- package/dist/runner/common/jsonrpc/client.d.ts +1 -1
- package/dist/runner/common/jsonrpc/client.js +1 -1
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +13 -13
- package/dist/runner/common/jsonrpc/connection-manager.js +37 -37
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +2 -2
- package/dist/runner/common/{usage.d.ts → result-usage.d.ts} +1 -1
- package/dist/runner/common/{usage.js → result-usage.js} +2 -2
- package/dist/runner/common/spawning-runner.d.ts +23 -10
- package/dist/runner/common/spawning-runner.js +48 -22
- package/dist/runner/common/{runner-utils.d.ts → synthetic-events.d.ts} +4 -4
- package/dist/runner/common/{runner-utils.js → synthetic-events.js} +5 -5
- package/dist/runner/dsh/client.d.ts +1 -1
- package/dist/runner/dsh/client.js +1 -1
- package/dist/runner/dsh/runner.js +3 -3
- package/dist/runner/kimi/acp/runner.d.ts +4 -4
- package/dist/runner/kimi/acp/runner.js +10 -10
- package/dist/runner/kimi/acp/translator.d.ts +2 -2
- package/dist/runner/kimi/acp/translator.js +5 -5
- package/dist/runner/opencode/acp/runner.d.ts +4 -4
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/opencode/acp/translator.js +2 -2
- package/dist/runner/pi/rpc/runner.d.ts +1 -1
- package/dist/runner/pi/rpc/runner.js +2 -2
- package/dist/runner/pi/rpc/translator.d.ts +1 -2
- package/dist/runner/pi/rpc/translator.js +4 -6
- package/dist/runner/registry.d.ts +3 -3
- package/dist/runner/registry.js +3 -3
- package/dist/runner/types.d.ts +1 -1
- package/dist/session/claude/session-index.d.ts +13 -1
- package/dist/session/claude/session-index.js +21 -6
- package/dist/session/claude/session-reader.d.ts +1 -1
- package/dist/session/claude/session-reader.js +1 -1
- package/dist/session/claude/sessions.js +10 -10
- package/dist/session/codex/rollout-reader.d.ts +4 -4
- package/dist/session/codex/rollout-reader.js +14 -14
- package/dist/session/common/two-pass.js +2 -2
- package/dist/session/common/usage-accumulator.d.ts +2 -2
- package/dist/session/common/usage-accumulator.js +2 -2
- package/dist/session/dsh/sessions.d.ts +1 -1
- package/dist/session/dsh/sessions.js +3 -3
- package/dist/session/kimi/sessions.d.ts +2 -6
- package/dist/session/kimi/sessions.js +13 -19
- package/dist/session/opencode/sessions.d.ts +3 -3
- package/dist/session/opencode/sessions.js +15 -13
- package/dist/session/pi/sessions.js +13 -12
- package/dist/session/registry.d.ts +1 -1
- package/dist/session/registry.js +1 -1
- package/dist/session/session-store.d.ts +3 -4
- package/dist/session/session-store.js +3 -5
- package/dist/update/index.d.ts +1 -1
- package/dist/update/index.js +1 -1
- package/dist/update/version-check.d.ts +5 -3
- package/dist/update/version-check.js +10 -8
- package/dist/workspace/index.d.ts +4 -4
- package/dist/workspace/index.js +5 -5
- package/package.json +1 -1
package/dist/router/index.js
CHANGED
|
@@ -10,11 +10,14 @@ import { resolveAlias } from '../order/alias-resolve.js';
|
|
|
10
10
|
import { diffConfig, setNestedValue } from './config-diff.js';
|
|
11
11
|
import { getLogger } from '../logger/index.js';
|
|
12
12
|
import { stripPlaceholders } from '../inbound/placeholder.js';
|
|
13
|
-
import {
|
|
13
|
+
import { clampInt } from '../common/clamp.js';
|
|
14
|
+
import { activeRunUsage } from './usage-format.js';
|
|
14
15
|
import { markdownDiv, buildSessionHistoryCard, paginationBar, searchBar, parseJumpOffset, SEARCH_INPUT_NAME, PAGE_JUMP_INVALID_HINT, } from './card-helpers.js';
|
|
15
16
|
import { MAX_FILE_UPLOAD_SIZE } from '../connector/file-limits.js';
|
|
17
|
+
import { newSessionButton, resumeCompactButton, agentDisplayName } from '../card/card-shared.js';
|
|
18
|
+
import { displayName } from '../platform/path.js';
|
|
16
19
|
import { atomicWrite } from '../persistence/atomic-write.js';
|
|
17
|
-
import { checkLatestVersion as defaultCheckLatestVersion,
|
|
20
|
+
import { checkLatestVersion as defaultCheckLatestVersion, compareVersions as defaultCompareVersions, runInstallLatest as defaultRunInstallLatest, } from '../update/index.js';
|
|
18
21
|
/** Config card builder - delegates to per-agent builders */
|
|
19
22
|
import { getConfigBuilder, listRegisteredAgents, sortAgentsForDisplay } from './config/index.js';
|
|
20
23
|
import { probeAllAgents, getCachedAvailability } from '../runner/probe.js';
|
|
@@ -128,8 +131,6 @@ const RESUME_CONTENT_PREFETCH = 5;
|
|
|
128
131
|
* `New Session`、空串),都不是真实输入,不得渲染成行标题。
|
|
129
132
|
*/
|
|
130
133
|
const RESUME_SUMMARY_PLACEHOLDERS = new Set(['', '(no user message)', '(无摘要)', 'New Session']);
|
|
131
|
-
import { newSessionButton, resumeCompactButton, agentDisplayName } from '../card/card-shared.js';
|
|
132
|
-
import { displayName } from '../platform/path.js';
|
|
133
134
|
/**
|
|
134
135
|
* Card-action commands that must run with enqueue({ immediate: true }).
|
|
135
136
|
* These bypass the serial queue and execute immediately.
|
|
@@ -145,7 +146,7 @@ import { displayName } from '../platform/path.js';
|
|
|
145
146
|
* stop — interrupt current run
|
|
146
147
|
* ls.file — send file only
|
|
147
148
|
* ws.page — paginate /ws list only
|
|
148
|
-
* ws.remove — delete
|
|
149
|
+
* ws.remove — delete cwd alias only
|
|
149
150
|
* resume.use — set sessionId + agent for correct reader routing
|
|
150
151
|
* help.* — read-only help commands
|
|
151
152
|
* order.delete — delete order only
|
|
@@ -261,7 +262,7 @@ const APPROVAL_ACTION_SPECS = {
|
|
|
261
262
|
},
|
|
262
263
|
};
|
|
263
264
|
// ===========================================================================
|
|
264
|
-
//
|
|
265
|
+
// 命令名单单一事实源
|
|
265
266
|
// 直返语义(cardAction 同步返回 toast 给飞书回调,index.ts 控制层据此不经
|
|
266
267
|
// 队列直接处理)与即时语义(绕串行队列 enqueueImmediate,§9.6)正交,两个
|
|
267
268
|
// 独立清单只共享审批命令名来源(APPROVAL_ACTION_SPECS keys)——历史上同一
|
|
@@ -320,7 +321,25 @@ const IMMEDIATE_ACTION_CMDS = new Set([
|
|
|
320
321
|
// coordinator 已删响应空转(线上复现:approval.respond 排队卡、审批永不生效)。
|
|
321
322
|
...APPROVAL_ACTION_CMDS,
|
|
322
323
|
]);
|
|
323
|
-
/**
|
|
324
|
+
/**
|
|
325
|
+
* 会话 / cwd / agent 变更类卡片动作(单源)。这些动作会改写 sessionStore 的
|
|
326
|
+
* sessionId / cwd / defaultAgent,必须与入站装配器的 700ms 静默窗口定序:
|
|
327
|
+
* 控制层(index.ts)在执行它们之前先 flush 该 user/chat 的装配窗口,保证窗口内
|
|
328
|
+
* 先到的文本消息在本动作之前 commit(绑定动作前的旧状态)。否则窗口内消息会
|
|
329
|
+
* 漂移到动作之后的新状态(2026-10-07 事故:输入「重新构建」后立即点「新会话」,
|
|
330
|
+
* 消息落到新会话)。
|
|
331
|
+
*
|
|
332
|
+
* 文本命令(/new、/cd、/resume、/config)已由 index.ts 命令分支先 flush,
|
|
333
|
+
* 不在此列——只有卡片动作绕过了该分支。
|
|
334
|
+
*/
|
|
335
|
+
export const SESSION_MUTATING_ACTION_CMDS = new Set([
|
|
336
|
+
'new-session',
|
|
337
|
+
'resume.use',
|
|
338
|
+
'ls.switch',
|
|
339
|
+
'ws.use',
|
|
340
|
+
'config.save',
|
|
341
|
+
]);
|
|
342
|
+
/** 单源:payload.offset → 钳位 offset(原先 9 处逐字副本)。 */
|
|
324
343
|
function payloadOffset(value) {
|
|
325
344
|
return Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
326
345
|
}
|
|
@@ -426,7 +445,7 @@ export class CommandRouter {
|
|
|
426
445
|
this.cloneSession = opts.cloneSession;
|
|
427
446
|
this.updateFns = {
|
|
428
447
|
checkLatestVersion: opts.updateFns?.checkLatestVersion ?? defaultCheckLatestVersion,
|
|
429
|
-
|
|
448
|
+
compareVersions: opts.updateFns?.compareVersions ?? defaultCompareVersions,
|
|
430
449
|
runInstallLatest: opts.updateFns?.runInstallLatest ?? defaultRunInstallLatest,
|
|
431
450
|
};
|
|
432
451
|
}
|
|
@@ -447,7 +466,7 @@ export class CommandRouter {
|
|
|
447
466
|
/**
|
|
448
467
|
* Route a message: if it starts with /, handle as command; if it starts with !, execute as bash; otherwise forward to Claude.
|
|
449
468
|
*
|
|
450
|
-
* 命令识别前置条件(2026-09-15
|
|
469
|
+
* 命令识别前置条件(2026-09-15修复,`inbound-unified-input-design.md` §5.6):
|
|
451
470
|
* 裸前缀判定会被结构占位符命中 —— SDK 把富文本里的图片渲染成 ``,
|
|
452
471
|
* 首字符恰好是 `!`,整段用户消息被当 shell 命令执行(`executeBash "[image](img_v3_…"`)。
|
|
453
472
|
* 因此命令前缀只在「调用方声明纯文本 + 剥离占位符后非空且无占位符 + 首字符是 / 或 !」
|
|
@@ -494,7 +513,7 @@ export class CommandRouter {
|
|
|
494
513
|
await this.bridge.executeBash(cmd, ctx);
|
|
495
514
|
return null;
|
|
496
515
|
}
|
|
497
|
-
// Forward to Claude (
|
|
516
|
+
// Forward to Claude (pass the enqueue-time cwd through so the
|
|
498
517
|
// run uses the same cwd as the serial queue lane, even if /cd ran while
|
|
499
518
|
// the message was queued)
|
|
500
519
|
// 剥离占位符后再转发:agent 只该看到用户文本(附图已转成 attachments 块,
|
|
@@ -519,7 +538,7 @@ export class CommandRouter {
|
|
|
519
538
|
* Validates payload and dispatches to the corresponding command.
|
|
520
539
|
*/
|
|
521
540
|
async handleCardAction(value, ctx) {
|
|
522
|
-
//
|
|
541
|
+
// 审批家族以 APPROVAL_ACTION_SPECS keys 为单一来源分发
|
|
523
542
|
// (原 7 个 case 与 spec keys 是同一份知识的两份拷贝,已两次漂移)。
|
|
524
543
|
if (APPROVAL_ACTION_CMDS.includes(value.cmd)) {
|
|
525
544
|
return this.handleApprovalAction(value, ctx);
|
|
@@ -565,7 +584,7 @@ export class CommandRouter {
|
|
|
565
584
|
await this.bridge.sendResult({ text: '请先 /cd 设置工作目录' }, ctx);
|
|
566
585
|
return;
|
|
567
586
|
}
|
|
568
|
-
//
|
|
587
|
+
// 校验前不得写入 sessionId。旧实现先无条件 setSessionId 再调
|
|
569
588
|
// cmdResume 校验,过期卡片(session 不在当前 cwd)校验失败时 store 已被
|
|
570
589
|
// 污染;value.sessionId 缺失时还会静默清空已有绑定。现在统一走 cmdResume
|
|
571
590
|
// 的已验证路径(:2352 校验通过才写入),sessionId 缺失直接报错。
|
|
@@ -635,8 +654,8 @@ export class CommandRouter {
|
|
|
635
654
|
// 调 updateCardInPlace → Feishu API 乱序到达导致 toggle 卡死。
|
|
636
655
|
// 2026-07-18: 返回 enqueueConfigAction 的结果以支持 toast 响应
|
|
637
656
|
return this.enqueueConfigAction(value, ctx);
|
|
638
|
-
case '
|
|
639
|
-
await this.bridge.
|
|
657
|
+
case 'compact':
|
|
658
|
+
await this.bridge.handleCompactAction(value, ctx);
|
|
640
659
|
return;
|
|
641
660
|
case 'resume.compact':
|
|
642
661
|
await this.bridge.handleResumeCompact(value, ctx);
|
|
@@ -672,13 +691,13 @@ export class CommandRouter {
|
|
|
672
691
|
* Routes to the appropriate bridge method based on value.cmd.
|
|
673
692
|
* Returns a toast response for immediate user feedback.
|
|
674
693
|
*/
|
|
675
|
-
/** queue.* 卡片动作通用守卫:
|
|
694
|
+
/** queue.* 卡片动作通用守卫:cwd/messageId 缺失时回复错误并返回 null。 */
|
|
676
695
|
async queuePayloadOrReply(value, ctx) {
|
|
677
|
-
if (!value.
|
|
696
|
+
if (!value.cwd || !value.messageId) {
|
|
678
697
|
await this.bridge.sendResult({ text: CARD_PAYLOAD_MISSING }, ctx);
|
|
679
698
|
return null;
|
|
680
699
|
}
|
|
681
|
-
return {
|
|
700
|
+
return { cwd: value.cwd, messageId: value.messageId };
|
|
682
701
|
}
|
|
683
702
|
/** 答案类操作的统一失败反馈:重复投递(同一 nonce 第二次点击)是已生效的
|
|
684
703
|
* 中性事件,报 error 会误导(首次点击实际已成功)。 */
|
|
@@ -712,11 +731,11 @@ export class CommandRouter {
|
|
|
712
731
|
const payload = await this.queuePayloadOrReply(value, ctx);
|
|
713
732
|
if (!payload)
|
|
714
733
|
return;
|
|
715
|
-
const {
|
|
716
|
-
const removed = this.bridge.removeFromQueue(
|
|
734
|
+
const { cwd, messageId } = payload;
|
|
735
|
+
const removed = this.bridge.removeFromQueue(cwd, messageId);
|
|
717
736
|
if (removed) {
|
|
718
737
|
// Update the queue card in-place to "cancelled" state
|
|
719
|
-
await this.bridge.updateQueueCardToCancelled(
|
|
738
|
+
await this.bridge.updateQueueCardToCancelled(cwd, messageId);
|
|
720
739
|
// Send a brief confirmation (user can see the card has been updated)
|
|
721
740
|
await this.bridge.sendResult({ text: '✅ 已从队列中撤销' }, ctx);
|
|
722
741
|
}
|
|
@@ -732,15 +751,15 @@ export class CommandRouter {
|
|
|
732
751
|
const payload = await this.queuePayloadOrReply(value, ctx);
|
|
733
752
|
if (!payload)
|
|
734
753
|
return;
|
|
735
|
-
const {
|
|
754
|
+
const { cwd, messageId } = payload;
|
|
736
755
|
// 1. Check target exists BEFORE any await. The queue chain can advance
|
|
737
756
|
// while later awaits (interruptCurrentRun / markQueueCardExecuting's card
|
|
738
757
|
// send) are in flight, so the target must be read synchronously at entry.
|
|
739
|
-
const targetTask = this.bridge.getQueuedTask(
|
|
758
|
+
const targetTask = this.bridge.getQueuedTask(cwd, messageId);
|
|
740
759
|
if (!targetTask) {
|
|
741
760
|
// The target already began (or was cancelled) before this handler ran. We
|
|
742
761
|
// must NOT claim the queue was cleared — nothing is removed in this branch
|
|
743
|
-
// and tasks queued behind the target are still waiting
|
|
762
|
+
// and tasks queued behind the target are still waiting.
|
|
744
763
|
await this.bridge.sendResult({
|
|
745
764
|
text: '该消息已不在队列中(可能已开始执行或被撤销),无法立即执行。其余排队消息保持原状。',
|
|
746
765
|
}, ctx);
|
|
@@ -758,11 +777,11 @@ export class CommandRouter {
|
|
|
758
777
|
// which would put the edited task behind tasks queued after it.
|
|
759
778
|
if (targetTask.editedMessage) {
|
|
760
779
|
const editedContent = targetTask.editedMessage;
|
|
761
|
-
|
|
780
|
+
///Step3: 替换闭包复用原任务的 binding(不重新快照)+ 恢复丢失的
|
|
762
781
|
// cwdOverride(lane cwd 同源)。否则编辑后重新执行的 run 会丢 cwd 与绑定。
|
|
763
|
-
this.bridge.setTaskReplacement(
|
|
782
|
+
this.bridge.setTaskReplacement(cwd, messageId, async () => {
|
|
764
783
|
await this.handle(editedContent, ctx, {
|
|
765
|
-
cwdOverride:
|
|
784
|
+
cwdOverride: cwd,
|
|
766
785
|
binding: targetTask.binding,
|
|
767
786
|
});
|
|
768
787
|
});
|
|
@@ -773,7 +792,7 @@ export class CommandRouter {
|
|
|
773
792
|
const stopped = await this.bridge.interruptCurrentRun({
|
|
774
793
|
userId: ctx.userId,
|
|
775
794
|
chatId: ctx.chatId,
|
|
776
|
-
|
|
795
|
+
cwd,
|
|
777
796
|
});
|
|
778
797
|
getLogger().info(`[router] queue.immediate: stopped=${stopped}`);
|
|
779
798
|
// 3. Remove all tasks BEFORE this one (by position in the current queue).
|
|
@@ -781,7 +800,7 @@ export class CommandRouter {
|
|
|
781
800
|
// advances concurrently): then it is already out of the snapshot and every
|
|
782
801
|
// remaining queued task is BEHIND it — clearing must stop, not run off the
|
|
783
802
|
// end and delete tasks queued after the target.
|
|
784
|
-
const tasks = this.bridge.getQueuedTasks(
|
|
803
|
+
const tasks = this.bridge.getQueuedTasks(cwd);
|
|
785
804
|
const targetIdx = tasks.findIndex((t) => t.messageId === messageId);
|
|
786
805
|
let removedCount = 0;
|
|
787
806
|
const removedIds = [];
|
|
@@ -792,7 +811,7 @@ export class CommandRouter {
|
|
|
792
811
|
// in flight.
|
|
793
812
|
for (let i = 0; i < targetIdx; i++) {
|
|
794
813
|
const task = tasks[i];
|
|
795
|
-
if (this.bridge.removeFromQueue(
|
|
814
|
+
if (this.bridge.removeFromQueue(cwd, task.messageId)) {
|
|
796
815
|
removedCount++;
|
|
797
816
|
removedIds.push(task.messageId);
|
|
798
817
|
}
|
|
@@ -804,7 +823,7 @@ export class CommandRouter {
|
|
|
804
823
|
// target are already gone, so any chain advance can only reach the target
|
|
805
824
|
// itself — the card updates may safely run in the background.
|
|
806
825
|
for (const removedId of removedIds) {
|
|
807
|
-
void this.bridge.updateQueueCardToCancelled(
|
|
826
|
+
void this.bridge.updateQueueCardToCancelled(cwd, removedId);
|
|
808
827
|
}
|
|
809
828
|
// 4. DO NOT remove the target task - keep it in queue to execute immediately
|
|
810
829
|
// The queue will naturally execute it after the current task (which we just stopped)
|
|
@@ -812,13 +831,13 @@ export class CommandRouter {
|
|
|
812
831
|
// re-registering over the same slot). Covers the user editing the message
|
|
813
832
|
// again while interruptCurrentRun was awaiting; skipped automatically once
|
|
814
833
|
// the task has begun, because getQueuedTask then returns undefined.
|
|
815
|
-
const latestTask = this.bridge.getQueuedTask(
|
|
834
|
+
const latestTask = this.bridge.getQueuedTask(cwd, messageId);
|
|
816
835
|
if (latestTask?.editedMessage) {
|
|
817
836
|
const editedContent = latestTask.editedMessage;
|
|
818
|
-
|
|
819
|
-
this.bridge.setTaskReplacement(
|
|
837
|
+
///Step3: 复用原任务 binding + 恢复 cwdOverride(同上)。
|
|
838
|
+
this.bridge.setTaskReplacement(cwd, messageId, async () => {
|
|
820
839
|
await this.handle(editedContent, ctx, {
|
|
821
|
-
cwdOverride:
|
|
840
|
+
cwdOverride: cwd,
|
|
822
841
|
binding: latestTask.binding,
|
|
823
842
|
});
|
|
824
843
|
});
|
|
@@ -826,9 +845,9 @@ export class CommandRouter {
|
|
|
826
845
|
// 4.5/6. The target must only be marked executing (and the toast may only
|
|
827
846
|
// promise immediate execution) when it is really the next task after the
|
|
828
847
|
// stop. While interruptCurrentRun was awaiting, a task ahead of the target
|
|
829
|
-
// may have begun and now occupy the
|
|
848
|
+
// may have begun and now occupy the cwd — the target is still queued
|
|
830
849
|
// but is NOT next, so its card must stay queued and the toast must say so.
|
|
831
|
-
const targetStillQueued = this.bridge.getQueuedTask(
|
|
850
|
+
const targetStillQueued = this.bridge.getQueuedTask(cwd, messageId) !== undefined;
|
|
832
851
|
if (!targetStillQueued) {
|
|
833
852
|
// The target began or was cancelled while earlier awaits were in flight —
|
|
834
853
|
// a success toast must never promise execution of a task that will not
|
|
@@ -847,14 +866,14 @@ export class CommandRouter {
|
|
|
847
866
|
}
|
|
848
867
|
return;
|
|
849
868
|
}
|
|
850
|
-
const workspaceBusy = this.bridge.isBusyFor(
|
|
869
|
+
const workspaceBusy = this.bridge.isBusyFor(cwd);
|
|
851
870
|
if (!workspaceBusy) {
|
|
852
871
|
// Normal path: nothing new began during the stop, the target is next.
|
|
853
872
|
// Mark its own card as executing now so buttons grey out immediately.
|
|
854
873
|
// Done BEFORE any removal so getQueuedTask still returns the task with
|
|
855
874
|
// its (possibly edited) messagePreview for the card. The queue callback's
|
|
856
875
|
// later updateQueueCardToExecuting call becomes a no-op (idempotent).
|
|
857
|
-
await this.bridge.markQueueCardExecuting(
|
|
876
|
+
await this.bridge.markQueueCardExecuting(cwd, messageId);
|
|
858
877
|
const stopPrefix = stopped ? '⚡ 已停止当前任务,' : '';
|
|
859
878
|
await this.bridge.sendResult({ text: `${stopPrefix}清除了 ${removedCount} 条排队消息。您的消息将立即执行。` }, ctx);
|
|
860
879
|
}
|
|
@@ -871,10 +890,10 @@ export class CommandRouter {
|
|
|
871
890
|
const payload = await this.queuePayloadOrReply(value, ctx);
|
|
872
891
|
if (!payload)
|
|
873
892
|
return;
|
|
874
|
-
const {
|
|
893
|
+
const { cwd, messageId } = payload;
|
|
875
894
|
const targetUserId = value.userId ?? ctx.userId;
|
|
876
|
-
const task = this.bridge.getQueuedTask(
|
|
877
|
-
const queueInfo = this.bridge.getQueueInfo(
|
|
895
|
+
const task = this.bridge.getQueuedTask(cwd, messageId);
|
|
896
|
+
const queueInfo = this.bridge.getQueueInfo(cwd);
|
|
878
897
|
const activeRuns = this.bridge.getAllActiveRuns();
|
|
879
898
|
// Build diagnostic info
|
|
880
899
|
const now = new Date().toLocaleString('zh-CN');
|
|
@@ -907,7 +926,7 @@ export class CommandRouter {
|
|
|
907
926
|
tag: 'div',
|
|
908
927
|
text: {
|
|
909
928
|
tag: 'lark_md',
|
|
910
|
-
content: `**消息信息**\n- 工作目录: \`${
|
|
929
|
+
content: `**消息信息**\n- 工作目录: \`${cwd}\`\n- 会话 ID: \`${sessionId ?? '(none)'}\`\n- 消息预览: ${task?.messagePreview ?? '(unknown)'}`,
|
|
911
930
|
},
|
|
912
931
|
},
|
|
913
932
|
{ tag: 'hr' },
|
|
@@ -943,9 +962,9 @@ export class CommandRouter {
|
|
|
943
962
|
const payload = await this.queuePayloadOrReply(value, ctx);
|
|
944
963
|
if (!payload)
|
|
945
964
|
return;
|
|
946
|
-
const {
|
|
965
|
+
const { cwd, messageId } = payload;
|
|
947
966
|
// Get current message preview
|
|
948
|
-
const task = this.bridge.getQueuedTask(
|
|
967
|
+
const task = this.bridge.getQueuedTask(cwd, messageId);
|
|
949
968
|
if (!task) {
|
|
950
969
|
await this.bridge.sendResult({ text: '⚠️ 该消息不在队列中(可能已开始执行)' }, ctx);
|
|
951
970
|
return;
|
|
@@ -987,7 +1006,7 @@ export class CommandRouter {
|
|
|
987
1006
|
placeholder: { tag: 'plain_text', content: '输入新的消息内容...' },
|
|
988
1007
|
default_value: task.messagePreview,
|
|
989
1008
|
behaviors: [
|
|
990
|
-
{ type: 'callback', value: { cmd: 'queue.input',
|
|
1009
|
+
{ type: 'callback', value: { cmd: 'queue.input', cwd, messageId } },
|
|
991
1010
|
],
|
|
992
1011
|
},
|
|
993
1012
|
],
|
|
@@ -1013,7 +1032,7 @@ export class CommandRouter {
|
|
|
1013
1032
|
const payload = await this.queuePayloadOrReply(value, ctx);
|
|
1014
1033
|
if (!payload)
|
|
1015
1034
|
return;
|
|
1016
|
-
const {
|
|
1035
|
+
const { cwd, messageId } = payload;
|
|
1017
1036
|
if (!newMessage) {
|
|
1018
1037
|
await this.bridge.sendResult({ text: '⚠️ 缺少新消息内容' }, ctx);
|
|
1019
1038
|
return;
|
|
@@ -1030,8 +1049,8 @@ export class CommandRouter {
|
|
|
1030
1049
|
// setTaskReplacement and the begin-path consumption are synchronous, so
|
|
1031
1050
|
// there is no interleaving window.
|
|
1032
1051
|
//
|
|
1033
|
-
|
|
1034
|
-
const inputTask = this.bridge.getQueuedTask(
|
|
1052
|
+
///Step3:复用原任务 binding(不重新快照)+ 恢复 cwdOverride。
|
|
1053
|
+
const inputTask = this.bridge.getQueuedTask(cwd, messageId);
|
|
1035
1054
|
if (!inputTask) {
|
|
1036
1055
|
// Task already left the queue (began or was cancelled) before we could
|
|
1037
1056
|
// register the replacement. Return a toast so the edit-form card gets
|
|
@@ -1040,8 +1059,8 @@ export class CommandRouter {
|
|
|
1040
1059
|
// toast/card field).
|
|
1041
1060
|
return { toast: { type: 'info', content: '任务已不在队列中(可能已开始执行或被撤销)' } };
|
|
1042
1061
|
}
|
|
1043
|
-
this.bridge.setTaskReplacement(
|
|
1044
|
-
await this.handle(newMessage, ctx, { cwdOverride:
|
|
1062
|
+
this.bridge.setTaskReplacement(cwd, messageId, async () => {
|
|
1063
|
+
await this.handle(newMessage, ctx, { cwdOverride: cwd, binding: inputTask.binding });
|
|
1045
1064
|
});
|
|
1046
1065
|
// Update the message preview (and editedMessage so handleQueueImmediate
|
|
1047
1066
|
// re-enqueues the edited content instead of the stale original closure),
|
|
@@ -1054,7 +1073,7 @@ export class CommandRouter {
|
|
|
1054
1073
|
// await here: even if the queue chain advances and the replacement is
|
|
1055
1074
|
// consumed, the user-facing card update is best-effort (the executing card
|
|
1056
1075
|
// is updated by updateQueueCardToExecuting instead).
|
|
1057
|
-
const card = await this.bridge.updateMessagePreview(
|
|
1076
|
+
const card = await this.bridge.updateMessagePreview(cwd, messageId, newMessage);
|
|
1058
1077
|
if (!card) {
|
|
1059
1078
|
// Task left the queue during the await (began or cancelled). The
|
|
1060
1079
|
// replacement was already consumed (begin path) or cleaned up
|
|
@@ -1136,7 +1155,7 @@ export class CommandRouter {
|
|
|
1136
1155
|
async handleLsRefresh(value, ctx) {
|
|
1137
1156
|
const targetPath = value.path;
|
|
1138
1157
|
const offset = value.offset ?? 0;
|
|
1139
|
-
// TOCTOU guard
|
|
1158
|
+
// TOCTOU guard: existsSync → statSync can race (target
|
|
1140
1159
|
// deleted in between), throwing into enqueueImmediate's catch which only
|
|
1141
1160
|
// logs — violating the "card button clicks must give visible feedback"
|
|
1142
1161
|
// red line. Treat a vanished/non-dir path as "use current cwd" instead of
|
|
@@ -1166,7 +1185,7 @@ export class CommandRouter {
|
|
|
1166
1185
|
return { toast: { type: 'error', content: paging.error } };
|
|
1167
1186
|
}
|
|
1168
1187
|
const resolvedTarget = path.resolve(targetPath);
|
|
1169
|
-
// TOCTOU guard
|
|
1188
|
+
// TOCTOU guard: existsSync → statSync can race; without the
|
|
1170
1189
|
// try/catch the throw lands in enqueueImmediate's silent .catch and the
|
|
1171
1190
|
// paginated card never updates (no visible feedback for the click).
|
|
1172
1191
|
let isDir;
|
|
@@ -1189,7 +1208,7 @@ export class CommandRouter {
|
|
|
1189
1208
|
*
|
|
1190
1209
|
* offset 恒重置为 0(新筛选从第 1 页开始)。路径校验与 TOCTOU 兜底照
|
|
1191
1210
|
* handleLsPage——校验失败只回错误 toast,绝不动卡片(否则用户看到一张空卡)。
|
|
1192
|
-
* ls.browse / ls.switch 的 value 里刻意不带 q
|
|
1211
|
+
* ls.browse / ls.switch 的 value 里刻意不带 q,换目录即清除筛选。
|
|
1193
1212
|
*/
|
|
1194
1213
|
async handleLsFilter(value, ctx) {
|
|
1195
1214
|
const targetPath = value.path;
|
|
@@ -1333,7 +1352,7 @@ export class CommandRouter {
|
|
|
1333
1352
|
};
|
|
1334
1353
|
}
|
|
1335
1354
|
/**
|
|
1336
|
-
*
|
|
1355
|
+
* 单源:重建 order/ws 列表卡(不投递,card 进 callback 响应体用)。
|
|
1337
1356
|
* `q` 只服务 ws 卡的关键词筛选;order 卡无此功能,不透传。
|
|
1338
1357
|
*/
|
|
1339
1358
|
rebuildListCard(kind, offset, ctx, q) {
|
|
@@ -1342,7 +1361,7 @@ export class CommandRouter {
|
|
|
1342
1361
|
return this.cmdWs([], ctx, offset, q).card;
|
|
1343
1362
|
}
|
|
1344
1363
|
/**
|
|
1345
|
-
*
|
|
1364
|
+
* 单源:「重建列表卡 → updateCardInPlace」原地刷新骨架(列表卡删除/
|
|
1346
1365
|
* 翻页/移除/排序/筛选动作共用)。返回重建后的卡片;无卡时返回 undefined 且跳过更新。
|
|
1347
1366
|
*/
|
|
1348
1367
|
async refreshListCard(kind, value, ctx) {
|
|
@@ -1353,7 +1372,7 @@ export class CommandRouter {
|
|
|
1353
1372
|
return card;
|
|
1354
1373
|
}
|
|
1355
1374
|
/**
|
|
1356
|
-
*
|
|
1375
|
+
* 单源:order 三个 input handler(aliasInput/aliasRemove/textInput)
|
|
1357
1376
|
* 的公共骨架——校验 orderId → reload+find → mutate → 成功后 toast + raw 卡。
|
|
1358
1377
|
* mutate 返回 error 时仅回 error toast,不重渲染。
|
|
1359
1378
|
* 注意:本 helper 只服务「toast + card 进响应体」语义(飞书需用响应体原地
|
|
@@ -1576,7 +1595,7 @@ export class CommandRouter {
|
|
|
1576
1595
|
});
|
|
1577
1596
|
}
|
|
1578
1597
|
/**
|
|
1579
|
-
* Handle ws.use card action: switch to the
|
|
1598
|
+
* Handle ws.use card action: switch to the cwd, then refresh the /ws
|
|
1580
1599
|
* list card in place so "recent" sort order is immediately visible.
|
|
1581
1600
|
*
|
|
1582
1601
|
* Unlike the command-line `/ws use` (which returns text), the card action
|
|
@@ -1586,7 +1605,7 @@ export class CommandRouter {
|
|
|
1586
1605
|
async handleWsUse(value, ctx) {
|
|
1587
1606
|
const name = value.name ?? '';
|
|
1588
1607
|
const useResult = this.cmdWs(['use', name], ctx);
|
|
1589
|
-
// Error cases (
|
|
1608
|
+
// Error cases (cwd not found, path invalid, etc.): relay as error toast
|
|
1590
1609
|
if (useResult.text && !useResult.text.includes('已切换')) {
|
|
1591
1610
|
return { toast: { type: 'error', content: useResult.text } };
|
|
1592
1611
|
}
|
|
@@ -1615,7 +1634,7 @@ export class CommandRouter {
|
|
|
1615
1634
|
};
|
|
1616
1635
|
}
|
|
1617
1636
|
/**
|
|
1618
|
-
* Handle ws.remove: remove the
|
|
1637
|
+
* Handle ws.remove: remove the cwd alias and refresh the /ws list card
|
|
1619
1638
|
* in place (mirrors handleOrderDelete). Without this the stale alias would
|
|
1620
1639
|
* remain visible on the card the user just clicked.
|
|
1621
1640
|
*/
|
|
@@ -1844,7 +1863,7 @@ export class CommandRouter {
|
|
|
1844
1863
|
try {
|
|
1845
1864
|
const oldDefaultAgent = this.config.defaultAgent;
|
|
1846
1865
|
this.config = setConfigValues(this.configPath, this.config, updates);
|
|
1847
|
-
//
|
|
1866
|
+
// 运行时传播(idleTimeout / clearRunners / syncAgentChoices /
|
|
1848
1867
|
// defaultAgent 切换的 session 处理)抽到共享方法,与文本直写路径共用。
|
|
1849
1868
|
const switchResult = this.propagateConfigSave(oldDefaultAgent, updates, ctx);
|
|
1850
1869
|
this.pendingConfig = null;
|
|
@@ -1890,7 +1909,7 @@ export class CommandRouter {
|
|
|
1890
1909
|
return diffConfig(original, pending);
|
|
1891
1910
|
}
|
|
1892
1911
|
/**
|
|
1893
|
-
* config
|
|
1912
|
+
* config 写盘后的运行时传播(自 config.save 卡片路径抽出,卡片保存与
|
|
1894
1913
|
* `/config <key> <value>` 文本直写两条路径共用)。
|
|
1895
1914
|
*
|
|
1896
1915
|
* 1. bridge 运行时值同步:setConfig + idle watchdog timeout;
|
|
@@ -1898,7 +1917,7 @@ export class CommandRouter {
|
|
|
1898
1917
|
* 配置值,/status 与真实 run 自相矛盾);
|
|
1899
1918
|
* 3. defaultAgent 切换 → 旧 agent session 存 previousSessions、新 agent 恢复
|
|
1900
1919
|
* 或清空(等效 /new);
|
|
1901
|
-
* 4. 当前 agent 配置变更 → syncAgentChoices
|
|
1920
|
+
* 4. 当前 agent 配置变更 → syncAgentChoices 并原子写盘(禁裸同步写盘,
|
|
1902
1921
|
* 写一半崩溃会截断 config.yaml 导致 bridge 起不来)。
|
|
1903
1922
|
*
|
|
1904
1923
|
* @param updates 以 config 路径形式给出的变更 key(卡片路径来自 diffConfig;
|
|
@@ -1972,7 +1991,7 @@ export class CommandRouter {
|
|
|
1972
1991
|
switchSessionId = '';
|
|
1973
1992
|
}
|
|
1974
1993
|
}
|
|
1975
|
-
// DSH preset 变更 = 下次 run 新建 session(§4.3
|
|
1994
|
+
// DSH preset 变更 = 下次 run 新建 session(§4.3):
|
|
1976
1995
|
// preset 在 session 创建时固定,中途切换会被服务端拒绝(agent-preset-conflict)。
|
|
1977
1996
|
// 旧 sessionId 存入 previousSessions 停车位(可手动 /resume,但 /resume 会做
|
|
1978
1997
|
// preset 一致性校验),当前 sessionId 清空——下次 run 走 session.create 新建。
|
|
@@ -1990,7 +2009,7 @@ export class CommandRouter {
|
|
|
1990
2009
|
switchNotice = `DSH preset 已变更,下次消息将新建会话`;
|
|
1991
2010
|
}
|
|
1992
2011
|
}
|
|
1993
|
-
// DSH host 变更 =
|
|
2012
|
+
// DSH host 变更 = 换服务:旧 host 的 sessionId 发往新 host 是错的。
|
|
1994
2013
|
// 视为 session 边界,仿 preset 分支停车 + 清空当前 sessionId,下次 run 新建会话。
|
|
1995
2014
|
const hasDshHostChange = Object.keys(updates).some((k) => k === 'agents.dsh.host');
|
|
1996
2015
|
if (hasDshHostChange && this.config.defaultAgent === 'dsh' && !switchNotice) {
|
|
@@ -2010,11 +2029,11 @@ export class CommandRouter {
|
|
|
2010
2029
|
const hasCurrentAgentUpdate = Object.keys(updates).some((k) => k === currentAgentConfigKey || k.startsWith(`${currentAgentConfigKey}.`));
|
|
2011
2030
|
if (hasCurrentAgentUpdate) {
|
|
2012
2031
|
this.config = syncAgentChoices(this.config, currentAgent);
|
|
2013
|
-
//
|
|
2032
|
+
// agentChoices 同步写盘必须走原子写(tmp+rename),禁裸同步写盘
|
|
2014
2033
|
atomicWrite(this.configPath, YAML.stringify(this.config));
|
|
2015
2034
|
this.bridge.setConfig(this.config);
|
|
2016
2035
|
}
|
|
2017
|
-
//
|
|
2036
|
+
// live approval settings: active workspace-lifetime runners
|
|
2018
2037
|
// are not evicted by clearRunners() (would orphan the live subprocess).
|
|
2019
2038
|
// Push the new approval-mode settings to those runners via the unified
|
|
2020
2039
|
// duck method `updateApprovalMode` (codex thread/settings/update,
|
|
@@ -2366,7 +2385,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2366
2385
|
catch (err) {
|
|
2367
2386
|
return { text: `❌ 版本检查失败: ${err.message}` };
|
|
2368
2387
|
}
|
|
2369
|
-
if (
|
|
2388
|
+
if (this.updateFns.compareVersions(current, latest) !== 1) {
|
|
2370
2389
|
return { text: `✅ 已是最新版本 ${current}` };
|
|
2371
2390
|
}
|
|
2372
2391
|
if (checkOnly) {
|
|
@@ -2398,7 +2417,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2398
2417
|
}
|
|
2399
2418
|
cmdNew(ctx) {
|
|
2400
2419
|
// 只清 sessionId 保留 cwd — 否则 /new 之后 /resume 会提示"请先 /cd
|
|
2401
|
-
// 设置工作目录",但用户的
|
|
2420
|
+
// 设置工作目录",但用户的 cwd 还在。2026-06-21 复盘:用户
|
|
2402
2421
|
// /new → /resume 后被误导以为需要重新 /cd。
|
|
2403
2422
|
const agentName = agentDisplayName(this.config.defaultAgent);
|
|
2404
2423
|
this.sessionStore.clearSessionId(ctx.userId, this.config.defaultAgent, {
|
|
@@ -2427,7 +2446,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2427
2446
|
// Claude writes into JSONL (e.g. `/tmp` → `/private/tmp` on macOS).
|
|
2428
2447
|
// Without this, a hand-typed unresolved path never matches any JSONL's
|
|
2429
2448
|
// cwd field and the user's `/cd /tmp/foo` never finds their sessions.
|
|
2430
|
-
// TOCTOU: the directory may vanish between stat and realpath
|
|
2449
|
+
// TOCTOU: the directory may vanish between stat and realpath.
|
|
2431
2450
|
// Reject gracefully instead of letting realpathSync throw into the queue
|
|
2432
2451
|
// task's .catch (which only logs — the user gets no feedback). Mirrors
|
|
2433
2452
|
// handleLsSwitch's realpath guard so /cd and ls.switch behave identically.
|
|
@@ -2484,7 +2503,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2484
2503
|
*/
|
|
2485
2504
|
switchCwdAndNotify(userId, canonical, _ctx) {
|
|
2486
2505
|
this.sessionStore.setCwd(userId, canonical);
|
|
2487
|
-
// Auto-resume newest session if exists (
|
|
2506
|
+
// Auto-resume newest session if exists (读取失败不阻断切换,给可见提示)
|
|
2488
2507
|
let newestSession;
|
|
2489
2508
|
try {
|
|
2490
2509
|
newestSession = this.sessionReader.getNewestSession(canonical);
|
|
@@ -2615,7 +2634,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2615
2634
|
}
|
|
2616
2635
|
/**
|
|
2617
2636
|
* Whether a session of the given agent kind can be compacted from a resume
|
|
2618
|
-
* card.
|
|
2637
|
+
* card. duck-typing — check if the runner has `runCompact`
|
|
2619
2638
|
* (codex app-server, kimi acp), instead of hardcoding agentKind === 'codex'.
|
|
2620
2639
|
*/
|
|
2621
2640
|
canCompactSession(agentKind, cwd) {
|
|
@@ -2667,7 +2686,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2667
2686
|
* @param offset 分页起点(条目数)
|
|
2668
2687
|
* @param rootDir 浏览起点:卡片「返回」按钮回到这里。省略时默认 = 本次列出的
|
|
2669
2688
|
* 目录(`/ls <dir>` 的起点即该目录),因此「返回」不会把用户
|
|
2670
|
-
* 丢回
|
|
2689
|
+
* 丢回 工作目录(用户明确反馈过)。
|
|
2671
2690
|
* @param q 关键词筛选:只匹配**当前层**条目名(不递归),大小写不敏感。
|
|
2672
2691
|
* 省略/空 = 无筛选。
|
|
2673
2692
|
*/
|
|
@@ -2709,7 +2728,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2709
2728
|
}
|
|
2710
2729
|
// 浏览起点(「返回」按钮的目标):`/ls <dir>` 的起点就是该目录本身,之后
|
|
2711
2730
|
// 进入子目录/翻页/刷新时由卡片 payload 的 root 一路带过来。省略时 = 本次
|
|
2712
|
-
//
|
|
2731
|
+
// 列出的目录,因此「返回」永远不会把用户丢回工作目录。
|
|
2713
2732
|
const browseRoot = rootDir ? path.resolve(cwd, rootDir) : targetDir;
|
|
2714
2733
|
try {
|
|
2715
2734
|
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
@@ -3025,7 +3044,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3025
3044
|
text: { tag: 'plain_text', content: '上级' },
|
|
3026
3045
|
type: 'default',
|
|
3027
3046
|
size: 'small',
|
|
3028
|
-
// 浏览起点 = 文件所在目录:进入后「返回」回到这里,不回
|
|
3047
|
+
// 浏览起点 = 文件所在目录:进入后「返回」回到这里,不回 工作目录
|
|
3029
3048
|
behaviors: [
|
|
3030
3049
|
{ type: 'callback', value: { cmd: 'ls.browse', path: parentDir, root: parentDir } },
|
|
3031
3050
|
],
|
|
@@ -3092,7 +3111,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3092
3111
|
if (!fs.existsSync(wsPath))
|
|
3093
3112
|
return { text: `路径不存在: ${wsPath}` };
|
|
3094
3113
|
// Canonicalize via realpath so it matches Claude JSONL cwd (2026-06-21).
|
|
3095
|
-
// TOCTOU guard
|
|
3114
|
+
// TOCTOU guard: mirror cmdCd — if the path vanishes
|
|
3096
3115
|
// between existsSync and realpath, reply gracefully instead of
|
|
3097
3116
|
// throwing into the queue task's silent .catch.
|
|
3098
3117
|
let canonical;
|
|
@@ -3145,7 +3164,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3145
3164
|
// Pagination calculations (mirror cmdOrder): clamp stale/out-of-range
|
|
3146
3165
|
// offsets so prev/next always step by WS_PAGE_SIZE.
|
|
3147
3166
|
const { totalCount, totalPages, safeOffset, currentPage, pageItems: pageEntries, hasPagination, } = pageSlice(visible, offset, WS_PAGE_SIZE);
|
|
3148
|
-
// Build body elements: current cwd +
|
|
3167
|
+
// Build body elements: current cwd + cwd list with dividers
|
|
3149
3168
|
const bodyElements = [];
|
|
3150
3169
|
// Current working directory section
|
|
3151
3170
|
bodyElements.push({
|
|
@@ -3313,7 +3332,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3313
3332
|
}
|
|
3314
3333
|
}
|
|
3315
3334
|
/**
|
|
3316
|
-
*
|
|
3335
|
+
* degraded card when a session's export is non-empty but unparseable
|
|
3317
3336
|
* (truncated by opencode's pipe cap on huge sessions, or otherwise corrupt).
|
|
3318
3337
|
* The session exists, so reuse listSessions metadata (title) instead of the
|
|
3319
3338
|
* misleading "未找到". Reuses the same card-builder helpers (markdownDiv) as
|
|
@@ -3408,7 +3427,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3408
3427
|
};
|
|
3409
3428
|
}
|
|
3410
3429
|
}
|
|
3411
|
-
//
|
|
3430
|
+
// 校验通过后才写入
|
|
3412
3431
|
this.sessionStore.setSessionId(ctx.userId, agentKind, sessionIdArg, cwd);
|
|
3413
3432
|
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(sessionIdArg, cwd, {
|
|
3414
3433
|
agentKind,
|
|
@@ -3471,7 +3490,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3471
3490
|
}
|
|
3472
3491
|
}
|
|
3473
3492
|
catch (err) {
|
|
3474
|
-
//
|
|
3493
|
+
// 读取失败必须给用户可见反馈,而不是误导性的「没有 session 记录」
|
|
3475
3494
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3476
3495
|
return { text: `读取 ${agentDisplayName(agentKind)} session 列表失败: ${msg}` };
|
|
3477
3496
|
}
|
|
@@ -3543,7 +3562,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3543
3562
|
const prefetchedTitle = meta?.displayTitle;
|
|
3544
3563
|
// aiTitle 不能单独作为"预取标题"键:若 displayTitle 缺失(理论上 opencode
|
|
3545
3564
|
// info.title 非空但 export 无 user text),标题兜底是 summary,label 必须
|
|
3546
|
-
// 是中性"会话摘要"而不是"最近输入"(Review
|
|
3565
|
+
// 是中性"会话摘要"而不是"最近输入"(Review 边缘分支)。
|
|
3547
3566
|
const titleText = prefetchedTitle ?? (summaryIsPlaceholder ? '' : s.summary);
|
|
3548
3567
|
const titleLabel = prefetchedTitle
|
|
3549
3568
|
? meta?.aiTitle
|
|
@@ -3708,7 +3727,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3708
3727
|
elements.push({ tag: 'hr' });
|
|
3709
3728
|
}
|
|
3710
3729
|
}
|
|
3711
|
-
// Pagination bar
|
|
3730
|
+
// Pagination bar(复用 paginationBar:safeOffset 已对齐页边界,
|
|
3712
3731
|
// offset±pageSize 与原先 currentPage±1 的页码换算等价)
|
|
3713
3732
|
if (totalPages > 1) {
|
|
3714
3733
|
elements.push(...paginationBar({
|
|
@@ -3744,14 +3763,14 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3744
3763
|
const value = args.slice(1).join(' ');
|
|
3745
3764
|
try {
|
|
3746
3765
|
const oldDefaultAgent = this.config.defaultAgent;
|
|
3747
|
-
//
|
|
3766
|
+
// 直写前若 pendingConfig 存在未保存差异,提示用户被丢弃的条数,
|
|
3748
3767
|
// 避免静默清空导致用户在卡片上的修改无反馈丢失。差异必须在 setConfigValue
|
|
3749
3768
|
// 之前计算——直写本身也会改变 config,不应计入「被丢弃的未保存修改」。
|
|
3750
3769
|
const discarded = this.pendingConfig
|
|
3751
3770
|
? Object.keys(this.diffConfig(this.config, this.pendingConfig)).length
|
|
3752
3771
|
: 0;
|
|
3753
3772
|
this.config = setConfigValue(this.configPath, this.config, key, value);
|
|
3754
|
-
//
|
|
3773
|
+
// 直写路径与 config.save 卡片路径共用运行时传播(idleTimeout /
|
|
3755
3774
|
// clearRunners / syncAgentChoices / defaultAgent 切换)。key 经 mapAgentKey
|
|
3756
3775
|
// 归一化,pi./codex./opencode./kimi. 命中 agents.* 判定;claude. 与
|
|
3757
3776
|
// defaultAgent 不再漏判。
|