lark-remote 0.1.1 → 0.1.3
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/README.en.md +2 -5
- package/README.md +1 -4
- package/dist/bridge/index.d.ts +1 -3
- package/dist/bridge/index.js +11 -12
- package/dist/card/card-budget.js +1 -2
- package/dist/card/card-shared.d.ts +8 -0
- package/dist/card/card-shared.js +14 -0
- package/dist/card/run-renderer.js +1 -0
- package/dist/card/run-state.d.ts +4 -0
- package/dist/config/codex-config.d.ts +3 -5
- package/dist/config/codex-config.js +7 -7
- package/dist/config/dir.d.ts +1 -0
- package/dist/config/dir.js +4 -0
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -11
- package/dist/connector/index.js +28 -0
- package/dist/index.js +89 -41
- package/dist/router/config/codex.js +8 -12
- package/dist/router/index.d.ts +15 -2
- package/dist/router/index.js +314 -44
- package/dist/router/utils.d.ts +3 -0
- package/dist/router/utils.js +15 -3
- package/dist/runner/claude/runner.d.ts +0 -1
- package/dist/runner/claude/runner.js +1 -1
- package/dist/runner/codex/runner.d.ts +0 -2
- package/dist/runner/codex/runner.js +1 -1
- package/dist/runner/common/spawning-runner.d.ts +0 -1
- package/dist/runner/common/spawning-runner.js +2 -4
- package/dist/runner/kimi/runner.d.ts +0 -1
- package/dist/runner/kimi/runner.js +1 -1
- package/dist/runner/opencode/runner.d.ts +0 -2
- package/dist/runner/opencode/runner.js +1 -1
- package/dist/runner/pi/runner.d.ts +0 -1
- package/dist/runner/pi/runner.js +1 -1
- package/dist/runner/probe.d.ts +45 -0
- package/dist/runner/probe.js +87 -0
- package/dist/runner/types.d.ts +3 -0
- package/dist/session/claude/sessions.d.ts +8 -6
- package/dist/session/claude/sessions.js +178 -26
- package/dist/session/codex/rollout-reader.d.ts +1 -0
- package/dist/session/codex/rollout-reader.js +15 -0
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +2 -1
- package/dist/session/kimi/sessions.d.ts +3 -1
- package/dist/session/kimi/sessions.js +42 -6
- package/dist/session/opencode/sessions.d.ts +0 -2
- package/dist/session/opencode/sessions.js +1 -1
- package/dist/startup-contact.d.ts +4 -2
- package/dist/startup-contact.js +7 -4
- package/package.json +39 -4
package/dist/router/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { MAX_FILE_UPLOAD_SIZE } from '../connector/file-limits.js';
|
|
|
13
13
|
import { atomicWrite } from '../persistence/atomic-write.js';
|
|
14
14
|
/** Config card builder - delegates to per-agent builders */
|
|
15
15
|
import { getConfigBuilder, listRegisteredAgents } from './config/index.js';
|
|
16
|
+
import { probeAllAgents, getCachedAvailability } from '../runner/probe.js';
|
|
16
17
|
import { buildConfigCardFromTabs } from './config/common/render.js';
|
|
17
18
|
/** Max events to read for auto-resume and /resume cards.
|
|
18
19
|
* Limits events at the read stage so enforceCardBudget rarely needs to truncate.
|
|
@@ -20,6 +21,10 @@ import { buildConfigCardFromTabs } from './config/common/render.js';
|
|
|
20
21
|
const AUTO_RESUME_MAX_EVENTS = 5;
|
|
21
22
|
/** /resume 列表页大小;`/resume [N]` 的 N clamp 到 [1, RESUME_PAGE_SIZE]。 */
|
|
22
23
|
const RESUME_PAGE_SIZE = 5;
|
|
24
|
+
/** /active 卡片每页显示的最大条目数(agent run + bash run 合计)。 */
|
|
25
|
+
const ACTIVE_PAGE_SIZE = 20;
|
|
26
|
+
/** /order 列表页大小;指令超过此数量时显示分页导航栏。 */
|
|
27
|
+
const ORDER_PAGE_SIZE = 20;
|
|
23
28
|
/** /resume 列表页内全量预取(readSessionContent)的行数上限;其余行用轻量 summary 兜底。 */
|
|
24
29
|
const RESUME_CONTENT_PREFETCH = 5;
|
|
25
30
|
/**
|
|
@@ -43,7 +48,7 @@ import { newSessionButton, agentDisplayName } from '../card/card-shared.js';
|
|
|
43
48
|
* stop — interrupt current run
|
|
44
49
|
* ls.file — send file only
|
|
45
50
|
* ws.remove — delete workspace alias only
|
|
46
|
-
* resume.use — set sessionId
|
|
51
|
+
* resume.use — set sessionId + agent for correct reader routing
|
|
47
52
|
* help.* — read-only help commands
|
|
48
53
|
* order.delete — delete order only
|
|
49
54
|
* order.exec is intercepted by index.ts at the enqueue boundary
|
|
@@ -61,6 +66,7 @@ export function isImmediateAction(cmd) {
|
|
|
61
66
|
cmd === 'ls.switch' ||
|
|
62
67
|
cmd === 'ls.page' ||
|
|
63
68
|
cmd === 'resume.page' || // control operation: paginate only, never spawns claude
|
|
69
|
+
cmd === 'active.page' || // control operation: paginate active card
|
|
64
70
|
cmd === 'ws.remove' ||
|
|
65
71
|
cmd === 'resume.use' ||
|
|
66
72
|
cmd === 'ws.use' ||
|
|
@@ -70,12 +76,15 @@ export function isImmediateAction(cmd) {
|
|
|
70
76
|
cmd === 'queue.edit' ||
|
|
71
77
|
cmd === 'queue.input' ||
|
|
72
78
|
cmd === 'order.delete' ||
|
|
79
|
+
cmd === 'order.page' ||
|
|
73
80
|
cmd === 'config.toggle' ||
|
|
74
81
|
cmd === 'config.set' ||
|
|
75
82
|
cmd === 'config.input' ||
|
|
76
83
|
cmd === 'config.save');
|
|
77
84
|
}
|
|
78
85
|
export class CommandRouter {
|
|
86
|
+
/** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
|
|
87
|
+
static VALID_AGENTS = ['claude', 'codex', 'opencode', 'pi', 'kimi'];
|
|
79
88
|
sessionStore;
|
|
80
89
|
bridge;
|
|
81
90
|
config;
|
|
@@ -200,10 +209,10 @@ export class CommandRouter {
|
|
|
200
209
|
case 'ws.remove':
|
|
201
210
|
return await this.handleWsRemove(value, ctx);
|
|
202
211
|
case 'resume.use': {
|
|
203
|
-
// resume.use
|
|
212
|
+
// resume.use carries an agent field from /resume [agent] list cards
|
|
213
|
+
// (both active and completed cards include the agent field).
|
|
204
214
|
const resumeAgent = value.agent ?? this.config.defaultAgent;
|
|
205
|
-
|
|
206
|
-
const validAgents = ['claude', 'codex', 'opencode', 'pi', 'kimi'];
|
|
215
|
+
const validAgents = CommandRouter.VALID_AGENTS;
|
|
207
216
|
const resolvedAgent = validAgents.includes(resumeAgent)
|
|
208
217
|
? resumeAgent
|
|
209
218
|
: this.config.defaultAgent;
|
|
@@ -226,6 +235,8 @@ export class CommandRouter {
|
|
|
226
235
|
}
|
|
227
236
|
case 'resume.page':
|
|
228
237
|
return this.handleResumePage(value, ctx);
|
|
238
|
+
case 'active.page':
|
|
239
|
+
return this.handleActivePage(value, ctx);
|
|
229
240
|
case 'new-session':
|
|
230
241
|
this.sessionStore.clearSessionId(ctx.userId, this.config.defaultAgent, {
|
|
231
242
|
clearSessionCwd: true,
|
|
@@ -260,6 +271,8 @@ export class CommandRouter {
|
|
|
260
271
|
return await this.handleQueueInput(value, ctx);
|
|
261
272
|
case 'order.delete':
|
|
262
273
|
return await this.handleOrderDelete(value, ctx);
|
|
274
|
+
case 'order.page':
|
|
275
|
+
return await this.handleOrderPage(value, ctx);
|
|
263
276
|
case 'config.toggle':
|
|
264
277
|
case 'config.set':
|
|
265
278
|
case 'config.input':
|
|
@@ -617,25 +630,57 @@ export class CommandRouter {
|
|
|
617
630
|
await this.bridge.sendResult({ text: '⚠️ 缺少新消息内容' }, ctx);
|
|
618
631
|
return;
|
|
619
632
|
}
|
|
633
|
+
// RACE FIX: register the replacement closure BEFORE any await. The old
|
|
634
|
+
// code awaited updateMessagePreview first, yielding to the microtask
|
|
635
|
+
// queue. Between that yield and the later setTaskReplacement call, the
|
|
636
|
+
// queue chain could advance: the task's begin path would find no
|
|
637
|
+
// replacement → the original stale closure would run → the user saw
|
|
638
|
+
// "消息已更新" but the agent executed the old prompt.
|
|
639
|
+
//
|
|
640
|
+
// By registering synchronously first, the begin path always finds the
|
|
641
|
+
// replacement if the task starts during the subsequent await. Both
|
|
642
|
+
// setTaskReplacement and the begin-path consumption are synchronous, so
|
|
643
|
+
// there is no interleaving window.
|
|
644
|
+
//
|
|
645
|
+
// D3/Step3:复用原任务 binding(不重新快照)+ 恢复 cwdOverride。
|
|
646
|
+
const inputTask = this.bridge.getQueuedTask(workspace, messageId);
|
|
647
|
+
if (!inputTask) {
|
|
648
|
+
// Task already left the queue (began or was cancelled) before we could
|
|
649
|
+
// register the replacement. Return a toast so the edit-form card gets
|
|
650
|
+
// dismissed — sendResult alone leaves the card stuck in the edit state
|
|
651
|
+
// (Feishu keeps the pre-click card when the callback response has no
|
|
652
|
+
// toast/card field).
|
|
653
|
+
return { toast: { type: 'info', content: '任务已不在队列中(可能已开始执行或被撤销)' } };
|
|
654
|
+
}
|
|
655
|
+
this.bridge.setTaskReplacement(workspace, messageId, async () => {
|
|
656
|
+
await this.handle(newMessage, ctx, { cwdOverride: workspace, binding: inputTask.binding });
|
|
657
|
+
});
|
|
620
658
|
// Update the message preview (and editedMessage so handleQueueImmediate
|
|
621
659
|
// re-enqueues the edited content instead of the stale original closure),
|
|
622
660
|
// and build the updated queue card. Returning the card in the callback
|
|
623
661
|
// response makes Feishu render it in place -- a toast-only response leaves
|
|
624
662
|
// the card stuck in the edit state (Feishu keeps the pre-click card when
|
|
625
663
|
// the callback response has no `card` field).
|
|
664
|
+
//
|
|
665
|
+
// After the synchronous replacement registration above, it is safe to
|
|
666
|
+
// await here: even if the queue chain advances and the replacement is
|
|
667
|
+
// consumed, the user-facing card update is best-effort (the executing card
|
|
668
|
+
// is updated by updateQueueCardToExecuting instead).
|
|
626
669
|
const card = await this.bridge.updateMessagePreview(workspace, messageId, newMessage);
|
|
627
670
|
if (!card) {
|
|
628
|
-
|
|
629
|
-
|
|
671
|
+
// Task left the queue during the await (began or cancelled). The
|
|
672
|
+
// replacement was already consumed (begin path) or cleaned up
|
|
673
|
+
// (cancel/removeFromQueue path). Inform the user the edit didn't stick
|
|
674
|
+
// as a *card update*, but the replacement closure was already in effect.
|
|
675
|
+
if (this.bridge.hasTaskBegan(messageId)) {
|
|
676
|
+
return {
|
|
677
|
+
toast: { type: 'success', content: '消息已更新(任务已开始执行)' },
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
toast: { type: 'info', content: '任务已不在队列中,编辑未生效' },
|
|
682
|
+
};
|
|
630
683
|
}
|
|
631
|
-
// 编辑即生效:自然轮到时(不经立即执行)也必须执行编辑后内容,而不是
|
|
632
|
-
// enqueue 时闭包冻结的旧指令。setTaskReplacement 原位替换该任务槽位的闭包,
|
|
633
|
-
// 保持队列位置;handleQueueImmediate 后续注册/刷新幂等覆盖。
|
|
634
|
-
// D3/Step3:复用原任务 binding(不重新快照)+ 恢复 cwdOverride。
|
|
635
|
-
const inputTask = this.bridge.getQueuedTask(workspace, messageId);
|
|
636
|
-
this.bridge.setTaskReplacement(workspace, messageId, async () => {
|
|
637
|
-
await this.handle(newMessage, ctx, { cwdOverride: workspace, binding: inputTask?.binding });
|
|
638
|
-
});
|
|
639
684
|
return {
|
|
640
685
|
toast: { type: 'success', content: '消息已更新' },
|
|
641
686
|
card: { type: 'raw', data: card },
|
|
@@ -737,8 +782,7 @@ export class CommandRouter {
|
|
|
737
782
|
*/
|
|
738
783
|
async handleResumePage(value, ctx) {
|
|
739
784
|
const resumeAgent = value.agent ?? this.config.defaultAgent;
|
|
740
|
-
|
|
741
|
-
const validAgents = ['claude', 'codex', 'opencode', 'pi', 'kimi'];
|
|
785
|
+
const validAgents = CommandRouter.VALID_AGENTS;
|
|
742
786
|
const resolvedAgent = validAgents.includes(resumeAgent)
|
|
743
787
|
? resumeAgent
|
|
744
788
|
: this.config.defaultAgent;
|
|
@@ -774,6 +818,21 @@ export class CommandRouter {
|
|
|
774
818
|
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
775
819
|
return { toast: { type: 'success', content: '' } };
|
|
776
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Handle active.page: paginate the /active run list in place.
|
|
823
|
+
* Mirrors handleResumePage — updates the same card, never sends a new one.
|
|
824
|
+
*/
|
|
825
|
+
async handleActivePage(value, ctx) {
|
|
826
|
+
const rawOffset = Number(value.offset);
|
|
827
|
+
const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.trunc(rawOffset)) : 0;
|
|
828
|
+
const alignedOffset = Math.floor(offset / ACTIVE_PAGE_SIZE) * ACTIVE_PAGE_SIZE;
|
|
829
|
+
const result = this.cmdActive([], ctx, alignedOffset);
|
|
830
|
+
if (!result.card) {
|
|
831
|
+
return { toast: { type: 'error', content: '当前没有正在进行中的任务' } };
|
|
832
|
+
}
|
|
833
|
+
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
834
|
+
return { toast: { type: 'success', content: '' } };
|
|
835
|
+
}
|
|
777
836
|
/**
|
|
778
837
|
* Handle ls.browse: browse directory without switching cwd.
|
|
779
838
|
*/
|
|
@@ -856,11 +915,23 @@ export class CommandRouter {
|
|
|
856
915
|
}
|
|
857
916
|
// Delete the order
|
|
858
917
|
this.orderStore.remove(orderId);
|
|
859
|
-
// Refresh the order list and update card in place
|
|
860
|
-
const
|
|
918
|
+
// Refresh the order list and update card in place, preserving current page
|
|
919
|
+
const currentOffset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
920
|
+
const result = this.cmdOrder([], ctx, currentOffset);
|
|
861
921
|
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
862
922
|
return { toast: { type: 'success', content: '已删除指令' } };
|
|
863
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* Handle order.page: paginate the /order list in place.
|
|
926
|
+
* Mirrors handleLsPage — updates the same card, never sends a new one.
|
|
927
|
+
*/
|
|
928
|
+
async handleOrderPage(value, ctx) {
|
|
929
|
+
const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
930
|
+
// cmdOrder internally clamps stale/out-of-range offsets
|
|
931
|
+
const result = this.cmdOrder([], ctx, offset);
|
|
932
|
+
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
933
|
+
return { toast: { type: 'success', content: '' } };
|
|
934
|
+
}
|
|
864
935
|
/**
|
|
865
936
|
* Handle ws.remove: remove the workspace alias and refresh the /ws list card
|
|
866
937
|
* in place (mirrors handleOrderDelete). Without this the stale alias would
|
|
@@ -1293,7 +1364,7 @@ export class CommandRouter {
|
|
|
1293
1364
|
return await this.cmdReconnect();
|
|
1294
1365
|
case 'config':
|
|
1295
1366
|
case 'c':
|
|
1296
|
-
return this.cmdConfig(args, ctx);
|
|
1367
|
+
return await this.cmdConfig(args, ctx);
|
|
1297
1368
|
case 'order':
|
|
1298
1369
|
case 'o':
|
|
1299
1370
|
return this.cmdOrder(args, ctx);
|
|
@@ -2085,7 +2156,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2085
2156
|
const entry = this.sessionStore.get(ctx.userId);
|
|
2086
2157
|
const cwd = entry?.cwd;
|
|
2087
2158
|
// Valid agent kinds for the /resume [agent] [N] feature
|
|
2088
|
-
const VALID_AGENTS =
|
|
2159
|
+
const VALID_AGENTS = CommandRouter.VALID_AGENTS;
|
|
2089
2160
|
// Parse arguments: /resume [agent] [N] or /resume [sessionId] or /resume [agent] [sessionId]
|
|
2090
2161
|
let agentKind = this.config.defaultAgent;
|
|
2091
2162
|
let limit;
|
|
@@ -2440,7 +2511,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2440
2511
|
};
|
|
2441
2512
|
return { card };
|
|
2442
2513
|
}
|
|
2443
|
-
cmdActive(_args, _ctx) {
|
|
2514
|
+
cmdActive(_args, _ctx, offset = 0) {
|
|
2444
2515
|
// List all active runs from bridge memory (not from file system scan)
|
|
2445
2516
|
// New semantics (2026-07-20): only shows runs started by THIS bridge process
|
|
2446
2517
|
const activeRuns = this.bridge.getActiveRuns();
|
|
@@ -2449,18 +2520,40 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2449
2520
|
return { text: '当前没有正在进行中的任务' };
|
|
2450
2521
|
}
|
|
2451
2522
|
// Build CardKit 2.0 card showing memory-based active runs
|
|
2452
|
-
return this.buildActiveCardFromMemory(activeRuns, activeBashRuns);
|
|
2523
|
+
return this.buildActiveCardFromMemory(activeRuns, activeBashRuns, offset);
|
|
2453
2524
|
}
|
|
2454
2525
|
/**
|
|
2455
2526
|
* Build CardKit 2.0 /active card from bridge memory (not file system).
|
|
2456
2527
|
* Shows only runs that were started by THIS bridge process.
|
|
2528
|
+
* Paginates with ACTIVE_PAGE_SIZE (default 20) items per page.
|
|
2457
2529
|
*/
|
|
2458
|
-
buildActiveCardFromMemory(activeRuns, activeBashRuns) {
|
|
2530
|
+
buildActiveCardFromMemory(activeRuns, activeBashRuns, offset = 0) {
|
|
2531
|
+
const totalCount = activeRuns.length + activeBashRuns.length;
|
|
2532
|
+
const pageSize = ACTIVE_PAGE_SIZE;
|
|
2533
|
+
const safeOffset = Math.max(0, Math.min(offset, Math.max(0, totalCount - 1)));
|
|
2534
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
2535
|
+
const currentPage = Math.floor(safeOffset / pageSize) + 1;
|
|
2536
|
+
// Slice: distribute offset across agent runs first, then bash runs
|
|
2459
2537
|
const elements = [];
|
|
2538
|
+
// Page info
|
|
2539
|
+
if (totalPages > 1) {
|
|
2540
|
+
elements.push({
|
|
2541
|
+
tag: 'div',
|
|
2542
|
+
text: {
|
|
2543
|
+
tag: 'lark_md',
|
|
2544
|
+
content: `**第 ${currentPage}/${totalPages} 页** (共 ${totalCount} 项)`,
|
|
2545
|
+
},
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
let remaining = pageSize;
|
|
2549
|
+
let skipped = safeOffset;
|
|
2460
2550
|
// Agent runs section
|
|
2461
|
-
if (activeRuns.length > 0) {
|
|
2551
|
+
if (activeRuns.length > 0 && skipped < activeRuns.length && remaining > 0) {
|
|
2462
2552
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '## 🤖 Agent 任务' } });
|
|
2463
|
-
|
|
2553
|
+
const start = skipped;
|
|
2554
|
+
const end = Math.min(start + remaining, activeRuns.length);
|
|
2555
|
+
for (let i = start; i < end; i++) {
|
|
2556
|
+
const run = activeRuns[i];
|
|
2464
2557
|
const statusLabel = run.terminal === 'running' ? '运行中' : '处理中';
|
|
2465
2558
|
elements.push({
|
|
2466
2559
|
tag: 'div',
|
|
@@ -2483,11 +2576,19 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2483
2576
|
});
|
|
2484
2577
|
elements.push({ tag: 'hr' });
|
|
2485
2578
|
}
|
|
2579
|
+
remaining -= end - start;
|
|
2580
|
+
skipped = 0;
|
|
2581
|
+
}
|
|
2582
|
+
else {
|
|
2583
|
+
skipped -= activeRuns.length;
|
|
2486
2584
|
}
|
|
2487
2585
|
// Bash runs section
|
|
2488
|
-
if (activeBashRuns.length > 0) {
|
|
2586
|
+
if (activeBashRuns.length > 0 && skipped < activeBashRuns.length && remaining > 0) {
|
|
2489
2587
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '## 💻 Bash 命令' } });
|
|
2490
|
-
|
|
2588
|
+
const start = skipped;
|
|
2589
|
+
const end = Math.min(start + remaining, activeBashRuns.length);
|
|
2590
|
+
for (let i = start; i < end; i++) {
|
|
2591
|
+
const run = activeBashRuns[i];
|
|
2491
2592
|
const statusLabel = run.terminal === 'running' ? '运行中' : '处理中';
|
|
2492
2593
|
elements.push({
|
|
2493
2594
|
tag: 'div',
|
|
@@ -2508,6 +2609,38 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2508
2609
|
elements.push({ tag: 'hr' });
|
|
2509
2610
|
}
|
|
2510
2611
|
}
|
|
2612
|
+
// Pagination buttons
|
|
2613
|
+
if (totalPages > 1) {
|
|
2614
|
+
const paginationButtons = [];
|
|
2615
|
+
if (currentPage > 1) {
|
|
2616
|
+
const prevOffset = (currentPage - 2) * pageSize;
|
|
2617
|
+
paginationButtons.push({
|
|
2618
|
+
tag: 'button',
|
|
2619
|
+
text: { tag: 'plain_text', content: '◀ 上一页' },
|
|
2620
|
+
type: 'default',
|
|
2621
|
+
size: 'small',
|
|
2622
|
+
behaviors: [{ type: 'callback', value: { cmd: 'active.page', offset: prevOffset } }],
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
if (currentPage < totalPages) {
|
|
2626
|
+
const nextOffset = currentPage * pageSize;
|
|
2627
|
+
paginationButtons.push({
|
|
2628
|
+
tag: 'button',
|
|
2629
|
+
text: { tag: 'plain_text', content: '下一页 ▶' },
|
|
2630
|
+
type: 'primary',
|
|
2631
|
+
size: 'small',
|
|
2632
|
+
behaviors: [{ type: 'callback', value: { cmd: 'active.page', offset: nextOffset } }],
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
elements.push({
|
|
2636
|
+
tag: 'column_set',
|
|
2637
|
+
columns: paginationButtons.map((btn) => ({
|
|
2638
|
+
tag: 'column',
|
|
2639
|
+
width: 'auto',
|
|
2640
|
+
elements: [btn],
|
|
2641
|
+
})),
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2511
2644
|
const card = {
|
|
2512
2645
|
schema: '2.0',
|
|
2513
2646
|
config: { wide_screen_mode: true },
|
|
@@ -2528,7 +2661,9 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2528
2661
|
return { text: '重连失败,请检查网络和配置' };
|
|
2529
2662
|
}
|
|
2530
2663
|
}
|
|
2531
|
-
cmdConfig(args, ctx) {
|
|
2664
|
+
async cmdConfig(args, ctx) {
|
|
2665
|
+
// Ensure probe cache is populated before building the card.
|
|
2666
|
+
await probeAllAgents();
|
|
2532
2667
|
// Check if this is a direct set command: /config <key> <value>
|
|
2533
2668
|
// Command-style set: immediate write to disk, clear pendingConfig
|
|
2534
2669
|
if (args.length >= 2) {
|
|
@@ -2574,25 +2709,58 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2574
2709
|
// 使用 pendingConfig(若存在)或当前 config
|
|
2575
2710
|
const displayConfig = this.pendingConfig ?? this.config;
|
|
2576
2711
|
// 定义字段分组:defaultAgent 选择器 + 该 agent 的配置字段 + idle.watchdogMinutes +
|
|
2577
|
-
// output/logging。stopGraceMs
|
|
2712
|
+
// output/logging。stopGraceMs 等实现细节走 YAML,不在卡片暴露。
|
|
2578
2713
|
// 根据 defaultAgent 构建完整的 Agent 配置字段组
|
|
2579
2714
|
// 使用 agent config builder 获取该 agent 的配置字段
|
|
2580
2715
|
const defaultAgent = displayConfig.defaultAgent ?? 'claude';
|
|
2581
2716
|
const configBuilder = getConfigBuilder(defaultAgent);
|
|
2582
|
-
|
|
2583
|
-
//
|
|
2584
|
-
//
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2717
|
+
// Build defaultAgent selector with availability annotations.
|
|
2718
|
+
// Direct inline construction (not via ConfigField) so that text (display label)
|
|
2719
|
+
// and value (agentKind for config storage) can differ — uninstalled agents get
|
|
2720
|
+
// a "⚠️ (未安装)" suffix in the label while the value stays clean.
|
|
2721
|
+
const allAgents = listRegisteredAgents();
|
|
2722
|
+
const agentOptions = allAgents.map((kind) => {
|
|
2723
|
+
const available = getCachedAvailability(kind);
|
|
2724
|
+
const label = available === false ? `${agentDisplayName(kind)} ⚠️ (未安装)` : agentDisplayName(kind);
|
|
2725
|
+
return { text: { tag: 'plain_text', content: label }, value: kind };
|
|
2590
2726
|
});
|
|
2727
|
+
const selectedAgent = displayConfig.defaultAgent ?? 'claude';
|
|
2728
|
+
const agentSelector = {
|
|
2729
|
+
tag: 'column_set',
|
|
2730
|
+
flex_mode: 'none',
|
|
2731
|
+
columns: [
|
|
2732
|
+
{
|
|
2733
|
+
tag: 'column',
|
|
2734
|
+
width: 'weighted',
|
|
2735
|
+
weight: 2,
|
|
2736
|
+
vertical_align: 'center',
|
|
2737
|
+
elements: [{ tag: 'div', text: { tag: 'lark_md', content: '默认 Agent' } }],
|
|
2738
|
+
},
|
|
2739
|
+
{
|
|
2740
|
+
tag: 'column',
|
|
2741
|
+
width: 'weighted',
|
|
2742
|
+
weight: 3,
|
|
2743
|
+
vertical_align: 'center',
|
|
2744
|
+
elements: [
|
|
2745
|
+
{
|
|
2746
|
+
tag: 'select_static',
|
|
2747
|
+
placeholder: { tag: 'plain_text', content: '请选择' },
|
|
2748
|
+
options: agentOptions,
|
|
2749
|
+
initial_option: selectedAgent,
|
|
2750
|
+
behaviors: [{ type: 'callback', value: { cmd: 'config.set', key: 'defaultAgent' } }],
|
|
2751
|
+
},
|
|
2752
|
+
],
|
|
2753
|
+
},
|
|
2754
|
+
],
|
|
2755
|
+
};
|
|
2756
|
+
// Agent config fields — no longer include defaultAgent as a ConfigField
|
|
2757
|
+
// (it's rendered inline above with text/value separation).
|
|
2758
|
+
const agentConfigFieldsNoSelector = configBuilder.buildFields(displayConfig);
|
|
2591
2759
|
const tabs = [
|
|
2592
2760
|
{
|
|
2593
2761
|
id: 'agent',
|
|
2594
2762
|
label: `🤖 ${agentDisplayName(defaultAgent)}`,
|
|
2595
|
-
fields:
|
|
2763
|
+
fields: agentConfigFieldsNoSelector,
|
|
2596
2764
|
},
|
|
2597
2765
|
{
|
|
2598
2766
|
id: 'idle',
|
|
@@ -2623,11 +2791,30 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2623
2791
|
];
|
|
2624
2792
|
// 使用共享渲染模块构建卡片
|
|
2625
2793
|
const card = buildConfigCardFromTabs(tabs, displayConfig);
|
|
2794
|
+
// Inject the agent selector as the first element in the agent tab section,
|
|
2795
|
+
// right after the "**🤖 ..." header, so text/value can differ for uninstalled agents.
|
|
2796
|
+
// NOTE: findIndex relies on the agent tab header starting with "**🤖" — if the
|
|
2797
|
+
// tab label format changes, this fallback will fire and the selector moves to top.
|
|
2798
|
+
const body = card.body;
|
|
2799
|
+
if (body && Array.isArray(body.elements)) {
|
|
2800
|
+
const headerIdx = body.elements.findIndex((el) => typeof el === 'object' &&
|
|
2801
|
+
el !== null &&
|
|
2802
|
+
el.tag === 'div' &&
|
|
2803
|
+
el.text?.tag === 'lark_md' &&
|
|
2804
|
+
(el.text?.content ?? '').startsWith('**🤖'));
|
|
2805
|
+
if (headerIdx >= 0) {
|
|
2806
|
+
body.elements.splice(headerIdx + 1, 0, agentSelector);
|
|
2807
|
+
}
|
|
2808
|
+
else {
|
|
2809
|
+
getLogger().warn('[buildConfigCard] agent tab header not found, falling back to unshift');
|
|
2810
|
+
body.elements.unshift(agentSelector);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2626
2813
|
return { card };
|
|
2627
2814
|
}
|
|
2628
|
-
cmdOrder(args, _ctx) {
|
|
2815
|
+
cmdOrder(args, _ctx, offset = 0) {
|
|
2629
2816
|
const sub = args[0]?.toLowerCase();
|
|
2630
|
-
// /order — list orders (
|
|
2817
|
+
// /order — list orders (CardKit 2.0 with pagination)
|
|
2631
2818
|
if (!sub || sub === 'list') {
|
|
2632
2819
|
this.orderStore.reload();
|
|
2633
2820
|
const allOrders = this.orderStore.get();
|
|
@@ -2636,8 +2823,17 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2636
2823
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '暂无指令' } });
|
|
2637
2824
|
}
|
|
2638
2825
|
else {
|
|
2639
|
-
|
|
2640
|
-
|
|
2826
|
+
// Pagination calculations
|
|
2827
|
+
const totalCount = allOrders.length;
|
|
2828
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / ORDER_PAGE_SIZE));
|
|
2829
|
+
// Clamp offset to last page boundary to avoid empty pages
|
|
2830
|
+
const maxOffset = Math.max(0, (totalPages - 1) * ORDER_PAGE_SIZE);
|
|
2831
|
+
const safeOffset = Math.min(Math.max(offset, 0), maxOffset);
|
|
2832
|
+
const currentPage = Math.floor(safeOffset / ORDER_PAGE_SIZE) + 1;
|
|
2833
|
+
const pageOrders = allOrders.slice(safeOffset, safeOffset + ORDER_PAGE_SIZE);
|
|
2834
|
+
const hasPagination = totalCount > ORDER_PAGE_SIZE;
|
|
2835
|
+
for (let i = 0; i < pageOrders.length; i++) {
|
|
2836
|
+
const order = pageOrders[i];
|
|
2641
2837
|
const displayText = order.text.length > 100 ? order.text.slice(0, 97) + '...' : order.text;
|
|
2642
2838
|
// 文字在上方
|
|
2643
2839
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: displayText } });
|
|
@@ -2670,7 +2866,10 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2670
2866
|
type: 'danger',
|
|
2671
2867
|
size: 'small',
|
|
2672
2868
|
behaviors: [
|
|
2673
|
-
{
|
|
2869
|
+
{
|
|
2870
|
+
type: 'callback',
|
|
2871
|
+
value: { cmd: 'order.delete', orderId: order.id, offset: safeOffset },
|
|
2872
|
+
},
|
|
2674
2873
|
],
|
|
2675
2874
|
},
|
|
2676
2875
|
],
|
|
@@ -2678,10 +2877,81 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2678
2877
|
],
|
|
2679
2878
|
});
|
|
2680
2879
|
// 非最后一项,添加分隔线
|
|
2681
|
-
if (i <
|
|
2880
|
+
if (i < pageOrders.length - 1) {
|
|
2682
2881
|
elements.push({ tag: 'hr' });
|
|
2683
2882
|
}
|
|
2684
2883
|
}
|
|
2884
|
+
// Pagination bar (only shown when there are more items than ORDER_PAGE_SIZE)
|
|
2885
|
+
if (hasPagination) {
|
|
2886
|
+
const hasPrev = safeOffset > 0;
|
|
2887
|
+
const hasNext = safeOffset + ORDER_PAGE_SIZE < totalCount;
|
|
2888
|
+
const pageColumns = [];
|
|
2889
|
+
if (hasPrev) {
|
|
2890
|
+
pageColumns.push({
|
|
2891
|
+
tag: 'column',
|
|
2892
|
+
width: 'auto',
|
|
2893
|
+
vertical_align: 'center',
|
|
2894
|
+
elements: [
|
|
2895
|
+
{
|
|
2896
|
+
tag: 'button',
|
|
2897
|
+
text: { tag: 'plain_text', content: '⬅ 上一页' },
|
|
2898
|
+
type: 'default',
|
|
2899
|
+
size: 'small',
|
|
2900
|
+
behaviors: [
|
|
2901
|
+
{
|
|
2902
|
+
type: 'callback',
|
|
2903
|
+
value: {
|
|
2904
|
+
cmd: 'order.page',
|
|
2905
|
+
offset: safeOffset - ORDER_PAGE_SIZE,
|
|
2906
|
+
},
|
|
2907
|
+
},
|
|
2908
|
+
],
|
|
2909
|
+
},
|
|
2910
|
+
],
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
pageColumns.push({
|
|
2914
|
+
tag: 'column',
|
|
2915
|
+
width: 'weighted',
|
|
2916
|
+
weight: 1,
|
|
2917
|
+
vertical_align: 'center',
|
|
2918
|
+
elements: [
|
|
2919
|
+
{
|
|
2920
|
+
tag: 'div',
|
|
2921
|
+
text: {
|
|
2922
|
+
tag: 'lark_md',
|
|
2923
|
+
content: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 条)`,
|
|
2924
|
+
},
|
|
2925
|
+
},
|
|
2926
|
+
],
|
|
2927
|
+
});
|
|
2928
|
+
if (hasNext) {
|
|
2929
|
+
pageColumns.push({
|
|
2930
|
+
tag: 'column',
|
|
2931
|
+
width: 'auto',
|
|
2932
|
+
vertical_align: 'center',
|
|
2933
|
+
elements: [
|
|
2934
|
+
{
|
|
2935
|
+
tag: 'button',
|
|
2936
|
+
text: { tag: 'plain_text', content: '下一页 ➡' },
|
|
2937
|
+
type: 'default',
|
|
2938
|
+
size: 'small',
|
|
2939
|
+
behaviors: [
|
|
2940
|
+
{
|
|
2941
|
+
type: 'callback',
|
|
2942
|
+
value: {
|
|
2943
|
+
cmd: 'order.page',
|
|
2944
|
+
offset: safeOffset + ORDER_PAGE_SIZE,
|
|
2945
|
+
},
|
|
2946
|
+
},
|
|
2947
|
+
],
|
|
2948
|
+
},
|
|
2949
|
+
],
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
elements.push({ tag: 'hr' });
|
|
2953
|
+
elements.push({ tag: 'column_set', columns: pageColumns });
|
|
2954
|
+
}
|
|
2685
2955
|
}
|
|
2686
2956
|
return {
|
|
2687
2957
|
card: {
|
package/dist/router/utils.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import type { ActiveRunSnapshot } from '../bridge/index.js';
|
|
6
6
|
export interface SessionDisplayUsage {
|
|
7
7
|
contextLength?: number;
|
|
8
|
+
/** 当前模型 context window 上限(仅 codex 提供);用于渲染 "Context - X (Y%)"。 */
|
|
9
|
+
contextLimit?: number;
|
|
8
10
|
compactCount?: number;
|
|
9
11
|
cacheReadTokens?: number;
|
|
10
12
|
cacheCreationTokens?: number;
|
|
@@ -18,6 +20,7 @@ export declare function activeRunUsage(activeRun: ActiveRunSnapshot | undefined)
|
|
|
18
20
|
*/
|
|
19
21
|
export declare function formatUsageStats(usage?: {
|
|
20
22
|
contextLength?: number;
|
|
23
|
+
contextLimit?: number;
|
|
21
24
|
compactCount?: number;
|
|
22
25
|
cacheReadTokens?: number;
|
|
23
26
|
cacheCreationTokens?: number;
|
package/dist/router/utils.js
CHANGED
|
@@ -14,10 +14,18 @@ export function activeRunUsage(activeRun) {
|
|
|
14
14
|
}
|
|
15
15
|
// --- Extracted helpers for refactoring ---
|
|
16
16
|
/**
|
|
17
|
-
* Format number to
|
|
18
|
-
* (e.g.,
|
|
17
|
+
* Format number to human-readable unit string.
|
|
18
|
+
* - >= 1M: one decimal, M suffix (e.g., 1200000 → 1.2M, 25000000 → 25M)
|
|
19
|
+
* - >= 1K: rounded to nearest integer K (e.g., 1500 → 2K, 120000 → 120K)
|
|
20
|
+
* - < 1K: as-is (e.g., 500 → "500")
|
|
19
21
|
*/
|
|
20
22
|
function formatTokenK(n) {
|
|
23
|
+
if (n >= 1_000_000) {
|
|
24
|
+
const m = n / 1_000_000;
|
|
25
|
+
const rounded = Math.round(m * 10) / 10;
|
|
26
|
+
// Drop unnecessary ".0" for whole numbers (2.0M → 2M)
|
|
27
|
+
return rounded === Math.floor(rounded) ? `${Math.floor(rounded)}M` : `${rounded}M`;
|
|
28
|
+
}
|
|
21
29
|
if (n >= 1000) {
|
|
22
30
|
const k = Math.round(n / 1000);
|
|
23
31
|
return `${k}K`;
|
|
@@ -40,7 +48,11 @@ export function formatUsageStats(usage, options) {
|
|
|
40
48
|
}
|
|
41
49
|
// 2. Context 长度
|
|
42
50
|
if (usage?.contextLength !== undefined) {
|
|
43
|
-
|
|
51
|
+
const ctxLimit = usage.contextLimit;
|
|
52
|
+
const ctxPercent = ctxLimit !== undefined && ctxLimit > 0
|
|
53
|
+
? ` (${Math.round((usage.contextLength / ctxLimit) * 100)}%)`
|
|
54
|
+
: '';
|
|
55
|
+
lines.push(`Context - ${formatTokenK(usage.contextLength)}${ctxPercent}`);
|
|
44
56
|
}
|
|
45
57
|
// 3. Compact 次数
|
|
46
58
|
if (usage?.compactCount) {
|
|
@@ -19,7 +19,6 @@ export class ClaudeRunner extends SpawningRunner {
|
|
|
19
19
|
defaultSettings;
|
|
20
20
|
constructor(opts) {
|
|
21
21
|
super({
|
|
22
|
-
binary: opts.binary ?? 'claude',
|
|
23
22
|
pidDir: opts.pidDir,
|
|
24
23
|
workspace: opts.workspace,
|
|
25
24
|
stopGraceMs: opts.stopGraceMs,
|
|
@@ -27,6 +26,7 @@ export class ClaudeRunner extends SpawningRunner {
|
|
|
27
26
|
pidFilePrefix: 'claude',
|
|
28
27
|
logTag: 'claude-runner',
|
|
29
28
|
});
|
|
29
|
+
this.binary = 'claude';
|
|
30
30
|
this.defaultModel = opts.model ?? 'claude-opus-4-8';
|
|
31
31
|
this.defaultEffort = opts.effort ?? 'medium';
|
|
32
32
|
this.defaultSettings = opts.settings;
|
|
@@ -26,8 +26,6 @@ import type { AgentKind, AgentSessionReader, AgentEvent, AgentRunner, AgentStatu
|
|
|
26
26
|
import type { SpawnOptions } from '../types.js';
|
|
27
27
|
import { SpawningRunner, type RunnerTranslator } from '../common/spawning-runner.js';
|
|
28
28
|
interface CodexExecRunnerOptions {
|
|
29
|
-
/** Path to codex binary. Default: 'codex' (in PATH). */
|
|
30
|
-
binary?: string;
|
|
31
29
|
/** Model override. Omitted → codex reads from its config.toml. */
|
|
32
30
|
model?: string;
|
|
33
31
|
/** Model provider override. Omitted → codex reads from its config.toml. */
|