lark-remote 0.1.2 → 0.1.4
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 +65 -0
- package/README.en.md +2 -5
- package/README.md +1 -4
- package/dist/bridge/index.d.ts +5 -1
- package/dist/bridge/index.js +12 -2
- package/dist/card/card-budget.js +1 -2
- 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 +74 -21
- package/dist/router/config/codex.js +8 -12
- package/dist/router/config/index.d.ts +10 -0
- package/dist/router/config/index.js +17 -0
- package/dist/router/index.d.ts +40 -1
- package/dist/router/index.js +514 -48
- 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 +1 -1
- package/dist/session/claude/sessions.js +77 -31
- package/dist/session/codex/rollout-reader.js +9 -0
- package/dist/session/kimi/sessions.d.ts +3 -1
- package/dist/session/kimi/sessions.js +9 -3
- 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/dist/workspace/index.d.ts +19 -1
- package/dist/workspace/index.js +51 -5
- package/package.json +39 -4
package/dist/router/index.js
CHANGED
|
@@ -12,7 +12,8 @@ import { markdownDiv, sessionEventPanel } from './card-helpers.js';
|
|
|
12
12
|
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
|
-
import { getConfigBuilder, listRegisteredAgents } from './config/index.js';
|
|
15
|
+
import { getConfigBuilder, listRegisteredAgents, sortAgentsByAvailability, } 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,21 @@ 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
|
+
/**
|
|
27
|
+
* /order 列表页大小;指令超过此数量时显示分页导航栏。
|
|
28
|
+
* 2026-08-13 实测:每行 2 个元素(div + column_set,行间 hr),20 行 + 分页栏
|
|
29
|
+
* = 61 个 body 元素,触发飞书 ErrCode 11310 "element exceeds the limit"
|
|
30
|
+
* (21+ 条指令时第 1 页必炸)。改为 15 行:3*15 - 1 + 2 = 46 个元素,留足余量。
|
|
31
|
+
*/
|
|
32
|
+
const ORDER_PAGE_SIZE = 15;
|
|
33
|
+
/**
|
|
34
|
+
* /ws 列表页大小。从 15 降为 5:头部 2 + 5 行 × 3(div + column_set + hr)+
|
|
35
|
+
* 底栏排序 1 + 分页栏 2 ≈ 20 个 body 元素,远低于单卡 60 元素上限(ErrCode 11310),
|
|
36
|
+
* 留足余量。
|
|
37
|
+
*/
|
|
38
|
+
const WS_PAGE_SIZE = 5;
|
|
23
39
|
/** /resume 列表页内全量预取(readSessionContent)的行数上限;其余行用轻量 summary 兜底。 */
|
|
24
40
|
const RESUME_CONTENT_PREFETCH = 5;
|
|
25
41
|
/**
|
|
@@ -34,6 +50,7 @@ import { newSessionButton, agentDisplayName } from '../card/card-shared.js';
|
|
|
34
50
|
* These bypass the serial queue and execute immediately.
|
|
35
51
|
*
|
|
36
52
|
* ws.use — /ws list card "使用 <name>"
|
|
53
|
+
* ws.page — /ws list card "上一页/下一页"
|
|
37
54
|
* queue.immediate — queue card "立即执行" button (§9.6)
|
|
38
55
|
* queue.cancel — queue card "撤销" button
|
|
39
56
|
* queue.diagnose — queue card "诊断" button
|
|
@@ -42,6 +59,7 @@ import { newSessionButton, agentDisplayName } from '../card/card-shared.js';
|
|
|
42
59
|
* new-session — clear sessionId only
|
|
43
60
|
* stop — interrupt current run
|
|
44
61
|
* ls.file — send file only
|
|
62
|
+
* ws.page — paginate /ws list only
|
|
45
63
|
* ws.remove — delete workspace alias only
|
|
46
64
|
* resume.use — set sessionId + agent for correct reader routing
|
|
47
65
|
* help.* — read-only help commands
|
|
@@ -61,7 +79,10 @@ export function isImmediateAction(cmd) {
|
|
|
61
79
|
cmd === 'ls.switch' ||
|
|
62
80
|
cmd === 'ls.page' ||
|
|
63
81
|
cmd === 'resume.page' || // control operation: paginate only, never spawns claude
|
|
82
|
+
cmd === 'active.page' || // control operation: paginate active card
|
|
83
|
+
cmd === 'ws.page' || // control operation: paginate /ws list only
|
|
64
84
|
cmd === 'ws.remove' ||
|
|
85
|
+
cmd === 'ws.sort' || // control operation: toggle sort mode only
|
|
65
86
|
cmd === 'resume.use' ||
|
|
66
87
|
cmd === 'ws.use' ||
|
|
67
88
|
cmd === 'queue.immediate' ||
|
|
@@ -70,12 +91,15 @@ export function isImmediateAction(cmd) {
|
|
|
70
91
|
cmd === 'queue.edit' ||
|
|
71
92
|
cmd === 'queue.input' ||
|
|
72
93
|
cmd === 'order.delete' ||
|
|
94
|
+
cmd === 'order.page' ||
|
|
73
95
|
cmd === 'config.toggle' ||
|
|
74
96
|
cmd === 'config.set' ||
|
|
75
97
|
cmd === 'config.input' ||
|
|
76
98
|
cmd === 'config.save');
|
|
77
99
|
}
|
|
78
100
|
export class CommandRouter {
|
|
101
|
+
/** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
|
|
102
|
+
static VALID_AGENTS = ['claude', 'codex', 'opencode', 'pi', 'kimi'];
|
|
79
103
|
sessionStore;
|
|
80
104
|
bridge;
|
|
81
105
|
config;
|
|
@@ -99,6 +123,11 @@ export class CommandRouter {
|
|
|
99
123
|
* millisecond); the counter guarantees uniqueness regardless of timing.
|
|
100
124
|
*/
|
|
101
125
|
orderExecKeyCounter = 0;
|
|
126
|
+
/**
|
|
127
|
+
* Per-user sort preference for /ws list (memory-only, not persisted).
|
|
128
|
+
* Default 'recent' (most recently used first), can toggle to 'alpha'.
|
|
129
|
+
*/
|
|
130
|
+
wsSortPreference = new Map();
|
|
102
131
|
/**
|
|
103
132
|
* Session reader registry for multi-agent path. Resolves the right
|
|
104
133
|
* reader by `config.defaultAgent` (and per-agent override params).
|
|
@@ -195,15 +224,18 @@ export class CommandRouter {
|
|
|
195
224
|
case 'ls.page':
|
|
196
225
|
return this.handleLsPage(value, ctx);
|
|
197
226
|
case 'ws.use':
|
|
198
|
-
await this.
|
|
199
|
-
|
|
227
|
+
return await this.handleWsUse(value, ctx);
|
|
228
|
+
case 'ws.sort':
|
|
229
|
+
return await this.handleWsSort(value, ctx);
|
|
230
|
+
case 'ws.page':
|
|
231
|
+
return await this.handleWsPage(value, ctx);
|
|
200
232
|
case 'ws.remove':
|
|
201
233
|
return await this.handleWsRemove(value, ctx);
|
|
202
234
|
case 'resume.use': {
|
|
203
|
-
// resume.use
|
|
235
|
+
// resume.use carries an agent field from /resume [agent] list cards
|
|
236
|
+
// (both active and completed cards include the agent field).
|
|
204
237
|
const resumeAgent = value.agent ?? this.config.defaultAgent;
|
|
205
|
-
|
|
206
|
-
const validAgents = ['claude', 'codex', 'opencode', 'pi', 'kimi'];
|
|
238
|
+
const validAgents = CommandRouter.VALID_AGENTS;
|
|
207
239
|
const resolvedAgent = validAgents.includes(resumeAgent)
|
|
208
240
|
? resumeAgent
|
|
209
241
|
: this.config.defaultAgent;
|
|
@@ -226,6 +258,8 @@ export class CommandRouter {
|
|
|
226
258
|
}
|
|
227
259
|
case 'resume.page':
|
|
228
260
|
return this.handleResumePage(value, ctx);
|
|
261
|
+
case 'active.page':
|
|
262
|
+
return this.handleActivePage(value, ctx);
|
|
229
263
|
case 'new-session':
|
|
230
264
|
this.sessionStore.clearSessionId(ctx.userId, this.config.defaultAgent, {
|
|
231
265
|
clearSessionCwd: true,
|
|
@@ -260,6 +294,8 @@ export class CommandRouter {
|
|
|
260
294
|
return await this.handleQueueInput(value, ctx);
|
|
261
295
|
case 'order.delete':
|
|
262
296
|
return await this.handleOrderDelete(value, ctx);
|
|
297
|
+
case 'order.page':
|
|
298
|
+
return await this.handleOrderPage(value, ctx);
|
|
263
299
|
case 'config.toggle':
|
|
264
300
|
case 'config.set':
|
|
265
301
|
case 'config.input':
|
|
@@ -633,9 +669,11 @@ export class CommandRouter {
|
|
|
633
669
|
const inputTask = this.bridge.getQueuedTask(workspace, messageId);
|
|
634
670
|
if (!inputTask) {
|
|
635
671
|
// Task already left the queue (began or was cancelled) before we could
|
|
636
|
-
// register the replacement.
|
|
637
|
-
|
|
638
|
-
|
|
672
|
+
// register the replacement. Return a toast so the edit-form card gets
|
|
673
|
+
// dismissed — sendResult alone leaves the card stuck in the edit state
|
|
674
|
+
// (Feishu keeps the pre-click card when the callback response has no
|
|
675
|
+
// toast/card field).
|
|
676
|
+
return { toast: { type: 'info', content: '任务已不在队列中(可能已开始执行或被撤销)' } };
|
|
639
677
|
}
|
|
640
678
|
this.bridge.setTaskReplacement(workspace, messageId, async () => {
|
|
641
679
|
await this.handle(newMessage, ctx, { cwdOverride: workspace, binding: inputTask.binding });
|
|
@@ -767,8 +805,7 @@ export class CommandRouter {
|
|
|
767
805
|
*/
|
|
768
806
|
async handleResumePage(value, ctx) {
|
|
769
807
|
const resumeAgent = value.agent ?? this.config.defaultAgent;
|
|
770
|
-
|
|
771
|
-
const validAgents = ['claude', 'codex', 'opencode', 'pi', 'kimi'];
|
|
808
|
+
const validAgents = CommandRouter.VALID_AGENTS;
|
|
772
809
|
const resolvedAgent = validAgents.includes(resumeAgent)
|
|
773
810
|
? resumeAgent
|
|
774
811
|
: this.config.defaultAgent;
|
|
@@ -804,6 +841,21 @@ export class CommandRouter {
|
|
|
804
841
|
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
805
842
|
return { toast: { type: 'success', content: '' } };
|
|
806
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Handle active.page: paginate the /active run list in place.
|
|
846
|
+
* Mirrors handleResumePage — updates the same card, never sends a new one.
|
|
847
|
+
*/
|
|
848
|
+
async handleActivePage(value, ctx) {
|
|
849
|
+
const rawOffset = Number(value.offset);
|
|
850
|
+
const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.trunc(rawOffset)) : 0;
|
|
851
|
+
const alignedOffset = Math.floor(offset / ACTIVE_PAGE_SIZE) * ACTIVE_PAGE_SIZE;
|
|
852
|
+
const result = this.cmdActive([], ctx, alignedOffset);
|
|
853
|
+
if (!result.card) {
|
|
854
|
+
return { toast: { type: 'error', content: '当前没有正在进行中的任务' } };
|
|
855
|
+
}
|
|
856
|
+
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
857
|
+
return { toast: { type: 'success', content: '' } };
|
|
858
|
+
}
|
|
807
859
|
/**
|
|
808
860
|
* Handle ls.browse: browse directory without switching cwd.
|
|
809
861
|
*/
|
|
@@ -886,11 +938,56 @@ export class CommandRouter {
|
|
|
886
938
|
}
|
|
887
939
|
// Delete the order
|
|
888
940
|
this.orderStore.remove(orderId);
|
|
889
|
-
// Refresh the order list and update card in place
|
|
890
|
-
const
|
|
941
|
+
// Refresh the order list and update card in place, preserving current page
|
|
942
|
+
const currentOffset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
943
|
+
const result = this.cmdOrder([], ctx, currentOffset);
|
|
891
944
|
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
892
945
|
return { toast: { type: 'success', content: '已删除指令' } };
|
|
893
946
|
}
|
|
947
|
+
/**
|
|
948
|
+
* Handle order.page: paginate the /order list in place.
|
|
949
|
+
* Mirrors handleLsPage — updates the same card, never sends a new one.
|
|
950
|
+
*/
|
|
951
|
+
async handleOrderPage(value, ctx) {
|
|
952
|
+
const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
953
|
+
// cmdOrder internally clamps stale/out-of-range offsets
|
|
954
|
+
const result = this.cmdOrder([], ctx, offset);
|
|
955
|
+
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
956
|
+
return { toast: { type: 'success', content: '' } };
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Handle ws.use card action: switch to the workspace, then refresh the /ws
|
|
960
|
+
* list card in place so "recent" sort order is immediately visible.
|
|
961
|
+
*
|
|
962
|
+
* Unlike the command-line `/ws use` (which returns text), the card action
|
|
963
|
+
* uses toast + inplace refresh for a cleaner UX (matching ws.remove/ws.sort).
|
|
964
|
+
* If auto-resume produces a card, it is sent as a separate reply.
|
|
965
|
+
*/
|
|
966
|
+
async handleWsUse(value, ctx) {
|
|
967
|
+
const name = value.name ?? '';
|
|
968
|
+
const useResult = this.cmdWs(['use', name], ctx);
|
|
969
|
+
// Error cases (workspace not found, path invalid, etc.): relay as error toast
|
|
970
|
+
if (useResult.text && !useResult.text.includes('已切换')) {
|
|
971
|
+
return { toast: { type: 'error', content: useResult.text } };
|
|
972
|
+
}
|
|
973
|
+
// Success: if auto-resume produced a card, send it as a reply
|
|
974
|
+
if (useResult.card) {
|
|
975
|
+
await this.bridge.sendResult(useResult, ctx);
|
|
976
|
+
}
|
|
977
|
+
// Refresh the /ws list card in place so "recent" sort is immediately visible
|
|
978
|
+
const currentOffset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
979
|
+
const refreshed = this.cmdWs([], ctx, currentOffset);
|
|
980
|
+
if (refreshed.card) {
|
|
981
|
+
await this.bridge.updateCardInPlace(refreshed.card, ctx);
|
|
982
|
+
}
|
|
983
|
+
// Toast feedback (suppress text when auto-resume card was already sent)
|
|
984
|
+
return {
|
|
985
|
+
toast: {
|
|
986
|
+
type: 'success',
|
|
987
|
+
content: useResult.card ? '' : (useResult.text ?? `已切换到 workspace "${name}"`),
|
|
988
|
+
},
|
|
989
|
+
};
|
|
990
|
+
}
|
|
894
991
|
/**
|
|
895
992
|
* Handle ws.remove: remove the workspace alias and refresh the /ws list card
|
|
896
993
|
* in place (mirrors handleOrderDelete). Without this the stale alias would
|
|
@@ -904,11 +1001,40 @@ export class CommandRouter {
|
|
|
904
1001
|
// Execute the removal (consumes the write-side effect; the returned text
|
|
905
1002
|
// is discarded — feedback flows through the toast + refreshed card).
|
|
906
1003
|
this.cmdWs(['remove', name], ctx);
|
|
907
|
-
// Rebuild the /ws list card and update it in place
|
|
908
|
-
|
|
1004
|
+
// Rebuild the /ws list card and update it in place, preserving the page
|
|
1005
|
+
// the user was on when they clicked 删除.
|
|
1006
|
+
const currentOffset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
1007
|
+
const refreshed = this.cmdWs([], ctx, currentOffset);
|
|
909
1008
|
await this.bridge.updateCardInPlace(refreshed.card, ctx);
|
|
910
1009
|
return { toast: { type: 'success', content: `已删除 workspace "${name}"` } };
|
|
911
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Handle ws.page: paginate the /ws list in place.
|
|
1013
|
+
* Mirrors handleOrderPage — updates the same card, never sends a new one.
|
|
1014
|
+
*/
|
|
1015
|
+
async handleWsPage(value, ctx) {
|
|
1016
|
+
const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
1017
|
+
// cmdWs internally clamps stale/out-of-range offsets
|
|
1018
|
+
const result = this.cmdWs([], ctx, offset);
|
|
1019
|
+
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
1020
|
+
return { toast: { type: 'success', content: '' } };
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Handle ws.sort: toggle sort preference and refresh the /ws list card.
|
|
1024
|
+
* Sort preference is memory-only (not persisted); restart resets to 'recent'.
|
|
1025
|
+
*/
|
|
1026
|
+
async handleWsSort(value, ctx) {
|
|
1027
|
+
const userId = ctx.userId;
|
|
1028
|
+
const current = this.wsSortPreference.get(userId) ?? 'recent';
|
|
1029
|
+
const next = current === 'recent' ? 'alpha' : 'recent';
|
|
1030
|
+
this.wsSortPreference.set(userId, next);
|
|
1031
|
+
const result = this.cmdWs([], ctx, 0);
|
|
1032
|
+
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
1033
|
+
const nextLabel = next === 'recent' ? '🕐 最近使用' : '🔤 字母顺序';
|
|
1034
|
+
return {
|
|
1035
|
+
toast: { type: 'success', content: `已切换为 ${nextLabel}` },
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
912
1038
|
/**
|
|
913
1039
|
* `ls.file` card button: send file to Feishu, rejecting if > 30MB.
|
|
914
1040
|
*/
|
|
@@ -1323,7 +1449,7 @@ export class CommandRouter {
|
|
|
1323
1449
|
return await this.cmdReconnect();
|
|
1324
1450
|
case 'config':
|
|
1325
1451
|
case 'c':
|
|
1326
|
-
return this.cmdConfig(args, ctx);
|
|
1452
|
+
return await this.cmdConfig(args, ctx);
|
|
1327
1453
|
case 'order':
|
|
1328
1454
|
case 'o':
|
|
1329
1455
|
return this.cmdOrder(args, ctx);
|
|
@@ -1989,7 +2115,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1989
2115
|
return { text: `读取目录失败: ${err.message}` };
|
|
1990
2116
|
}
|
|
1991
2117
|
}
|
|
1992
|
-
cmdWs(args, ctx) {
|
|
2118
|
+
cmdWs(args, ctx, offset = 0) {
|
|
1993
2119
|
const sub = args[0]?.toLowerCase();
|
|
1994
2120
|
switch (sub) {
|
|
1995
2121
|
case 'save': {
|
|
@@ -2022,6 +2148,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2022
2148
|
catch {
|
|
2023
2149
|
return { text: `路径无效: ${wsPath}` };
|
|
2024
2150
|
}
|
|
2151
|
+
// Record usage timestamp AFTER successful path validation
|
|
2152
|
+
this.workspaceStore.touch(name);
|
|
2025
2153
|
// Switch cwd with auto-resume and user feedback (shared with /cd and ls.switch)
|
|
2026
2154
|
return this.switchCwdAndNotify(ctx.userId, canonical, ctx);
|
|
2027
2155
|
}
|
|
@@ -2038,6 +2166,29 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2038
2166
|
default: {
|
|
2039
2167
|
const entries = this.workspaceStore.list();
|
|
2040
2168
|
const currentCwd = this.sessionStore.getCwd(ctx.userId);
|
|
2169
|
+
const sortMode = this.wsSortPreference.get(ctx.userId) ?? 'recent';
|
|
2170
|
+
// Sort entries by the current preference (view concern, not stored)
|
|
2171
|
+
if (sortMode === 'recent') {
|
|
2172
|
+
// Most recently used first; same lastUsedAt → alphabetical by name (deterministic)
|
|
2173
|
+
entries.sort((a, b) => {
|
|
2174
|
+
if (b.lastUsedAt !== a.lastUsedAt)
|
|
2175
|
+
return b.lastUsedAt - a.lastUsedAt;
|
|
2176
|
+
return a.name.localeCompare(b.name);
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
else {
|
|
2180
|
+
// Alphabetical by alias name
|
|
2181
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2182
|
+
}
|
|
2183
|
+
// Pagination calculations (mirror cmdOrder): clamp stale/out-of-range
|
|
2184
|
+
// offsets so prev/next always step by WS_PAGE_SIZE.
|
|
2185
|
+
const totalCount = entries.length;
|
|
2186
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / WS_PAGE_SIZE));
|
|
2187
|
+
const maxOffset = Math.max(0, (totalPages - 1) * WS_PAGE_SIZE);
|
|
2188
|
+
const safeOffset = Math.min(Math.max(offset, 0), maxOffset);
|
|
2189
|
+
const currentPage = Math.floor(safeOffset / WS_PAGE_SIZE) + 1;
|
|
2190
|
+
const pageEntries = entries.slice(safeOffset, safeOffset + WS_PAGE_SIZE);
|
|
2191
|
+
const hasPagination = totalCount > WS_PAGE_SIZE;
|
|
2041
2192
|
// Build body elements: current cwd + workspace list with dividers
|
|
2042
2193
|
const bodyElements = [];
|
|
2043
2194
|
// Current working directory section
|
|
@@ -2045,18 +2196,56 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2045
2196
|
tag: 'div',
|
|
2046
2197
|
text: { tag: 'lark_md', content: `📂 当前工作目录:\`${currentCwd ?? '(未设置)'}\`` },
|
|
2047
2198
|
});
|
|
2048
|
-
bodyElements.push({ tag: 'hr' });
|
|
2049
2199
|
if (entries.length === 0) {
|
|
2200
|
+
bodyElements.push({ tag: 'hr' });
|
|
2050
2201
|
bodyElements.push({
|
|
2051
2202
|
tag: 'div',
|
|
2052
2203
|
text: { tag: 'lark_md', content: '没有保存的 workspace' },
|
|
2053
2204
|
});
|
|
2054
2205
|
}
|
|
2055
2206
|
else {
|
|
2056
|
-
|
|
2207
|
+
// Sort mode indicator + toggle button: placed above the list so the user
|
|
2208
|
+
// sees "current mode" and "switch to X" before scanning entries.
|
|
2209
|
+
// On narrow (mobile) screens, the label and button must convey both
|
|
2210
|
+
// what-is-active and what-clicking-does to avoid ambiguity.
|
|
2211
|
+
const currentLabel = sortMode === 'recent' ? '🕐 最近使用' : '🔤 字母顺序';
|
|
2212
|
+
const switchLabel = sortMode === 'recent' ? '🔤 字母顺序' : '🕐 最近使用';
|
|
2213
|
+
bodyElements.push({ tag: 'hr' });
|
|
2214
|
+
bodyElements.push({
|
|
2215
|
+
tag: 'column_set',
|
|
2216
|
+
columns: [
|
|
2217
|
+
{
|
|
2218
|
+
tag: 'column',
|
|
2219
|
+
width: 'weighted',
|
|
2220
|
+
weight: 1,
|
|
2221
|
+
vertical_align: 'center',
|
|
2222
|
+
elements: [
|
|
2223
|
+
{
|
|
2224
|
+
tag: 'div',
|
|
2225
|
+
text: { tag: 'lark_md', content: `排序:${currentLabel}` },
|
|
2226
|
+
},
|
|
2227
|
+
],
|
|
2228
|
+
},
|
|
2229
|
+
{
|
|
2230
|
+
tag: 'column',
|
|
2231
|
+
width: 'auto',
|
|
2232
|
+
elements: [
|
|
2233
|
+
{
|
|
2234
|
+
tag: 'button',
|
|
2235
|
+
text: { tag: 'plain_text', content: `切换为 ${switchLabel}` },
|
|
2236
|
+
type: 'default',
|
|
2237
|
+
size: 'small',
|
|
2238
|
+
behaviors: [{ type: 'callback', value: { cmd: 'ws.sort' } }],
|
|
2239
|
+
},
|
|
2240
|
+
],
|
|
2241
|
+
},
|
|
2242
|
+
],
|
|
2243
|
+
});
|
|
2244
|
+
bodyElements.push({ tag: 'hr' });
|
|
2245
|
+
for (const entry of pageEntries) {
|
|
2057
2246
|
bodyElements.push({
|
|
2058
2247
|
tag: 'div',
|
|
2059
|
-
text: { tag: 'lark_md', content: `**${name}** → \`${
|
|
2248
|
+
text: { tag: 'lark_md', content: `**${entry.name}** → \`${entry.path}\`` },
|
|
2060
2249
|
});
|
|
2061
2250
|
// Use column_set+column for 2.0 (action tag not supported in 2.0)
|
|
2062
2251
|
bodyElements.push({
|
|
@@ -2071,7 +2260,12 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2071
2260
|
text: { tag: 'plain_text', content: '切换' },
|
|
2072
2261
|
type: 'primary',
|
|
2073
2262
|
size: 'small',
|
|
2074
|
-
behaviors: [
|
|
2263
|
+
behaviors: [
|
|
2264
|
+
{
|
|
2265
|
+
type: 'callback',
|
|
2266
|
+
value: { cmd: 'ws.use', name: entry.name, offset: safeOffset },
|
|
2267
|
+
},
|
|
2268
|
+
],
|
|
2075
2269
|
},
|
|
2076
2270
|
],
|
|
2077
2271
|
},
|
|
@@ -2084,15 +2278,86 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2084
2278
|
text: { tag: 'plain_text', content: '删除' },
|
|
2085
2279
|
type: 'danger',
|
|
2086
2280
|
size: 'small',
|
|
2087
|
-
behaviors: [
|
|
2281
|
+
behaviors: [
|
|
2282
|
+
{
|
|
2283
|
+
type: 'callback',
|
|
2284
|
+
value: { cmd: 'ws.remove', name: entry.name, offset: safeOffset },
|
|
2285
|
+
},
|
|
2286
|
+
],
|
|
2088
2287
|
},
|
|
2089
2288
|
],
|
|
2090
2289
|
},
|
|
2091
2290
|
],
|
|
2092
2291
|
});
|
|
2093
2292
|
bodyElements.push({ tag: 'hr' });
|
|
2094
|
-
}
|
|
2293
|
+
}
|
|
2095
2294
|
bodyElements.pop();
|
|
2295
|
+
// Pagination bar — separate row below the list (not crowded with sort
|
|
2296
|
+
// toggle; mobile-friendly vertical layout).
|
|
2297
|
+
if (hasPagination) {
|
|
2298
|
+
const hasPrev = safeOffset > 0;
|
|
2299
|
+
const hasNext = safeOffset + WS_PAGE_SIZE < totalCount;
|
|
2300
|
+
const pageColumns = [];
|
|
2301
|
+
if (hasPrev) {
|
|
2302
|
+
pageColumns.push({
|
|
2303
|
+
tag: 'column',
|
|
2304
|
+
width: 'auto',
|
|
2305
|
+
vertical_align: 'center',
|
|
2306
|
+
elements: [
|
|
2307
|
+
{
|
|
2308
|
+
tag: 'button',
|
|
2309
|
+
text: { tag: 'plain_text', content: '⬅' },
|
|
2310
|
+
type: 'default',
|
|
2311
|
+
size: 'small',
|
|
2312
|
+
behaviors: [
|
|
2313
|
+
{
|
|
2314
|
+
type: 'callback',
|
|
2315
|
+
value: { cmd: 'ws.page', offset: safeOffset - WS_PAGE_SIZE },
|
|
2316
|
+
},
|
|
2317
|
+
],
|
|
2318
|
+
},
|
|
2319
|
+
],
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
pageColumns.push({
|
|
2323
|
+
tag: 'column',
|
|
2324
|
+
width: 'weighted',
|
|
2325
|
+
weight: 1,
|
|
2326
|
+
vertical_align: 'center',
|
|
2327
|
+
elements: [
|
|
2328
|
+
{
|
|
2329
|
+
tag: 'div',
|
|
2330
|
+
text: {
|
|
2331
|
+
tag: 'lark_md',
|
|
2332
|
+
content: `**${currentPage}/${totalPages}**(${totalCount})`,
|
|
2333
|
+
},
|
|
2334
|
+
},
|
|
2335
|
+
],
|
|
2336
|
+
});
|
|
2337
|
+
if (hasNext) {
|
|
2338
|
+
pageColumns.push({
|
|
2339
|
+
tag: 'column',
|
|
2340
|
+
width: 'auto',
|
|
2341
|
+
vertical_align: 'center',
|
|
2342
|
+
elements: [
|
|
2343
|
+
{
|
|
2344
|
+
tag: 'button',
|
|
2345
|
+
text: { tag: 'plain_text', content: '➡' },
|
|
2346
|
+
type: 'default',
|
|
2347
|
+
size: 'small',
|
|
2348
|
+
behaviors: [
|
|
2349
|
+
{
|
|
2350
|
+
type: 'callback',
|
|
2351
|
+
value: { cmd: 'ws.page', offset: safeOffset + WS_PAGE_SIZE },
|
|
2352
|
+
},
|
|
2353
|
+
],
|
|
2354
|
+
},
|
|
2355
|
+
],
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
bodyElements.push({ tag: 'hr' });
|
|
2359
|
+
bodyElements.push({ tag: 'column_set', columns: pageColumns });
|
|
2360
|
+
}
|
|
2096
2361
|
}
|
|
2097
2362
|
const card = {
|
|
2098
2363
|
schema: '2.0',
|
|
@@ -2115,7 +2380,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2115
2380
|
const entry = this.sessionStore.get(ctx.userId);
|
|
2116
2381
|
const cwd = entry?.cwd;
|
|
2117
2382
|
// Valid agent kinds for the /resume [agent] [N] feature
|
|
2118
|
-
const VALID_AGENTS =
|
|
2383
|
+
const VALID_AGENTS = CommandRouter.VALID_AGENTS;
|
|
2119
2384
|
// Parse arguments: /resume [agent] [N] or /resume [sessionId] or /resume [agent] [sessionId]
|
|
2120
2385
|
let agentKind = this.config.defaultAgent;
|
|
2121
2386
|
let limit;
|
|
@@ -2470,7 +2735,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2470
2735
|
};
|
|
2471
2736
|
return { card };
|
|
2472
2737
|
}
|
|
2473
|
-
cmdActive(_args, _ctx) {
|
|
2738
|
+
cmdActive(_args, _ctx, offset = 0) {
|
|
2474
2739
|
// List all active runs from bridge memory (not from file system scan)
|
|
2475
2740
|
// New semantics (2026-07-20): only shows runs started by THIS bridge process
|
|
2476
2741
|
const activeRuns = this.bridge.getActiveRuns();
|
|
@@ -2479,18 +2744,40 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2479
2744
|
return { text: '当前没有正在进行中的任务' };
|
|
2480
2745
|
}
|
|
2481
2746
|
// Build CardKit 2.0 card showing memory-based active runs
|
|
2482
|
-
return this.buildActiveCardFromMemory(activeRuns, activeBashRuns);
|
|
2747
|
+
return this.buildActiveCardFromMemory(activeRuns, activeBashRuns, offset);
|
|
2483
2748
|
}
|
|
2484
2749
|
/**
|
|
2485
2750
|
* Build CardKit 2.0 /active card from bridge memory (not file system).
|
|
2486
2751
|
* Shows only runs that were started by THIS bridge process.
|
|
2752
|
+
* Paginates with ACTIVE_PAGE_SIZE (default 20) items per page.
|
|
2487
2753
|
*/
|
|
2488
|
-
buildActiveCardFromMemory(activeRuns, activeBashRuns) {
|
|
2754
|
+
buildActiveCardFromMemory(activeRuns, activeBashRuns, offset = 0) {
|
|
2755
|
+
const totalCount = activeRuns.length + activeBashRuns.length;
|
|
2756
|
+
const pageSize = ACTIVE_PAGE_SIZE;
|
|
2757
|
+
const safeOffset = Math.max(0, Math.min(offset, Math.max(0, totalCount - 1)));
|
|
2758
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
2759
|
+
const currentPage = Math.floor(safeOffset / pageSize) + 1;
|
|
2760
|
+
// Slice: distribute offset across agent runs first, then bash runs
|
|
2489
2761
|
const elements = [];
|
|
2762
|
+
// Page info
|
|
2763
|
+
if (totalPages > 1) {
|
|
2764
|
+
elements.push({
|
|
2765
|
+
tag: 'div',
|
|
2766
|
+
text: {
|
|
2767
|
+
tag: 'lark_md',
|
|
2768
|
+
content: `**第 ${currentPage}/${totalPages} 页** (共 ${totalCount} 项)`,
|
|
2769
|
+
},
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
let remaining = pageSize;
|
|
2773
|
+
let skipped = safeOffset;
|
|
2490
2774
|
// Agent runs section
|
|
2491
|
-
if (activeRuns.length > 0) {
|
|
2775
|
+
if (activeRuns.length > 0 && skipped < activeRuns.length && remaining > 0) {
|
|
2492
2776
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '## 🤖 Agent 任务' } });
|
|
2493
|
-
|
|
2777
|
+
const start = skipped;
|
|
2778
|
+
const end = Math.min(start + remaining, activeRuns.length);
|
|
2779
|
+
for (let i = start; i < end; i++) {
|
|
2780
|
+
const run = activeRuns[i];
|
|
2494
2781
|
const statusLabel = run.terminal === 'running' ? '运行中' : '处理中';
|
|
2495
2782
|
elements.push({
|
|
2496
2783
|
tag: 'div',
|
|
@@ -2513,11 +2800,19 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2513
2800
|
});
|
|
2514
2801
|
elements.push({ tag: 'hr' });
|
|
2515
2802
|
}
|
|
2803
|
+
remaining -= end - start;
|
|
2804
|
+
skipped = 0;
|
|
2805
|
+
}
|
|
2806
|
+
else {
|
|
2807
|
+
skipped -= activeRuns.length;
|
|
2516
2808
|
}
|
|
2517
2809
|
// Bash runs section
|
|
2518
|
-
if (activeBashRuns.length > 0) {
|
|
2810
|
+
if (activeBashRuns.length > 0 && skipped < activeBashRuns.length && remaining > 0) {
|
|
2519
2811
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '## 💻 Bash 命令' } });
|
|
2520
|
-
|
|
2812
|
+
const start = skipped;
|
|
2813
|
+
const end = Math.min(start + remaining, activeBashRuns.length);
|
|
2814
|
+
for (let i = start; i < end; i++) {
|
|
2815
|
+
const run = activeBashRuns[i];
|
|
2521
2816
|
const statusLabel = run.terminal === 'running' ? '运行中' : '处理中';
|
|
2522
2817
|
elements.push({
|
|
2523
2818
|
tag: 'div',
|
|
@@ -2538,6 +2833,38 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2538
2833
|
elements.push({ tag: 'hr' });
|
|
2539
2834
|
}
|
|
2540
2835
|
}
|
|
2836
|
+
// Pagination buttons
|
|
2837
|
+
if (totalPages > 1) {
|
|
2838
|
+
const paginationButtons = [];
|
|
2839
|
+
if (currentPage > 1) {
|
|
2840
|
+
const prevOffset = (currentPage - 2) * pageSize;
|
|
2841
|
+
paginationButtons.push({
|
|
2842
|
+
tag: 'button',
|
|
2843
|
+
text: { tag: 'plain_text', content: '◀ 上一页' },
|
|
2844
|
+
type: 'default',
|
|
2845
|
+
size: 'small',
|
|
2846
|
+
behaviors: [{ type: 'callback', value: { cmd: 'active.page', offset: prevOffset } }],
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
if (currentPage < totalPages) {
|
|
2850
|
+
const nextOffset = currentPage * pageSize;
|
|
2851
|
+
paginationButtons.push({
|
|
2852
|
+
tag: 'button',
|
|
2853
|
+
text: { tag: 'plain_text', content: '下一页 ▶' },
|
|
2854
|
+
type: 'primary',
|
|
2855
|
+
size: 'small',
|
|
2856
|
+
behaviors: [{ type: 'callback', value: { cmd: 'active.page', offset: nextOffset } }],
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
elements.push({
|
|
2860
|
+
tag: 'column_set',
|
|
2861
|
+
columns: paginationButtons.map((btn) => ({
|
|
2862
|
+
tag: 'column',
|
|
2863
|
+
width: 'auto',
|
|
2864
|
+
elements: [btn],
|
|
2865
|
+
})),
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2541
2868
|
const card = {
|
|
2542
2869
|
schema: '2.0',
|
|
2543
2870
|
config: { wide_screen_mode: true },
|
|
@@ -2558,7 +2885,9 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2558
2885
|
return { text: '重连失败,请检查网络和配置' };
|
|
2559
2886
|
}
|
|
2560
2887
|
}
|
|
2561
|
-
cmdConfig(args, ctx) {
|
|
2888
|
+
async cmdConfig(args, ctx) {
|
|
2889
|
+
// Ensure probe cache is populated before building the card.
|
|
2890
|
+
await probeAllAgents();
|
|
2562
2891
|
// Check if this is a direct set command: /config <key> <value>
|
|
2563
2892
|
// Command-style set: immediate write to disk, clear pendingConfig
|
|
2564
2893
|
if (args.length >= 2) {
|
|
@@ -2604,25 +2933,60 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2604
2933
|
// 使用 pendingConfig(若存在)或当前 config
|
|
2605
2934
|
const displayConfig = this.pendingConfig ?? this.config;
|
|
2606
2935
|
// 定义字段分组:defaultAgent 选择器 + 该 agent 的配置字段 + idle.watchdogMinutes +
|
|
2607
|
-
// output/logging。stopGraceMs
|
|
2936
|
+
// output/logging。stopGraceMs 等实现细节走 YAML,不在卡片暴露。
|
|
2608
2937
|
// 根据 defaultAgent 构建完整的 Agent 配置字段组
|
|
2609
2938
|
// 使用 agent config builder 获取该 agent 的配置字段
|
|
2610
2939
|
const defaultAgent = displayConfig.defaultAgent ?? 'claude';
|
|
2611
2940
|
const configBuilder = getConfigBuilder(defaultAgent);
|
|
2612
|
-
|
|
2613
|
-
//
|
|
2614
|
-
//
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2941
|
+
// Build defaultAgent selector with availability annotations.
|
|
2942
|
+
// Direct inline construction (not via ConfigField) so that text (display label)
|
|
2943
|
+
// and value (agentKind for config storage) can differ — uninstalled agents get
|
|
2944
|
+
// a "⚠️ (未安装)" suffix in the label while the value stays clean.
|
|
2945
|
+
// Uninstalled agents sink to the bottom; installed ones keep registration
|
|
2946
|
+
// order (claude → codex → pi → opencode → kimi).
|
|
2947
|
+
const allAgents = sortAgentsByAvailability(listRegisteredAgents(), getCachedAvailability);
|
|
2948
|
+
const agentOptions = allAgents.map((kind) => {
|
|
2949
|
+
const available = getCachedAvailability(kind);
|
|
2950
|
+
const label = available === false ? `${agentDisplayName(kind)} ⚠️ (未安装)` : agentDisplayName(kind);
|
|
2951
|
+
return { text: { tag: 'plain_text', content: label }, value: kind };
|
|
2620
2952
|
});
|
|
2953
|
+
const selectedAgent = displayConfig.defaultAgent ?? 'claude';
|
|
2954
|
+
const agentSelector = {
|
|
2955
|
+
tag: 'column_set',
|
|
2956
|
+
flex_mode: 'none',
|
|
2957
|
+
columns: [
|
|
2958
|
+
{
|
|
2959
|
+
tag: 'column',
|
|
2960
|
+
width: 'weighted',
|
|
2961
|
+
weight: 2,
|
|
2962
|
+
vertical_align: 'center',
|
|
2963
|
+
elements: [{ tag: 'div', text: { tag: 'lark_md', content: '默认 Agent' } }],
|
|
2964
|
+
},
|
|
2965
|
+
{
|
|
2966
|
+
tag: 'column',
|
|
2967
|
+
width: 'weighted',
|
|
2968
|
+
weight: 3,
|
|
2969
|
+
vertical_align: 'center',
|
|
2970
|
+
elements: [
|
|
2971
|
+
{
|
|
2972
|
+
tag: 'select_static',
|
|
2973
|
+
placeholder: { tag: 'plain_text', content: '请选择' },
|
|
2974
|
+
options: agentOptions,
|
|
2975
|
+
initial_option: selectedAgent,
|
|
2976
|
+
behaviors: [{ type: 'callback', value: { cmd: 'config.set', key: 'defaultAgent' } }],
|
|
2977
|
+
},
|
|
2978
|
+
],
|
|
2979
|
+
},
|
|
2980
|
+
],
|
|
2981
|
+
};
|
|
2982
|
+
// Agent config fields — no longer include defaultAgent as a ConfigField
|
|
2983
|
+
// (it's rendered inline above with text/value separation).
|
|
2984
|
+
const agentConfigFieldsNoSelector = configBuilder.buildFields(displayConfig);
|
|
2621
2985
|
const tabs = [
|
|
2622
2986
|
{
|
|
2623
2987
|
id: 'agent',
|
|
2624
2988
|
label: `🤖 ${agentDisplayName(defaultAgent)}`,
|
|
2625
|
-
fields:
|
|
2989
|
+
fields: agentConfigFieldsNoSelector,
|
|
2626
2990
|
},
|
|
2627
2991
|
{
|
|
2628
2992
|
id: 'idle',
|
|
@@ -2653,11 +3017,30 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2653
3017
|
];
|
|
2654
3018
|
// 使用共享渲染模块构建卡片
|
|
2655
3019
|
const card = buildConfigCardFromTabs(tabs, displayConfig);
|
|
3020
|
+
// Inject the agent selector as the first element in the agent tab section,
|
|
3021
|
+
// right after the "**🤖 ..." header, so text/value can differ for uninstalled agents.
|
|
3022
|
+
// NOTE: findIndex relies on the agent tab header starting with "**🤖" — if the
|
|
3023
|
+
// tab label format changes, this fallback will fire and the selector moves to top.
|
|
3024
|
+
const body = card.body;
|
|
3025
|
+
if (body && Array.isArray(body.elements)) {
|
|
3026
|
+
const headerIdx = body.elements.findIndex((el) => typeof el === 'object' &&
|
|
3027
|
+
el !== null &&
|
|
3028
|
+
el.tag === 'div' &&
|
|
3029
|
+
el.text?.tag === 'lark_md' &&
|
|
3030
|
+
(el.text?.content ?? '').startsWith('**🤖'));
|
|
3031
|
+
if (headerIdx >= 0) {
|
|
3032
|
+
body.elements.splice(headerIdx + 1, 0, agentSelector);
|
|
3033
|
+
}
|
|
3034
|
+
else {
|
|
3035
|
+
getLogger().warn('[buildConfigCard] agent tab header not found, falling back to unshift');
|
|
3036
|
+
body.elements.unshift(agentSelector);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
2656
3039
|
return { card };
|
|
2657
3040
|
}
|
|
2658
|
-
cmdOrder(args, _ctx) {
|
|
3041
|
+
cmdOrder(args, _ctx, offset = 0) {
|
|
2659
3042
|
const sub = args[0]?.toLowerCase();
|
|
2660
|
-
// /order — list orders (
|
|
3043
|
+
// /order — list orders (CardKit 2.0 with pagination)
|
|
2661
3044
|
if (!sub || sub === 'list') {
|
|
2662
3045
|
this.orderStore.reload();
|
|
2663
3046
|
const allOrders = this.orderStore.get();
|
|
@@ -2666,8 +3049,17 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2666
3049
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '暂无指令' } });
|
|
2667
3050
|
}
|
|
2668
3051
|
else {
|
|
2669
|
-
|
|
2670
|
-
|
|
3052
|
+
// Pagination calculations
|
|
3053
|
+
const totalCount = allOrders.length;
|
|
3054
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / ORDER_PAGE_SIZE));
|
|
3055
|
+
// Clamp offset to last page boundary to avoid empty pages
|
|
3056
|
+
const maxOffset = Math.max(0, (totalPages - 1) * ORDER_PAGE_SIZE);
|
|
3057
|
+
const safeOffset = Math.min(Math.max(offset, 0), maxOffset);
|
|
3058
|
+
const currentPage = Math.floor(safeOffset / ORDER_PAGE_SIZE) + 1;
|
|
3059
|
+
const pageOrders = allOrders.slice(safeOffset, safeOffset + ORDER_PAGE_SIZE);
|
|
3060
|
+
const hasPagination = totalCount > ORDER_PAGE_SIZE;
|
|
3061
|
+
for (let i = 0; i < pageOrders.length; i++) {
|
|
3062
|
+
const order = pageOrders[i];
|
|
2671
3063
|
const displayText = order.text.length > 100 ? order.text.slice(0, 97) + '...' : order.text;
|
|
2672
3064
|
// 文字在上方
|
|
2673
3065
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: displayText } });
|
|
@@ -2700,7 +3092,10 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2700
3092
|
type: 'danger',
|
|
2701
3093
|
size: 'small',
|
|
2702
3094
|
behaviors: [
|
|
2703
|
-
{
|
|
3095
|
+
{
|
|
3096
|
+
type: 'callback',
|
|
3097
|
+
value: { cmd: 'order.delete', orderId: order.id, offset: safeOffset },
|
|
3098
|
+
},
|
|
2704
3099
|
],
|
|
2705
3100
|
},
|
|
2706
3101
|
],
|
|
@@ -2708,10 +3103,81 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2708
3103
|
],
|
|
2709
3104
|
});
|
|
2710
3105
|
// 非最后一项,添加分隔线
|
|
2711
|
-
if (i <
|
|
3106
|
+
if (i < pageOrders.length - 1) {
|
|
2712
3107
|
elements.push({ tag: 'hr' });
|
|
2713
3108
|
}
|
|
2714
3109
|
}
|
|
3110
|
+
// Pagination bar (only shown when there are more items than ORDER_PAGE_SIZE)
|
|
3111
|
+
if (hasPagination) {
|
|
3112
|
+
const hasPrev = safeOffset > 0;
|
|
3113
|
+
const hasNext = safeOffset + ORDER_PAGE_SIZE < totalCount;
|
|
3114
|
+
const pageColumns = [];
|
|
3115
|
+
if (hasPrev) {
|
|
3116
|
+
pageColumns.push({
|
|
3117
|
+
tag: 'column',
|
|
3118
|
+
width: 'auto',
|
|
3119
|
+
vertical_align: 'center',
|
|
3120
|
+
elements: [
|
|
3121
|
+
{
|
|
3122
|
+
tag: 'button',
|
|
3123
|
+
text: { tag: 'plain_text', content: '⬅ 上一页' },
|
|
3124
|
+
type: 'default',
|
|
3125
|
+
size: 'small',
|
|
3126
|
+
behaviors: [
|
|
3127
|
+
{
|
|
3128
|
+
type: 'callback',
|
|
3129
|
+
value: {
|
|
3130
|
+
cmd: 'order.page',
|
|
3131
|
+
offset: safeOffset - ORDER_PAGE_SIZE,
|
|
3132
|
+
},
|
|
3133
|
+
},
|
|
3134
|
+
],
|
|
3135
|
+
},
|
|
3136
|
+
],
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
pageColumns.push({
|
|
3140
|
+
tag: 'column',
|
|
3141
|
+
width: 'weighted',
|
|
3142
|
+
weight: 1,
|
|
3143
|
+
vertical_align: 'center',
|
|
3144
|
+
elements: [
|
|
3145
|
+
{
|
|
3146
|
+
tag: 'div',
|
|
3147
|
+
text: {
|
|
3148
|
+
tag: 'lark_md',
|
|
3149
|
+
content: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 条)`,
|
|
3150
|
+
},
|
|
3151
|
+
},
|
|
3152
|
+
],
|
|
3153
|
+
});
|
|
3154
|
+
if (hasNext) {
|
|
3155
|
+
pageColumns.push({
|
|
3156
|
+
tag: 'column',
|
|
3157
|
+
width: 'auto',
|
|
3158
|
+
vertical_align: 'center',
|
|
3159
|
+
elements: [
|
|
3160
|
+
{
|
|
3161
|
+
tag: 'button',
|
|
3162
|
+
text: { tag: 'plain_text', content: '下一页 ➡' },
|
|
3163
|
+
type: 'default',
|
|
3164
|
+
size: 'small',
|
|
3165
|
+
behaviors: [
|
|
3166
|
+
{
|
|
3167
|
+
type: 'callback',
|
|
3168
|
+
value: {
|
|
3169
|
+
cmd: 'order.page',
|
|
3170
|
+
offset: safeOffset + ORDER_PAGE_SIZE,
|
|
3171
|
+
},
|
|
3172
|
+
},
|
|
3173
|
+
],
|
|
3174
|
+
},
|
|
3175
|
+
],
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
elements.push({ tag: 'hr' });
|
|
3179
|
+
elements.push({ tag: 'column_set', columns: pageColumns });
|
|
3180
|
+
}
|
|
2715
3181
|
}
|
|
2716
3182
|
return {
|
|
2717
3183
|
card: {
|