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.
Files changed (50) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.en.md +2 -5
  3. package/README.md +1 -4
  4. package/dist/bridge/index.d.ts +5 -1
  5. package/dist/bridge/index.js +12 -2
  6. package/dist/card/card-budget.js +1 -2
  7. package/dist/card/run-renderer.js +1 -0
  8. package/dist/card/run-state.d.ts +4 -0
  9. package/dist/config/codex-config.d.ts +3 -5
  10. package/dist/config/codex-config.js +7 -7
  11. package/dist/config/dir.d.ts +1 -0
  12. package/dist/config/dir.js +4 -0
  13. package/dist/config/index.d.ts +0 -10
  14. package/dist/config/index.js +0 -11
  15. package/dist/connector/index.js +28 -0
  16. package/dist/index.js +74 -21
  17. package/dist/router/config/codex.js +8 -12
  18. package/dist/router/config/index.d.ts +10 -0
  19. package/dist/router/config/index.js +17 -0
  20. package/dist/router/index.d.ts +40 -1
  21. package/dist/router/index.js +514 -48
  22. package/dist/router/utils.d.ts +3 -0
  23. package/dist/router/utils.js +15 -3
  24. package/dist/runner/claude/runner.d.ts +0 -1
  25. package/dist/runner/claude/runner.js +1 -1
  26. package/dist/runner/codex/runner.d.ts +0 -2
  27. package/dist/runner/codex/runner.js +1 -1
  28. package/dist/runner/common/spawning-runner.d.ts +0 -1
  29. package/dist/runner/common/spawning-runner.js +2 -4
  30. package/dist/runner/kimi/runner.d.ts +0 -1
  31. package/dist/runner/kimi/runner.js +1 -1
  32. package/dist/runner/opencode/runner.d.ts +0 -2
  33. package/dist/runner/opencode/runner.js +1 -1
  34. package/dist/runner/pi/runner.d.ts +0 -1
  35. package/dist/runner/pi/runner.js +1 -1
  36. package/dist/runner/probe.d.ts +45 -0
  37. package/dist/runner/probe.js +87 -0
  38. package/dist/runner/types.d.ts +3 -0
  39. package/dist/session/claude/sessions.d.ts +1 -1
  40. package/dist/session/claude/sessions.js +77 -31
  41. package/dist/session/codex/rollout-reader.js +9 -0
  42. package/dist/session/kimi/sessions.d.ts +3 -1
  43. package/dist/session/kimi/sessions.js +9 -3
  44. package/dist/session/opencode/sessions.d.ts +0 -2
  45. package/dist/session/opencode/sessions.js +1 -1
  46. package/dist/startup-contact.d.ts +4 -2
  47. package/dist/startup-contact.js +7 -4
  48. package/dist/workspace/index.d.ts +19 -1
  49. package/dist/workspace/index.js +51 -5
  50. package/package.json +39 -4
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { parseCliArgs, resolveConfigDir, setConfigDir, printHelp, printVersion,
5
5
  import { FeishuConnector } from './connector/index.js';
6
6
  import { ClaudeRunner, CodexExecRunner, OpencodeExecRunner, PiRunner, KimiRunner, } from './runner/index.js';
7
7
  import { AgentRegistry } from './runner/registry.js';
8
+ import { probeAllAgents } from './runner/probe.js';
8
9
  import { SessionReaderRegistry, SessionStore } from './session/index.js';
9
10
  import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, } from './session/index.js';
10
11
  import { CommandRouter, isImmediateAction, formatUsageStats, } from './router/index.js';
@@ -21,6 +22,7 @@ import { markdownDiv } from './card/collapsible.js';
21
22
  import { sessionEventPanel } from './router/card-helpers.js';
22
23
  import { newSessionButton, agentDisplayName } from './card/card-shared.js';
23
24
  import path from 'node:path';
25
+ import fs from 'node:fs';
24
26
  /** Send auto-resume card for a restored session */
25
27
  async function sendAutoResumeCard(connector, contact, cwd, session, state) {
26
28
  if (!contact.userId)
@@ -145,14 +147,13 @@ function setupInstanceLockAndHandlers(instanceLock, logger, configDir) {
145
147
  function initializeRunner(config, configDir, cliArgs) {
146
148
  const agentRegistry = new AgentRegistry();
147
149
  // P1-15: Claude factory reads from configContainer (not closure) so runtime
148
- // config changes (binary, model, effort, stopGraceMs) take effect after
150
+ // config changes (model, effort, stopGraceMs) take effect after
149
151
  // bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
150
152
  agentRegistry.register('claude', (ws) => {
151
153
  const container = agentRegistry.getConfigContainer();
152
154
  const latestConfig = container?.current ?? config;
153
155
  const claudeConfig = latestConfig.claude;
154
156
  return new ClaudeRunner({
155
- binary: claudeConfig.binary,
156
157
  model: claudeConfig.model,
157
158
  effort: claudeConfig.effort,
158
159
  stopGraceMs: claudeConfig.stopGraceMs,
@@ -170,7 +171,6 @@ function initializeRunner(config, configDir, cliArgs) {
170
171
  const latestConfig = container?.current ?? config;
171
172
  const codexConfig = getAgentConfig(latestConfig, 'codex');
172
173
  return new CodexExecRunner({
173
- binary: codexConfig?.binary ?? 'codex',
174
174
  model: codexConfig?.model,
175
175
  modelProvider: codexConfig?.modelProvider,
176
176
  reasoningEffort: codexConfig?.reasoningEffort,
@@ -182,20 +182,17 @@ function initializeRunner(config, configDir, cliArgs) {
182
182
  });
183
183
  // Register OpencodeExecRunner (run mode)
184
184
  // Using `opencode run --format json --auto` instead of HTTP+SSE server mode
185
- const opencodeConfig = getAgentConfig(config, 'opencode');
186
185
  // Register session reader (same instance as used by runner)
187
186
  const sessionReaderRegistry = new SessionReaderRegistry();
188
187
  sessionReaderRegistry.register('claude', new ClaudeSessionReader());
189
188
  sessionReaderRegistry.register('codex', codexSessionReader);
190
189
  // Register OpencodeSessionReader (CLI version)
191
190
  // Using `opencode session list` and `opencode export` instead of HTTP API
192
- const opencodeSessionReader = new OpencodeSessionReader({
193
- binary: opencodeConfig?.binary ?? 'opencode',
194
- });
191
+ const opencodeSessionReader = new OpencodeSessionReader();
195
192
  agentRegistry.register('opencode', (ws) => {
196
193
  // Get latest config from container (set below for pi)
197
194
  const container = agentRegistry.getConfigContainer();
198
- const latestConfig = container?.current;
195
+ const latestConfig = container?.current ?? config;
199
196
  const ocConfig = getAgentConfig(latestConfig, 'opencode');
200
197
  // Build model string with validation: provider/model format
201
198
  let model;
@@ -210,7 +207,6 @@ function initializeRunner(config, configDir, cliArgs) {
210
207
  }
211
208
  }
212
209
  return new OpencodeExecRunner({
213
- binary: ocConfig?.binary ?? 'opencode',
214
210
  model,
215
211
  // P1-15: read stopGraceMs from latestConfig (not startup closure)
216
212
  stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
@@ -230,10 +226,9 @@ function initializeRunner(config, configDir, cliArgs) {
230
226
  agentRegistry.register('pi', (ws) => {
231
227
  // 每次 factory 调用时从 registry 获取最新 config
232
228
  const container = agentRegistry.getConfigContainer();
233
- const latestConfig = container?.current;
229
+ const latestConfig = container?.current ?? config;
234
230
  const piConf = getAgentConfig(latestConfig, 'pi');
235
231
  return new PiRunner({
236
- binary: piConf?.binary ?? 'pi',
237
232
  provider: piConf?.provider ?? 'Volcano',
238
233
  model: piConf?.model ?? 'glm-5.2',
239
234
  thinking: piConf?.thinking ?? 'medium',
@@ -252,12 +247,12 @@ function initializeRunner(config, configDir, cliArgs) {
252
247
  agentRegistry.register('kimi', (ws) => {
253
248
  // 每次 factory 调用时从 registry 获取最新 config
254
249
  const container = agentRegistry.getConfigContainer();
255
- const latestConfig = container?.current;
250
+ const latestConfig = container?.current ?? config;
256
251
  const kimiConf = getAgentConfig(latestConfig, 'kimi');
257
252
  return new KimiRunner({
258
- binary: kimiConf?.binary ?? 'kimi',
259
253
  model: kimiConf?.model ?? 'kimi-code/k3',
260
254
  thinkingEffort: kimiConf?.thinkingEffort ?? 'max',
255
+ // P1-15: read stopGraceMs from latestConfig (not startup closure)
261
256
  stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
262
257
  pidDir: configDir,
263
258
  workspace: ws,
@@ -273,10 +268,25 @@ function initializeRunner(config, configDir, cliArgs) {
273
268
  agentRegistry.registerDisplayName('kimi', 'Kimi');
274
269
  // 设置全局 registry,供 agentDisplayName 等全局函数使用
275
270
  AgentRegistry.setGlobalInstance(agentRegistry);
271
+ // Probe agent CLI availability (fire-and-forget, populates cache for /config card).
272
+ // Don't block startup — log unavailable agents as warnings.
273
+ probeAllAgents()
274
+ .then((availability) => {
275
+ const unavailable = [...availability.entries()].filter(([, ok]) => !ok).map(([kind]) => kind);
276
+ if (unavailable.length > 0) {
277
+ getLogger().warn(`[probe] agent CLI not found or not functional: ${unavailable.join(', ')}`);
278
+ }
279
+ else {
280
+ getLogger().info('[probe] all agent CLIs available');
281
+ }
282
+ })
283
+ .catch(() => {
284
+ // Probe failure is non-fatal; /config card will retry on open.
285
+ });
276
286
  return { agentRegistry, sessionReaderRegistry };
277
287
  }
278
288
  /** Setup message and card action handlers. */
279
- function setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger) {
289
+ function setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger, config) {
280
290
  connector.setMessageHandler((msg) => {
281
291
  // 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时要求 PIN 认领
282
292
  const decision = binder.classify(msg.userId, msg.content, msg.chatId);
@@ -286,9 +296,50 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
286
296
  if (decision.kind === 'pin_wrong')
287
297
  return;
288
298
  if (decision.kind === 'bind_success') {
289
- void connector
290
- .sendWithRetry(msg.chatId, { text: '✅ 已绑定到本账号,此后仅你可使用本应用' })
291
- .catch((err) => logger.error('[binder] bind-success reply failed:', err));
299
+ // First-run onboarding: set default cwd + send welcome + Help card.
300
+ // setCwd is synchronous (outside the async closure) so that the user's
301
+ // next message — which will hit the `owner` branch now that
302
+ // startup-contact.json has been written — finds cwd already set.
303
+ // process.cwd() is the only sensible default: the user started
304
+ // lark-remote in the directory they want to work in.
305
+ // setCwd only when no cwd is set yet (e.g. fresh start after bind).
306
+ // Don't overwrite an existing cwd — the user may have already set one
307
+ // via /cd or a previous bind. Use realpath for canonical form (matching
308
+ // Claude JSONL cwd, e.g. /tmp → /private/tmp on macOS).
309
+ let resolvedCwd;
310
+ try {
311
+ resolvedCwd = fs.realpathSync(process.cwd());
312
+ }
313
+ catch {
314
+ resolvedCwd = process.cwd();
315
+ }
316
+ if (!sessionStore.getCwd(msg.userId)) {
317
+ sessionStore.setCwd(msg.userId, resolvedCwd);
318
+ }
319
+ void (async () => {
320
+ try {
321
+ // Message 1: bind confirmation + status (combined to avoid
322
+ // Feishu out-of-order delivery across three separate messages).
323
+ await connector.sendWithRetry(msg.chatId, {
324
+ text: `✅ 已绑定到本账号,此后仅你可使用本应用\n` +
325
+ `📂 当前工作目录: \`${resolvedCwd}\`\n` +
326
+ `🤖 默认 Agent: ${agentDisplayName(config.defaultAgent)}\n` +
327
+ `💡 直接输入消息即可开始对话,或 /cd 切换目录`,
328
+ });
329
+ // Message 2: Help card.
330
+ // Use router.cmdHelp() + manual send (without replyTo) so the Help
331
+ // card appears as an independent message, not a reply to the PIN
332
+ // message. router.handle('/help') would set replyTo=msg.messageId
333
+ // (the PIN), causing the card to nest under the PIN in Feishu UI.
334
+ const helpResult = router.cmdHelp();
335
+ if (helpResult.card) {
336
+ await connector.sendWithRetry(msg.chatId, { card: helpResult.card });
337
+ }
338
+ }
339
+ catch (err) {
340
+ logger.error('[binder] onboarding send failed:', err);
341
+ }
342
+ })();
292
343
  return;
293
344
  }
294
345
  // decision.kind === 'owner':正常处理(不再每条覆盖 startup-contact)
@@ -343,7 +394,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
343
394
  if (!workspace && workspaceStore) {
344
395
  const workspaces = workspaceStore.list();
345
396
  if (workspaces.length > 0) {
346
- workspace = workspaces[0][1];
397
+ // NOTE: fallback uses insertion order, not sort preference — by design
398
+ // (workspace-sorting.md §9: cwd fallback stays insertion-order for now)
399
+ workspace = workspaces[0].path;
347
400
  }
348
401
  }
349
402
  // Step4/D4: 入队时刻(T0)快照 agent+session,随任务闭包带到 T1 执行时刻,
@@ -486,7 +539,7 @@ async function main() {
486
539
  logger.info('config loaded');
487
540
  logger.info(`configDir = ${configDir}`);
488
541
  logger.info(`feishu.appId = ${config.feishu.appId}`);
489
- logger.info(`claude.binary = ${config.claude.binary}`);
542
+ logger.info(`claude.model = ${config.claude.model}`);
490
543
  if (cliArgs.settings) {
491
544
  logger.info(`claude.settings = ${cliArgs.settings}`);
492
545
  }
@@ -522,7 +575,7 @@ async function main() {
522
575
  restartSpawner: () => spawnReplacementBridge(path.join(configDir, 'logs')),
523
576
  sessionReaderRegistry,
524
577
  });
525
- setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger);
578
+ setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger, config);
526
579
  try {
527
580
  await connector.connect();
528
581
  }
@@ -532,7 +585,7 @@ async function main() {
532
585
  }
533
586
  if (binder.isBound()) {
534
587
  // 仅已绑定时发送启动通知;未绑定时不打扰(PIN 引导已在控制台输出)
535
- await sendStartupHello(connector, startupContactStore);
588
+ await sendStartupHello(connector, startupContactStore, { dev: cliArgs.dev });
536
589
  }
537
590
  // Auto-restore: resume the persisted sessionId if available
538
591
  const restoredContact = startupContactStore.getContact();
@@ -6,14 +6,12 @@ export class CodexConfigBuilder {
6
6
  buildFields(displayConfig) {
7
7
  const fields = [];
8
8
  // 从 codex config.toml 动态读取 provider 和 model
9
- const codexBinary = displayConfig.agents?.codex?.binary ?? 'codex';
10
- const codexCfg = loadCodexConfig({ binary: codexBinary });
9
+ const codexCfg = loadCodexConfig();
11
10
  const codexProviderNames = codexCfg.providerNames;
12
11
  const currentModelProvider = displayConfig.agents?.codex?.modelProvider ?? codexCfg.currentProvider;
13
12
  const currentCodexModel = displayConfig.agents?.codex?.model ?? codexCfg.currentModel;
14
- const currentReasoningEffort = displayConfig.agents?.codex?.reasoningEffort ??
15
- getDefaultReasoningEffort(currentCodexModel, codexBinary);
16
- // 传入当前 provider 过滤模型列���
13
+ const currentReasoningEffort = displayConfig.agents?.codex?.reasoningEffort ?? getDefaultReasoningEffort(currentCodexModel);
14
+ // 传入当前 provider 过滤模型列表
17
15
  const codexModelOptions = codexCfg.modelOptions(currentModelProvider);
18
16
  fields.push({
19
17
  key: 'agents.codex.modelProvider',
@@ -42,7 +40,7 @@ export class CodexConfigBuilder {
42
40
  label: '推理强度',
43
41
  type: 'select',
44
42
  // 档位按目录实际声明透传(P2-5):codex 支持 none/Custom,不再按标准枚举过滤
45
- options: getReasoningEffortOptions(currentCodexModel, codexBinary),
43
+ options: getReasoningEffortOptions(currentCodexModel),
46
44
  currentValue: currentReasoningEffort,
47
45
  });
48
46
  return fields;
@@ -50,10 +48,9 @@ export class CodexConfigBuilder {
50
48
  handleFieldChange(key, value, config) {
51
49
  const patches = [];
52
50
  patches.push({ key, value });
53
- // provider 变更时,重置 model 为�� provider 的首个模型
51
+ // provider 变更时,重置 model 为新 provider 的首个模型
54
52
  if (key === 'agents.codex.modelProvider' && typeof value === 'string') {
55
- const codexBinary = config.agents?.codex?.binary ?? 'codex';
56
- const codexCfg = loadCodexConfig({ binary: codexBinary });
53
+ const codexCfg = loadCodexConfig();
57
54
  const newModelOptions = codexCfg.modelOptions(value);
58
55
  const currentModel = config.agents?.codex?.model;
59
56
  const currentModelIsValid = newModelOptions.some((m) => m === currentModel);
@@ -86,14 +83,13 @@ export class CodexConfigBuilder {
86
83
  * 对 undefined 做键删除,P1)。
87
84
  */
88
85
  effortPatchForModel(config, newModel) {
89
- const codexBinary = config.agents?.codex?.binary ?? 'codex';
90
86
  const currentReasoningEffort = config.agents?.codex?.reasoningEffort;
91
- const newModelSupportedEfforts = getReasoningEffortOptions(newModel, codexBinary);
87
+ const newModelSupportedEfforts = getReasoningEffortOptions(newModel);
92
88
  const isCurrentEffortValid = currentReasoningEffort && newModelSupportedEfforts.includes(currentReasoningEffort);
93
89
  if (isCurrentEffortValid)
94
90
  return null;
95
91
  const middle = newModelSupportedEfforts[Math.floor((newModelSupportedEfforts.length - 1) / 2)];
96
- const newEffort = middle ?? getDefaultReasoningEffort(newModel, codexBinary);
92
+ const newEffort = middle ?? getDefaultReasoningEffort(newModel);
97
93
  return { key: 'agents.codex.reasoningEffort', value: newEffort };
98
94
  }
99
95
  }
@@ -16,3 +16,13 @@ import type { AgentConfigCardBuilder } from './types.js';
16
16
  export declare function getConfigBuilder(agent: AgentKind): AgentConfigCardBuilder;
17
17
  /** List all registered agent kinds. */
18
18
  export declare function listRegisteredAgents(): AgentKind[];
19
+ /**
20
+ * Sort agent kinds for UI lists: agents that are definitively unavailable
21
+ * (availability === false) sink to the bottom; everything else keeps its
22
+ * input order. The sort is stable, so relative order within both groups is
23
+ * preserved (e.g. registration order in the config card selector).
24
+ *
25
+ * `undefined` (probe cache missing/expired) is treated as "keep in place",
26
+ * so a stale cache cannot misplace installed agents.
27
+ */
28
+ export declare function sortAgentsByAvailability(kinds: AgentKind[], isAvailable: (kind: AgentKind) => boolean | undefined): AgentKind[];
@@ -35,4 +35,21 @@ export function getConfigBuilder(agent) {
35
35
  export function listRegisteredAgents() {
36
36
  return Object.keys(builders);
37
37
  }
38
+ /**
39
+ * Sort agent kinds for UI lists: agents that are definitively unavailable
40
+ * (availability === false) sink to the bottom; everything else keeps its
41
+ * input order. The sort is stable, so relative order within both groups is
42
+ * preserved (e.g. registration order in the config card selector).
43
+ *
44
+ * `undefined` (probe cache missing/expired) is treated as "keep in place",
45
+ * so a stale cache cannot misplace installed agents.
46
+ */
47
+ export function sortAgentsByAvailability(kinds, isAvailable) {
48
+ const unavailable = new Set(kinds.filter((kind) => isAvailable(kind) === false));
49
+ return [...kinds].sort((a, b) => {
50
+ const rankA = unavailable.has(a) ? 1 : 0;
51
+ const rankB = unavailable.has(b) ? 1 : 0;
52
+ return rankA - rankB;
53
+ });
54
+ }
38
55
  //# sourceMappingURL=index.js.map
@@ -17,6 +17,7 @@ interface CommandResult {
17
17
  * These bypass the serial queue and execute immediately.
18
18
  *
19
19
  * ws.use — /ws list card "使用 <name>"
20
+ * ws.page — /ws list card "上一页/下一页"
20
21
  * queue.immediate — queue card "立即执行" button (§9.6)
21
22
  * queue.cancel — queue card "撤销" button
22
23
  * queue.diagnose — queue card "诊断" button
@@ -25,6 +26,7 @@ interface CommandResult {
25
26
  * new-session — clear sessionId only
26
27
  * stop — interrupt current run
27
28
  * ls.file — send file only
29
+ * ws.page — paginate /ws list only
28
30
  * ws.remove — delete workspace alias only
29
31
  * resume.use — set sessionId + agent for correct reader routing
30
32
  * help.* — read-only help commands
@@ -73,6 +75,8 @@ interface CardActionResponse {
73
75
  [key: string]: unknown;
74
76
  }
75
77
  export declare class CommandRouter {
78
+ /** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
79
+ static readonly VALID_AGENTS: readonly ["claude", "codex", "opencode", "pi", "kimi"];
76
80
  private sessionStore;
77
81
  private bridge;
78
82
  private config;
@@ -96,6 +100,11 @@ export declare class CommandRouter {
96
100
  * millisecond); the counter guarantees uniqueness regardless of timing.
97
101
  */
98
102
  private orderExecKeyCounter;
103
+ /**
104
+ * Per-user sort preference for /ws list (memory-only, not persisted).
105
+ * Default 'recent' (most recently used first), can toggle to 'alpha'.
106
+ */
107
+ private wsSortPreference;
99
108
  /**
100
109
  * Session reader registry for multi-agent path. Resolves the right
101
110
  * reader by `config.defaultAgent` (and per-agent override params).
@@ -187,6 +196,11 @@ export declare class CommandRouter {
187
196
  * Mirrors handleLsPage — updates the same card, never sends a new one.
188
197
  */
189
198
  private handleResumePage;
199
+ /**
200
+ * Handle active.page: paginate the /active run list in place.
201
+ * Mirrors handleResumePage — updates the same card, never sends a new one.
202
+ */
203
+ private handleActivePage;
190
204
  /**
191
205
  * Handle ls.browse: browse directory without switching cwd.
192
206
  */
@@ -223,12 +237,36 @@ export declare class CommandRouter {
223
237
  * Handle order.delete: remove the order and update card in place.
224
238
  */
225
239
  private handleOrderDelete;
240
+ /**
241
+ * Handle order.page: paginate the /order list in place.
242
+ * Mirrors handleLsPage — updates the same card, never sends a new one.
243
+ */
244
+ private handleOrderPage;
245
+ /**
246
+ * Handle ws.use card action: switch to the workspace, then refresh the /ws
247
+ * list card in place so "recent" sort order is immediately visible.
248
+ *
249
+ * Unlike the command-line `/ws use` (which returns text), the card action
250
+ * uses toast + inplace refresh for a cleaner UX (matching ws.remove/ws.sort).
251
+ * If auto-resume produces a card, it is sent as a separate reply.
252
+ */
253
+ private handleWsUse;
226
254
  /**
227
255
  * Handle ws.remove: remove the workspace alias and refresh the /ws list card
228
256
  * in place (mirrors handleOrderDelete). Without this the stale alias would
229
257
  * remain visible on the card the user just clicked.
230
258
  */
231
259
  private handleWsRemove;
260
+ /**
261
+ * Handle ws.page: paginate the /ws list in place.
262
+ * Mirrors handleOrderPage — updates the same card, never sends a new one.
263
+ */
264
+ private handleWsPage;
265
+ /**
266
+ * Handle ws.sort: toggle sort preference and refresh the /ws list card.
267
+ * Sort preference is memory-only (not persisted); restart resets to 'recent'.
268
+ */
269
+ private handleWsSort;
232
270
  /**
233
271
  * `ls.file` card button: send file to Feishu, rejecting if > 30MB.
234
272
  */
@@ -273,7 +311,7 @@ export declare class CommandRouter {
273
311
  */
274
312
  private propagateConfigSave;
275
313
  private executeCommand;
276
- private cmdHelp;
314
+ cmdHelp(): CommandResult;
277
315
  private cmdStatus;
278
316
  private cmdPs;
279
317
  private cmdStop;
@@ -307,6 +345,7 @@ export declare class CommandRouter {
307
345
  /**
308
346
  * Build CardKit 2.0 /active card from bridge memory (not file system).
309
347
  * Shows only runs that were started by THIS bridge process.
348
+ * Paginates with ACTIVE_PAGE_SIZE (default 20) items per page.
310
349
  */
311
350
  private buildActiveCardFromMemory;
312
351
  private cmdReconnect;