@hopgoldy/agent-bridge 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -116,18 +116,33 @@ var resources = {
116
116
  progress: {
117
117
  noProgress: "No progress yet.",
118
118
  collapsed: "Collapsed {{count}} earlier updates.",
119
- running: "Running {{subject}}",
120
- finished: "Finished {{subject}}",
121
- failed: "Failed {{subject}}",
122
- failedWithDetail: "Failed {{subject}}: {{detail}}",
119
+ running: "\u23F3 {{subject}}",
120
+ finished: "\u2705 {{subject}}",
121
+ failed: "\u274C {{subject}}",
122
+ failedWithDetail: "\u274C {{subject}}: {{detail}}",
123
123
  compacting: "Compacting session",
124
124
  compactingWithDetail: "Compacting session: {{detail}}"
125
125
  },
126
126
  client: {
127
127
  processing: "Processing...",
128
- helpMessage: "Available commands:\n\n- `/new` (`/n`) - Start a new agent session\n- `/compact` (`/c`) - Compact the current session context\n- `/stop` (`/s`) - Stop the active agent run\n- `/help` (`/h`) - Show this help message",
128
+ helpMessage: "Available commands:\n\n- `/new` (`/n`) - Start a new agent session\n- `/compact` (`/c`) - Compact the current session context\n- `/stop` (`/s`) - Stop the active agent run\n- `/status` (`/st`) - Show the current agent session status\n- `/model` (`/m`) - List available models, or switch with `/model provider/modelId`\n- `/help` (`/h`) - Show this help message",
129
129
  messageDeliveryFailedTitle: "[agent-bridge error] Message delivery failed",
130
- weixinCooldown: "Weixin send is cooling down after rate limiting. Please try again shortly."
130
+ weixinCooldown: "Weixin send is cooling down after rate limiting. Please try again shortly.",
131
+ statusTitle: "Current session status",
132
+ statusSessionId: "Session ID",
133
+ statusModel: "Model",
134
+ statusThinkingLevel: "Thinking level",
135
+ statusContext: "Context",
136
+ statusUnavailable: "Current session status is unavailable.",
137
+ statusUnavailableValue: "Unavailable",
138
+ modelListTitle: "Available models",
139
+ modelListCurrent: "current",
140
+ modelListUsage: "Use `/model provider/modelId` to switch.",
141
+ modelListUnavailable: "Available models are unavailable for the current session.",
142
+ modelSetUnavailable: "Current session model switching is unavailable.",
143
+ modelInvalid: "The requested model is invalid or unavailable.",
144
+ modelBusy: "Current session is busy, so the model cannot be switched. Please use `/stop` first.",
145
+ modelUpdated: "Switched current model to `{{model}}`."
131
146
  },
132
147
  gateway: {
133
148
  noActiveSessionToCompact: "No active agent session to compact.",
@@ -143,18 +158,33 @@ var resources = {
143
158
  progress: {
144
159
  noProgress: "\u6682\u65E0\u8FDB\u5EA6\u3002",
145
160
  collapsed: "\u5DF2\u6298\u53E0 {{count}} \u6761\u8F83\u65E9\u66F4\u65B0\u3002",
146
- running: "\u6B63\u5728\u6267\u884C {{subject}}",
147
- finished: "\u5DF2\u5B8C\u6210 {{subject}}",
148
- failed: "{{subject}} \u6267\u884C\u5931\u8D25",
149
- failedWithDetail: "{{subject}} \u6267\u884C\u5931\u8D25\uFF1A{{detail}}",
161
+ running: "\u23F3 {{subject}}",
162
+ finished: "\u2705 {{subject}}",
163
+ failed: "\u274C {{subject}}",
164
+ failedWithDetail: "\u274C {{subject}}: {{detail}}",
150
165
  compacting: "\u6B63\u5728\u538B\u7F29\u4F1A\u8BDD",
151
166
  compactingWithDetail: "\u6B63\u5728\u538B\u7F29\u4F1A\u8BDD: {{detail}}"
152
167
  },
153
168
  client: {
154
169
  processing: "\u6B63\u5728\u5904\u7406\u4E2D...",
155
- helpMessage: "\u53EF\u7528\u547D\u4EE4\uFF1A\n\n- `/new` (`/n`) - \u5F00\u59CB\u4E00\u4E2A\u65B0\u4F1A\u8BDD\n- `/compact` (`/c`) - \u538B\u7F29\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\n- `/stop` (`/s`) - \u505C\u6B62\u5F53\u524D\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\n- `/help` (`/h`) - \u67E5\u770B\u8FD9\u6761\u5E2E\u52A9\u4FE1\u606F",
170
+ helpMessage: "\u53EF\u7528\u547D\u4EE4\uFF1A\n\n- `/new` (`/n`) - \u5F00\u59CB\u4E00\u4E2A\u65B0\u4F1A\u8BDD\n- `/compact` (`/c`) - \u538B\u7F29\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\n- `/stop` (`/s`) - \u505C\u6B62\u5F53\u524D\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\n- `/status` (`/st`) - \u67E5\u770B\u5F53\u524D\u667A\u80FD\u4F53\u4F1A\u8BDD\u72B6\u6001\n- `/model` (`/m`) - \u67E5\u770B\u53EF\u7528\u6A21\u578B\uFF0C\u6216\u4F7F\u7528 `/model provider/modelId` \u5207\u6362\u6A21\u578B\n- `/help` (`/h`) - \u67E5\u770B\u8FD9\u6761\u5E2E\u52A9\u4FE1\u606F",
156
171
  messageDeliveryFailedTitle: "[agent-bridge \u9519\u8BEF] \u6D88\u606F\u53D1\u9001\u5931\u8D25",
157
- weixinCooldown: "\u5FAE\u4FE1\u53D1\u9001\u56E0\u9650\u6D41\u5DF2\u8FDB\u5165\u51B7\u5374\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002"
172
+ weixinCooldown: "\u5FAE\u4FE1\u53D1\u9001\u56E0\u9650\u6D41\u5DF2\u8FDB\u5165\u51B7\u5374\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",
173
+ statusTitle: "\u5F53\u524D\u4F1A\u8BDD\u72B6\u6001",
174
+ statusSessionId: "Session ID",
175
+ statusModel: "\u6A21\u578B",
176
+ statusThinkingLevel: "\u601D\u8003\u7B49\u7EA7",
177
+ statusContext: "\u4E0A\u4E0B\u6587",
178
+ statusUnavailable: "\u5F53\u524D\u65E0\u6CD5\u83B7\u53D6\u4F1A\u8BDD\u72B6\u6001\u3002",
179
+ statusUnavailableValue: "\u6682\u4E0D\u53EF\u7528",
180
+ modelListTitle: "\u53EF\u7528\u6A21\u578B",
181
+ modelListCurrent: "\u5F53\u524D",
182
+ modelListUsage: "\u4F7F\u7528 `/model provider/modelId` \u5207\u6362\u6A21\u578B\u3002",
183
+ modelListUnavailable: "\u5F53\u524D\u4F1A\u8BDD\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\u53EF\u7528\u6A21\u578B\u5217\u8868\u3002",
184
+ modelSetUnavailable: "\u5F53\u524D\u4F1A\u8BDD\u6682\u65F6\u65E0\u6CD5\u5207\u6362\u6A21\u578B\u3002",
185
+ modelInvalid: "\u8BF7\u6C42\u7684\u6A21\u578B\u65E0\u6548\u6216\u5F53\u524D\u4E0D\u53EF\u7528\u3002",
186
+ modelBusy: "\u5F53\u524D\u6B63\u5728\u8FD0\u884C\uFF0C\u65E0\u6CD5\u5207\u6362\u6A21\u578B\u3002\u8BF7\u5148\u4F7F\u7528 `/stop`\u3002",
187
+ modelUpdated: "\u5F53\u524D\u6A21\u578B\u5DF2\u5207\u6362\u81F3 `{{model}}`\u3002"
158
188
  },
159
189
  gateway: {
160
190
  noActiveSessionToCompact: "\u5F53\u524D\u6CA1\u6709\u53EF\u538B\u7F29\u7684\u667A\u80FD\u4F53\u4F1A\u8BDD\u3002",
@@ -335,6 +365,18 @@ var GatewayCore = class {
335
365
  await this.#handleSessionStop(event.clientSessionId);
336
366
  return;
337
367
  }
368
+ if (event.type === "command.session.status") {
369
+ await this.#handleSessionStatus(event.clientSessionId);
370
+ return;
371
+ }
372
+ if (event.type === "command.session.model.list") {
373
+ await this.#handleSessionModelList(event.clientSessionId);
374
+ return;
375
+ }
376
+ if (event.type === "command.session.model.set") {
377
+ await this.#handleSessionModelSet(event.clientSessionId, event.target);
378
+ return;
379
+ }
338
380
  await this.#handleUserMessage(event.clientSessionId, event.text);
339
381
  }
340
382
  async #handleUserMessage(clientSessionId, text2) {
@@ -389,6 +431,123 @@ var GatewayCore = class {
389
431
  }
390
432
  await runtime.agentAdapter.abort();
391
433
  }
434
+ async #handleSessionStatus(clientSessionId) {
435
+ const runtime = await this.#getActiveRuntime(clientSessionId);
436
+ if (!runtime) {
437
+ await this.#deliverClientInput({
438
+ type: "error",
439
+ clientSessionId,
440
+ kind: "agent.status.unavailable"
441
+ });
442
+ return;
443
+ }
444
+ this.#touchRuntime(runtime);
445
+ if (!runtime.agentAdapter.getStatus) {
446
+ await this.#deliverClientInput({
447
+ type: "error",
448
+ clientSessionId,
449
+ kind: "agent.status.unavailable"
450
+ });
451
+ return;
452
+ }
453
+ try {
454
+ const status = await runtime.agentAdapter.getStatus();
455
+ await this.#deliverClientInput({
456
+ type: "agent.status.info",
457
+ clientSessionId,
458
+ status
459
+ });
460
+ } catch (error) {
461
+ const detail = error instanceof Error ? error.message : String(error);
462
+ await this.#deliverClientInput({
463
+ type: "error",
464
+ clientSessionId,
465
+ kind: "agent.status.unavailable",
466
+ detail
467
+ });
468
+ }
469
+ }
470
+ async #handleSessionModelList(clientSessionId) {
471
+ const runtime = await this.#getActiveRuntime(clientSessionId);
472
+ if (!runtime) {
473
+ await this.#deliverClientInput({
474
+ type: "error",
475
+ clientSessionId,
476
+ kind: "agent.model.list.unavailable"
477
+ });
478
+ return;
479
+ }
480
+ this.#touchRuntime(runtime);
481
+ if (!runtime.agentAdapter.getAvailableModels) {
482
+ await this.#deliverClientInput({
483
+ type: "error",
484
+ clientSessionId,
485
+ kind: "agent.model.list.unavailable"
486
+ });
487
+ return;
488
+ }
489
+ try {
490
+ const models = await runtime.agentAdapter.getAvailableModels();
491
+ await this.#deliverClientInput({
492
+ type: "agent.model.list",
493
+ clientSessionId,
494
+ models
495
+ });
496
+ } catch (error) {
497
+ const detail = error instanceof Error ? error.message : String(error);
498
+ await this.#deliverClientInput({
499
+ type: "error",
500
+ clientSessionId,
501
+ kind: "agent.model.list.unavailable",
502
+ detail
503
+ });
504
+ }
505
+ }
506
+ async #handleSessionModelSet(clientSessionId, target) {
507
+ const runtime = await this.#getActiveRuntime(clientSessionId);
508
+ if (!runtime) {
509
+ await this.#deliverClientInput({
510
+ type: "error",
511
+ clientSessionId,
512
+ kind: "agent.model.set.unavailable"
513
+ });
514
+ return;
515
+ }
516
+ this.#touchRuntime(runtime);
517
+ if (!runtime.agentAdapter.setModel) {
518
+ await this.#deliverClientInput({
519
+ type: "error",
520
+ clientSessionId,
521
+ kind: "agent.model.set.unavailable"
522
+ });
523
+ return;
524
+ }
525
+ if (await runtime.agentAdapter.isBusy()) {
526
+ await this.#deliverClientInput({
527
+ type: "error",
528
+ clientSessionId,
529
+ kind: "agent.model.busy"
530
+ });
531
+ return;
532
+ }
533
+ try {
534
+ const result = await runtime.agentAdapter.setModel(target);
535
+ await this.#deliverClientInput({
536
+ type: "agent.model.updated",
537
+ clientSessionId,
538
+ provider: result.provider,
539
+ modelId: result.modelId
540
+ });
541
+ } catch (error) {
542
+ const { kind, detail } = this.#resolveModelCommandError(error);
543
+ await this.#deliverClientInput({
544
+ type: "error",
545
+ clientSessionId,
546
+ kind,
547
+ ...detail ? { detail } : {}
548
+ });
549
+ }
550
+ }
392
551
  async #handleSessionNew(clientSessionId) {
393
552
  const previousAgentSessionId = this.#clientToAgentSession.get(clientSessionId);
394
553
  if (previousAgentSessionId) {
@@ -482,6 +641,18 @@ var GatewayCore = class {
482
641
  this.#logger.error("failed to persist session bindings:", error);
483
642
  }
484
643
  }
644
+ #resolveModelCommandError(error) {
645
+ const detail = error instanceof Error ? error.message : String(error);
646
+ if (typeof error === "object" && error && "kind" in error && typeof error.kind === "string") {
647
+ switch (error.kind) {
648
+ case "agent.model.invalid":
649
+ case "agent.model.busy":
650
+ case "agent.model.set.unavailable":
651
+ return { kind: error.kind, ...detail ? { detail } : {} };
652
+ }
653
+ }
654
+ return { kind: "agent.model.set.unavailable", ...detail ? { detail } : {} };
655
+ }
485
656
  async #handleAgentOutput(event) {
486
657
  const agentSessionId = event.agentSessionId;
487
658
  const runtime = this.#agentRuntimes.get(agentSessionId);
@@ -884,6 +1055,19 @@ var PiRpcClient = class {
884
1055
  const response = await this.#send({ type: "get_state" });
885
1056
  return response.data ?? {};
886
1057
  }
1058
+ async getSessionStats() {
1059
+ const response = await this.#send({ type: "get_session_stats" });
1060
+ return response.data ?? {};
1061
+ }
1062
+ async getAvailableModels() {
1063
+ const response = await this.#send({ type: "get_available_models" });
1064
+ const data = response.data;
1065
+ return data?.models ?? [];
1066
+ }
1067
+ async setModel(provider, modelId) {
1068
+ const response = await this.#send({ type: "set_model", provider, modelId });
1069
+ return response.data ?? {};
1070
+ }
887
1071
  async setSessionName(name) {
888
1072
  await this.#send({ type: "set_session_name", name });
889
1073
  }
@@ -990,6 +1174,13 @@ function toPiSessionId(agentSessionId) {
990
1174
  }
991
1175
 
992
1176
  // src/modules/agent/pi-coding-agent/adapter/pi-coding-agent-adapter.ts
1177
+ var PiModelCommandError = class extends Error {
1178
+ kind;
1179
+ constructor(kind, message) {
1180
+ super(message);
1181
+ this.kind = kind;
1182
+ }
1183
+ };
993
1184
  var PiCodingAgentAdapter = class {
994
1185
  #agentSessionId;
995
1186
  #piSessionId;
@@ -1072,6 +1263,71 @@ var PiCodingAgentAdapter = class {
1072
1263
  async isBusy() {
1073
1264
  return this.#processing || this.#inputQueue.length > 0;
1074
1265
  }
1266
+ async getStatus() {
1267
+ if (!this.#client) {
1268
+ throw new Error("PiCodingAgentAdapter is not started");
1269
+ }
1270
+ const [state, stats] = await Promise.all([this.#client.getState(), this.#client.getSessionStats()]);
1271
+ const contextUsage = stats.contextUsage;
1272
+ return {
1273
+ sessionId: state.sessionId ?? this.#agentSessionId,
1274
+ provider: state.model?.provider,
1275
+ modelId: state.model?.id,
1276
+ thinkingLevel: state.thinkingLevel,
1277
+ context: contextUsage ? {
1278
+ tokens: contextUsage.tokens ?? null,
1279
+ contextWindow: contextUsage.contextWindow ?? null,
1280
+ percent: contextUsage.percent ?? null
1281
+ } : void 0
1282
+ };
1283
+ }
1284
+ async getAvailableModels() {
1285
+ if (!this.#client) {
1286
+ throw new Error("PiCodingAgentAdapter is not started");
1287
+ }
1288
+ const [state, models] = await Promise.all([this.#client.getState(), this.#client.getAvailableModels()]);
1289
+ return models.filter((model) => typeof model.provider === "string" && typeof model.id === "string").map((model) => ({
1290
+ provider: model.provider,
1291
+ modelId: model.id,
1292
+ isCurrent: state.model?.provider === model.provider && state.model?.id === model.id
1293
+ }));
1294
+ }
1295
+ async setModel(target) {
1296
+ if (!this.#client) {
1297
+ throw new PiModelCommandError("agent.model.set.unavailable", "PiCodingAgentAdapter is not started");
1298
+ }
1299
+ const parsed = this.#parseModelTarget(target);
1300
+ const state = await this.#client.getState();
1301
+ if (state.isStreaming || state.isCompacting) {
1302
+ throw new PiModelCommandError("agent.model.busy", "Current session is busy, so the model cannot be switched. Please use /stop first.");
1303
+ }
1304
+ try {
1305
+ const result = await this.#client.setModel(parsed.provider, parsed.modelId);
1306
+ return {
1307
+ provider: result.provider ?? parsed.provider,
1308
+ modelId: result.id ?? parsed.modelId
1309
+ };
1310
+ } catch (error) {
1311
+ const message = error instanceof Error ? error.message : String(error);
1312
+ if (/Model not found:/i.test(message)) {
1313
+ throw new PiModelCommandError("agent.model.invalid", message);
1314
+ }
1315
+ throw new PiModelCommandError("agent.model.set.unavailable", message);
1316
+ }
1317
+ }
1318
+ #parseModelTarget(target) {
1319
+ const trimmed = target.trim();
1320
+ const slashIndex = trimmed.indexOf("/");
1321
+ if (slashIndex <= 0 || slashIndex === trimmed.length - 1) {
1322
+ throw new PiModelCommandError("agent.model.invalid", `Invalid model target: ${target}`);
1323
+ }
1324
+ const provider = trimmed.slice(0, slashIndex).trim();
1325
+ const modelId = trimmed.slice(slashIndex + 1).trim();
1326
+ if (!provider || !modelId) {
1327
+ throw new PiModelCommandError("agent.model.invalid", `Invalid model target: ${target}`);
1328
+ }
1329
+ return { provider, modelId };
1330
+ }
1075
1331
  async #drainInputQueue() {
1076
1332
  if (this.#processing) {
1077
1333
  return;
@@ -1183,7 +1439,7 @@ var PiCodingAgentAdapter = class {
1183
1439
  toolCallId,
1184
1440
  toolInput,
1185
1441
  toolLabel,
1186
- text: `Running ${toolName}`
1442
+ text: void 0
1187
1443
  });
1188
1444
  return;
1189
1445
  }
@@ -1204,7 +1460,7 @@ var PiCodingAgentAdapter = class {
1204
1460
  toolInput,
1205
1461
  toolLabel,
1206
1462
  partialResult: "partialResult" in rpcEvent ? rpcEvent.partialResult : void 0,
1207
- text: `Running ${toolName}`
1463
+ text: void 0
1208
1464
  });
1209
1465
  return;
1210
1466
  }
@@ -1222,7 +1478,7 @@ var PiCodingAgentAdapter = class {
1222
1478
  toolInput,
1223
1479
  toolLabel,
1224
1480
  result: "result" in rpcEvent ? rpcEvent.result : void 0,
1225
- text: isError ? void 0 : `Finished ${toolName}`
1481
+ text: void 0
1226
1482
  });
1227
1483
  if (toolCallId) {
1228
1484
  this.#toolLabelByCallId.delete(toolCallId);
@@ -1419,7 +1675,7 @@ function getTypedAgentModule(config) {
1419
1675
 
1420
1676
  // src/modules/client/utils/progress-renderer.ts
1421
1677
  var DEFAULT_COLLAPSE_THRESHOLD = 10;
1422
- var MAX_TOOL_LABEL_DISPLAY_LENGTH = 15;
1678
+ var MAX_TOOL_LABEL_DISPLAY_LENGTH = 24;
1423
1679
  var ProgressRenderer = class {
1424
1680
  #collapseThreshold;
1425
1681
  #t;
@@ -1531,11 +1787,6 @@ var ProgressRenderer = class {
1531
1787
  if (!normalizedText) {
1532
1788
  return `- ${this.#humanizeToolError(toolName)}`;
1533
1789
  }
1534
- const lowerText = normalizedText.toLowerCase();
1535
- const lowerToolName = toolName.toLowerCase();
1536
- if (lowerText === lowerToolName || lowerText === this.#t("progress.failed", { subject: toolName }).toLowerCase() || lowerText === this.#t("progress.running", { subject: toolName }).toLowerCase() || lowerText === this.#t("progress.finished", { subject: toolName }).toLowerCase()) {
1537
- return `- ${this.#humanizeToolError(toolName)}`;
1538
- }
1539
1790
  return `- ${this.#t("progress.failedWithDetail", { subject: toolName, detail: normalizedText })}`;
1540
1791
  }
1541
1792
  #humanizeToolError(toolName) {
@@ -1566,7 +1817,26 @@ function isHelpCommand(text2) {
1566
1817
  function resolveHelpMarkdown(text2, t) {
1567
1818
  return isHelpCommand(text2) ? t("client.helpMessage") : null;
1568
1819
  }
1820
+ function parseModelCommand(text2, clientSessionId) {
1821
+ const match = text2.match(/^\/(model|m)(?:\s+(.*))?$/i);
1822
+ if (!match) {
1823
+ return null;
1824
+ }
1825
+ const target = match[2]?.trim();
1826
+ if (!target) {
1827
+ return { type: "command.session.model.list", clientSessionId };
1828
+ }
1829
+ return {
1830
+ type: "command.session.model.set",
1831
+ clientSessionId,
1832
+ target
1833
+ };
1834
+ }
1569
1835
  function parseSlashCommand(text2, clientSessionId) {
1836
+ const modelCommand = parseModelCommand(text2, clientSessionId);
1837
+ if (modelCommand) {
1838
+ return modelCommand;
1839
+ }
1570
1840
  switch (text2.toLowerCase()) {
1571
1841
  case "/new":
1572
1842
  case "/n":
@@ -1577,11 +1847,87 @@ function parseSlashCommand(text2, clientSessionId) {
1577
1847
  case "/stop":
1578
1848
  case "/s":
1579
1849
  return { type: "command.session.stop", clientSessionId };
1850
+ case "/status":
1851
+ case "/st":
1852
+ return { type: "command.session.status", clientSessionId };
1580
1853
  default:
1581
1854
  return null;
1582
1855
  }
1583
1856
  }
1584
1857
 
1858
+ // src/modules/client/utils/status-markdown.ts
1859
+ function formatModel(status, t) {
1860
+ if (status.provider && status.modelId) {
1861
+ return `\`${status.provider}/${status.modelId}\``;
1862
+ }
1863
+ if (status.modelId) {
1864
+ return `\`${status.modelId}\``;
1865
+ }
1866
+ if (status.provider) {
1867
+ return `\`${status.provider}\``;
1868
+ }
1869
+ return t("client.statusUnavailableValue");
1870
+ }
1871
+ function formatContext(status, t) {
1872
+ const context = status.context;
1873
+ if (!context) {
1874
+ return t("client.statusUnavailableValue");
1875
+ }
1876
+ const { tokens, contextWindow, percent } = context;
1877
+ if (tokens == null || contextWindow == null || percent == null) {
1878
+ return t("client.statusUnavailableValue");
1879
+ }
1880
+ return `\`${tokens.toLocaleString()} / ${contextWindow.toLocaleString()} (${percent}%)\``;
1881
+ }
1882
+ function formatAvailableModel(model, t) {
1883
+ const label = `\`${model.provider}/${model.modelId}\``;
1884
+ return model.isCurrent ? `- ${label} \u2705 ${t("client.modelListCurrent")}` : `- ${label}`;
1885
+ }
1886
+ function formatErrorMarkdown(title, detail) {
1887
+ return [`**${title}**`, ...detail ? ["", detail] : []].join("\n");
1888
+ }
1889
+ function renderStatusMarkdown(event, t) {
1890
+ if (event.type === "agent.model.list") {
1891
+ return [
1892
+ `**${t("client.modelListTitle")}**`,
1893
+ "",
1894
+ ...event.models.length > 0 ? event.models.map((model) => formatAvailableModel(model, t)) : [`- ${t("client.statusUnavailableValue")}`],
1895
+ "",
1896
+ t("client.modelListUsage")
1897
+ ].join("\n");
1898
+ }
1899
+ if (event.type === "agent.model.updated") {
1900
+ return t("client.modelUpdated", { model: `${event.provider}/${event.modelId}` });
1901
+ }
1902
+ if (event.type === "agent.status.info") {
1903
+ return [
1904
+ `**${t("client.statusTitle")}**`,
1905
+ "",
1906
+ `- ${t("client.statusSessionId")}: \`${event.status.sessionId}\``,
1907
+ `- ${t("client.statusModel")}: ${formatModel(event.status, t)}`,
1908
+ `- ${t("client.statusThinkingLevel")}: ${event.status.thinkingLevel ? `\`${event.status.thinkingLevel}\`` : t("client.statusUnavailableValue")}`,
1909
+ `- ${t("client.statusContext")}: ${formatContext(event.status, t)}`
1910
+ ].join("\n");
1911
+ }
1912
+ if (event.type === "error") {
1913
+ switch (event.kind) {
1914
+ case "agent.status.unavailable":
1915
+ return formatErrorMarkdown(t("client.statusUnavailable"), event.detail);
1916
+ case "agent.model.list.unavailable":
1917
+ return formatErrorMarkdown(t("client.modelListUnavailable"), event.detail);
1918
+ case "agent.model.set.unavailable":
1919
+ return formatErrorMarkdown(t("client.modelSetUnavailable"), event.detail);
1920
+ case "agent.model.invalid":
1921
+ return formatErrorMarkdown(t("client.modelInvalid"), event.detail);
1922
+ case "agent.model.busy":
1923
+ return event.detail ?? t("client.modelBusy");
1924
+ default:
1925
+ return null;
1926
+ }
1927
+ }
1928
+ return null;
1929
+ }
1930
+
1585
1931
  // src/modules/client/feishu/adapter/feishu-client.ts
1586
1932
  import * as Lark from "@larksuiteoapi/node-sdk";
1587
1933
  import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
@@ -1981,10 +2327,14 @@ var FeishuIMAdapter = class _FeishuIMAdapter {
1981
2327
  return;
1982
2328
  }
1983
2329
  const normalizedText = text2.trim();
2330
+ this.#lastInboundMessageIdBySession.set(clientSessionId, messageId);
2331
+ this.#resetProgressState(clientSessionId);
2332
+ await this.#client?.startTyping(chatId, messageId);
1984
2333
  const helpMarkdown = resolveHelpMarkdown(normalizedText, this.#t);
1985
2334
  if (helpMarkdown) {
1986
2335
  this.#logger.info(`received local help command ${normalizedText} (session=${clientSessionId})`);
1987
2336
  await this.#client?.sendText(chatId, helpMarkdown, messageId);
2337
+ await this.#client?.stopTyping(chatId);
1988
2338
  return;
1989
2339
  }
1990
2340
  const commandEvent = parseSlashCommand(normalizedText, clientSessionId);
@@ -1993,9 +2343,6 @@ var FeishuIMAdapter = class _FeishuIMAdapter {
1993
2343
  await this.#onOutput(commandEvent);
1994
2344
  return;
1995
2345
  }
1996
- this.#lastInboundMessageIdBySession.set(clientSessionId, messageId);
1997
- this.#resetProgressState(clientSessionId);
1998
- await this.#client?.startTyping(chatId, messageId);
1999
2346
  this.#logger.info(`received user message (session=${clientSessionId}): ${normalizedText}`);
2000
2347
  await this.#onOutput({
2001
2348
  type: "user.message",
@@ -2041,6 +2388,13 @@ var FeishuIMAdapter = class _FeishuIMAdapter {
2041
2388
  try {
2042
2389
  const target = parseFeishuSessionId(event.clientSessionId);
2043
2390
  if (event.type !== "assistant.message") {
2391
+ const statusMarkdown = renderStatusMarkdown(event, this.#t);
2392
+ if (statusMarkdown) {
2393
+ const replyToMessageId2 = this.#lastInboundMessageIdBySession.get(event.clientSessionId);
2394
+ await this.#client.sendText(target.chatId, statusMarkdown, replyToMessageId2);
2395
+ await this.#client.stopTyping(target.chatId);
2396
+ continue;
2397
+ }
2044
2398
  await this.#handleProgressEvent(target.chatId, event);
2045
2399
  continue;
2046
2400
  }
@@ -2718,14 +3072,14 @@ var WecomIMAdapter = class {
2718
3072
  await this.#client?.sendText(chatId, helpMarkdown, messageId);
2719
3073
  return;
2720
3074
  }
3075
+ this.#lastInboundMessageIdBySession.set(clientSessionId, messageId);
3076
+ this.#resetProgressState(clientSessionId);
3077
+ await this.#announceStart(chatId, messageId, clientSessionId);
2721
3078
  const commandEvent = parseSlashCommand(normalizedText, clientSessionId);
2722
3079
  if (commandEvent) {
2723
3080
  await this.#onOutput(commandEvent);
2724
3081
  return;
2725
3082
  }
2726
- this.#lastInboundMessageIdBySession.set(clientSessionId, messageId);
2727
- this.#resetProgressState(clientSessionId);
2728
- await this.#announceStart(chatId, messageId, clientSessionId);
2729
3083
  await this.#onOutput({
2730
3084
  type: "user.message",
2731
3085
  clientSessionId,
@@ -2770,6 +3124,13 @@ var WecomIMAdapter = class {
2770
3124
  try {
2771
3125
  const target = parseWecomSessionId(event.clientSessionId);
2772
3126
  if (event.type !== "assistant.message") {
3127
+ const statusMarkdown = renderStatusMarkdown(event, this.#t);
3128
+ if (statusMarkdown) {
3129
+ const replyToMessageId2 = this.#lastInboundMessageIdBySession.get(event.clientSessionId);
3130
+ await this.#finishProgressMessage(target.chatId, event.clientSessionId, replyToMessageId2);
3131
+ await this.#client.sendText(target.chatId, statusMarkdown, replyToMessageId2);
3132
+ continue;
3133
+ }
2773
3134
  await this.#handleProgressEvent(target.chatId, event);
2774
3135
  continue;
2775
3136
  }
@@ -3243,19 +3604,22 @@ var WeixinIMAdapter = class {
3243
3604
  return;
3244
3605
  }
3245
3606
  const normalizedText = text2.trim();
3607
+ this.#resetProgressState(clientSessionId);
3608
+ await this.#client?.sendTyping(chatId);
3609
+ this.#startTypingHeartbeat(clientSessionId, chatId);
3246
3610
  const helpMarkdown = resolveHelpMarkdown(normalizedText, this.#t);
3247
3611
  if (helpMarkdown) {
3248
3612
  await this.#client?.sendText(chatId, helpMarkdown);
3613
+ this.#stopProgressTimer(clientSessionId);
3614
+ this.#stopTypingHeartbeat(clientSessionId);
3615
+ await this.#client?.stopTyping(chatId);
3249
3616
  return;
3250
3617
  }
3251
- this.#resetProgressState(clientSessionId);
3252
3618
  const commandEvent = parseSlashCommand(normalizedText, clientSessionId);
3253
3619
  if (commandEvent) {
3254
3620
  await this.#onOutput(commandEvent);
3255
3621
  return;
3256
3622
  }
3257
- await this.#client?.sendTyping(chatId);
3258
- this.#startTypingHeartbeat(clientSessionId, chatId);
3259
3623
  await this.#onOutput({
3260
3624
  type: "user.message",
3261
3625
  clientSessionId,
@@ -3311,6 +3675,14 @@ var WeixinIMAdapter = class {
3311
3675
  continue;
3312
3676
  }
3313
3677
  if (event.type !== "assistant.message") {
3678
+ const statusMarkdown = renderStatusMarkdown(event, this.#t);
3679
+ if (statusMarkdown) {
3680
+ await this.#sendTextWithProtection(target.chatId, statusMarkdown);
3681
+ this.#stopProgressTimer(event.clientSessionId);
3682
+ this.#stopTypingHeartbeat(event.clientSessionId);
3683
+ await this.#client.stopTyping(target.chatId);
3684
+ continue;
3685
+ }
3314
3686
  this.#recordProgressEvent(event);
3315
3687
  continue;
3316
3688
  }
package/dist/cli.js CHANGED
@@ -116,18 +116,33 @@ var resources = {
116
116
  progress: {
117
117
  noProgress: "No progress yet.",
118
118
  collapsed: "Collapsed {{count}} earlier updates.",
119
- running: "Running {{subject}}",
120
- finished: "Finished {{subject}}",
121
- failed: "Failed {{subject}}",
122
- failedWithDetail: "Failed {{subject}}: {{detail}}",
119
+ running: "\u23F3 {{subject}}",
120
+ finished: "\u2705 {{subject}}",
121
+ failed: "\u274C {{subject}}",
122
+ failedWithDetail: "\u274C {{subject}}: {{detail}}",
123
123
  compacting: "Compacting session",
124
124
  compactingWithDetail: "Compacting session: {{detail}}"
125
125
  },
126
126
  client: {
127
127
  processing: "Processing...",
128
- helpMessage: "Available commands:\n\n- `/new` (`/n`) - Start a new agent session\n- `/compact` (`/c`) - Compact the current session context\n- `/stop` (`/s`) - Stop the active agent run\n- `/help` (`/h`) - Show this help message",
128
+ helpMessage: "Available commands:\n\n- `/new` (`/n`) - Start a new agent session\n- `/compact` (`/c`) - Compact the current session context\n- `/stop` (`/s`) - Stop the active agent run\n- `/status` (`/st`) - Show the current agent session status\n- `/model` (`/m`) - List available models, or switch with `/model provider/modelId`\n- `/help` (`/h`) - Show this help message",
129
129
  messageDeliveryFailedTitle: "[agent-bridge error] Message delivery failed",
130
- weixinCooldown: "Weixin send is cooling down after rate limiting. Please try again shortly."
130
+ weixinCooldown: "Weixin send is cooling down after rate limiting. Please try again shortly.",
131
+ statusTitle: "Current session status",
132
+ statusSessionId: "Session ID",
133
+ statusModel: "Model",
134
+ statusThinkingLevel: "Thinking level",
135
+ statusContext: "Context",
136
+ statusUnavailable: "Current session status is unavailable.",
137
+ statusUnavailableValue: "Unavailable",
138
+ modelListTitle: "Available models",
139
+ modelListCurrent: "current",
140
+ modelListUsage: "Use `/model provider/modelId` to switch.",
141
+ modelListUnavailable: "Available models are unavailable for the current session.",
142
+ modelSetUnavailable: "Current session model switching is unavailable.",
143
+ modelInvalid: "The requested model is invalid or unavailable.",
144
+ modelBusy: "Current session is busy, so the model cannot be switched. Please use `/stop` first.",
145
+ modelUpdated: "Switched current model to `{{model}}`."
131
146
  },
132
147
  gateway: {
133
148
  noActiveSessionToCompact: "No active agent session to compact.",
@@ -143,18 +158,33 @@ var resources = {
143
158
  progress: {
144
159
  noProgress: "\u6682\u65E0\u8FDB\u5EA6\u3002",
145
160
  collapsed: "\u5DF2\u6298\u53E0 {{count}} \u6761\u8F83\u65E9\u66F4\u65B0\u3002",
146
- running: "\u6B63\u5728\u6267\u884C {{subject}}",
147
- finished: "\u5DF2\u5B8C\u6210 {{subject}}",
148
- failed: "{{subject}} \u6267\u884C\u5931\u8D25",
149
- failedWithDetail: "{{subject}} \u6267\u884C\u5931\u8D25\uFF1A{{detail}}",
161
+ running: "\u23F3 {{subject}}",
162
+ finished: "\u2705 {{subject}}",
163
+ failed: "\u274C {{subject}}",
164
+ failedWithDetail: "\u274C {{subject}}: {{detail}}",
150
165
  compacting: "\u6B63\u5728\u538B\u7F29\u4F1A\u8BDD",
151
166
  compactingWithDetail: "\u6B63\u5728\u538B\u7F29\u4F1A\u8BDD: {{detail}}"
152
167
  },
153
168
  client: {
154
169
  processing: "\u6B63\u5728\u5904\u7406\u4E2D...",
155
- helpMessage: "\u53EF\u7528\u547D\u4EE4\uFF1A\n\n- `/new` (`/n`) - \u5F00\u59CB\u4E00\u4E2A\u65B0\u4F1A\u8BDD\n- `/compact` (`/c`) - \u538B\u7F29\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\n- `/stop` (`/s`) - \u505C\u6B62\u5F53\u524D\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\n- `/help` (`/h`) - \u67E5\u770B\u8FD9\u6761\u5E2E\u52A9\u4FE1\u606F",
170
+ helpMessage: "\u53EF\u7528\u547D\u4EE4\uFF1A\n\n- `/new` (`/n`) - \u5F00\u59CB\u4E00\u4E2A\u65B0\u4F1A\u8BDD\n- `/compact` (`/c`) - \u538B\u7F29\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\n- `/stop` (`/s`) - \u505C\u6B62\u5F53\u524D\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\n- `/status` (`/st`) - \u67E5\u770B\u5F53\u524D\u667A\u80FD\u4F53\u4F1A\u8BDD\u72B6\u6001\n- `/model` (`/m`) - \u67E5\u770B\u53EF\u7528\u6A21\u578B\uFF0C\u6216\u4F7F\u7528 `/model provider/modelId` \u5207\u6362\u6A21\u578B\n- `/help` (`/h`) - \u67E5\u770B\u8FD9\u6761\u5E2E\u52A9\u4FE1\u606F",
156
171
  messageDeliveryFailedTitle: "[agent-bridge \u9519\u8BEF] \u6D88\u606F\u53D1\u9001\u5931\u8D25",
157
- weixinCooldown: "\u5FAE\u4FE1\u53D1\u9001\u56E0\u9650\u6D41\u5DF2\u8FDB\u5165\u51B7\u5374\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002"
172
+ weixinCooldown: "\u5FAE\u4FE1\u53D1\u9001\u56E0\u9650\u6D41\u5DF2\u8FDB\u5165\u51B7\u5374\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",
173
+ statusTitle: "\u5F53\u524D\u4F1A\u8BDD\u72B6\u6001",
174
+ statusSessionId: "Session ID",
175
+ statusModel: "\u6A21\u578B",
176
+ statusThinkingLevel: "\u601D\u8003\u7B49\u7EA7",
177
+ statusContext: "\u4E0A\u4E0B\u6587",
178
+ statusUnavailable: "\u5F53\u524D\u65E0\u6CD5\u83B7\u53D6\u4F1A\u8BDD\u72B6\u6001\u3002",
179
+ statusUnavailableValue: "\u6682\u4E0D\u53EF\u7528",
180
+ modelListTitle: "\u53EF\u7528\u6A21\u578B",
181
+ modelListCurrent: "\u5F53\u524D",
182
+ modelListUsage: "\u4F7F\u7528 `/model provider/modelId` \u5207\u6362\u6A21\u578B\u3002",
183
+ modelListUnavailable: "\u5F53\u524D\u4F1A\u8BDD\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\u53EF\u7528\u6A21\u578B\u5217\u8868\u3002",
184
+ modelSetUnavailable: "\u5F53\u524D\u4F1A\u8BDD\u6682\u65F6\u65E0\u6CD5\u5207\u6362\u6A21\u578B\u3002",
185
+ modelInvalid: "\u8BF7\u6C42\u7684\u6A21\u578B\u65E0\u6548\u6216\u5F53\u524D\u4E0D\u53EF\u7528\u3002",
186
+ modelBusy: "\u5F53\u524D\u6B63\u5728\u8FD0\u884C\uFF0C\u65E0\u6CD5\u5207\u6362\u6A21\u578B\u3002\u8BF7\u5148\u4F7F\u7528 `/stop`\u3002",
187
+ modelUpdated: "\u5F53\u524D\u6A21\u578B\u5DF2\u5207\u6362\u81F3 `{{model}}`\u3002"
158
188
  },
159
189
  gateway: {
160
190
  noActiveSessionToCompact: "\u5F53\u524D\u6CA1\u6709\u53EF\u538B\u7F29\u7684\u667A\u80FD\u4F53\u4F1A\u8BDD\u3002",
@@ -335,6 +365,18 @@ var GatewayCore = class {
335
365
  await this.#handleSessionStop(event.clientSessionId);
336
366
  return;
337
367
  }
368
+ if (event.type === "command.session.status") {
369
+ await this.#handleSessionStatus(event.clientSessionId);
370
+ return;
371
+ }
372
+ if (event.type === "command.session.model.list") {
373
+ await this.#handleSessionModelList(event.clientSessionId);
374
+ return;
375
+ }
376
+ if (event.type === "command.session.model.set") {
377
+ await this.#handleSessionModelSet(event.clientSessionId, event.target);
378
+ return;
379
+ }
338
380
  await this.#handleUserMessage(event.clientSessionId, event.text);
339
381
  }
340
382
  async #handleUserMessage(clientSessionId, text2) {
@@ -389,6 +431,123 @@ var GatewayCore = class {
389
431
  }
390
432
  await runtime.agentAdapter.abort();
391
433
  }
434
+ async #handleSessionStatus(clientSessionId) {
435
+ const runtime = await this.#getActiveRuntime(clientSessionId);
436
+ if (!runtime) {
437
+ await this.#deliverClientInput({
438
+ type: "error",
439
+ clientSessionId,
440
+ kind: "agent.status.unavailable"
441
+ });
442
+ return;
443
+ }
444
+ this.#touchRuntime(runtime);
445
+ if (!runtime.agentAdapter.getStatus) {
446
+ await this.#deliverClientInput({
447
+ type: "error",
448
+ clientSessionId,
449
+ kind: "agent.status.unavailable"
450
+ });
451
+ return;
452
+ }
453
+ try {
454
+ const status = await runtime.agentAdapter.getStatus();
455
+ await this.#deliverClientInput({
456
+ type: "agent.status.info",
457
+ clientSessionId,
458
+ status
459
+ });
460
+ } catch (error) {
461
+ const detail = error instanceof Error ? error.message : String(error);
462
+ await this.#deliverClientInput({
463
+ type: "error",
464
+ clientSessionId,
465
+ kind: "agent.status.unavailable",
466
+ detail
467
+ });
468
+ }
469
+ }
470
+ async #handleSessionModelList(clientSessionId) {
471
+ const runtime = await this.#getActiveRuntime(clientSessionId);
472
+ if (!runtime) {
473
+ await this.#deliverClientInput({
474
+ type: "error",
475
+ clientSessionId,
476
+ kind: "agent.model.list.unavailable"
477
+ });
478
+ return;
479
+ }
480
+ this.#touchRuntime(runtime);
481
+ if (!runtime.agentAdapter.getAvailableModels) {
482
+ await this.#deliverClientInput({
483
+ type: "error",
484
+ clientSessionId,
485
+ kind: "agent.model.list.unavailable"
486
+ });
487
+ return;
488
+ }
489
+ try {
490
+ const models = await runtime.agentAdapter.getAvailableModels();
491
+ await this.#deliverClientInput({
492
+ type: "agent.model.list",
493
+ clientSessionId,
494
+ models
495
+ });
496
+ } catch (error) {
497
+ const detail = error instanceof Error ? error.message : String(error);
498
+ await this.#deliverClientInput({
499
+ type: "error",
500
+ clientSessionId,
501
+ kind: "agent.model.list.unavailable",
502
+ detail
503
+ });
504
+ }
505
+ }
506
+ async #handleSessionModelSet(clientSessionId, target) {
507
+ const runtime = await this.#getActiveRuntime(clientSessionId);
508
+ if (!runtime) {
509
+ await this.#deliverClientInput({
510
+ type: "error",
511
+ clientSessionId,
512
+ kind: "agent.model.set.unavailable"
513
+ });
514
+ return;
515
+ }
516
+ this.#touchRuntime(runtime);
517
+ if (!runtime.agentAdapter.setModel) {
518
+ await this.#deliverClientInput({
519
+ type: "error",
520
+ clientSessionId,
521
+ kind: "agent.model.set.unavailable"
522
+ });
523
+ return;
524
+ }
525
+ if (await runtime.agentAdapter.isBusy()) {
526
+ await this.#deliverClientInput({
527
+ type: "error",
528
+ clientSessionId,
529
+ kind: "agent.model.busy"
530
+ });
531
+ return;
532
+ }
533
+ try {
534
+ const result = await runtime.agentAdapter.setModel(target);
535
+ await this.#deliverClientInput({
536
+ type: "agent.model.updated",
537
+ clientSessionId,
538
+ provider: result.provider,
539
+ modelId: result.modelId
540
+ });
541
+ } catch (error) {
542
+ const { kind, detail } = this.#resolveModelCommandError(error);
543
+ await this.#deliverClientInput({
544
+ type: "error",
545
+ clientSessionId,
546
+ kind,
547
+ ...detail ? { detail } : {}
548
+ });
549
+ }
550
+ }
392
551
  async #handleSessionNew(clientSessionId) {
393
552
  const previousAgentSessionId = this.#clientToAgentSession.get(clientSessionId);
394
553
  if (previousAgentSessionId) {
@@ -482,6 +641,18 @@ var GatewayCore = class {
482
641
  this.#logger.error("failed to persist session bindings:", error);
483
642
  }
484
643
  }
644
+ #resolveModelCommandError(error) {
645
+ const detail = error instanceof Error ? error.message : String(error);
646
+ if (typeof error === "object" && error && "kind" in error && typeof error.kind === "string") {
647
+ switch (error.kind) {
648
+ case "agent.model.invalid":
649
+ case "agent.model.busy":
650
+ case "agent.model.set.unavailable":
651
+ return { kind: error.kind, ...detail ? { detail } : {} };
652
+ }
653
+ }
654
+ return { kind: "agent.model.set.unavailable", ...detail ? { detail } : {} };
655
+ }
485
656
  async #handleAgentOutput(event) {
486
657
  const agentSessionId = event.agentSessionId;
487
658
  const runtime = this.#agentRuntimes.get(agentSessionId);
@@ -884,6 +1055,19 @@ var PiRpcClient = class {
884
1055
  const response = await this.#send({ type: "get_state" });
885
1056
  return response.data ?? {};
886
1057
  }
1058
+ async getSessionStats() {
1059
+ const response = await this.#send({ type: "get_session_stats" });
1060
+ return response.data ?? {};
1061
+ }
1062
+ async getAvailableModels() {
1063
+ const response = await this.#send({ type: "get_available_models" });
1064
+ const data = response.data;
1065
+ return data?.models ?? [];
1066
+ }
1067
+ async setModel(provider, modelId) {
1068
+ const response = await this.#send({ type: "set_model", provider, modelId });
1069
+ return response.data ?? {};
1070
+ }
887
1071
  async setSessionName(name) {
888
1072
  await this.#send({ type: "set_session_name", name });
889
1073
  }
@@ -990,6 +1174,13 @@ function toPiSessionId(agentSessionId) {
990
1174
  }
991
1175
 
992
1176
  // src/modules/agent/pi-coding-agent/adapter/pi-coding-agent-adapter.ts
1177
+ var PiModelCommandError = class extends Error {
1178
+ kind;
1179
+ constructor(kind, message) {
1180
+ super(message);
1181
+ this.kind = kind;
1182
+ }
1183
+ };
993
1184
  var PiCodingAgentAdapter = class {
994
1185
  #agentSessionId;
995
1186
  #piSessionId;
@@ -1072,6 +1263,71 @@ var PiCodingAgentAdapter = class {
1072
1263
  async isBusy() {
1073
1264
  return this.#processing || this.#inputQueue.length > 0;
1074
1265
  }
1266
+ async getStatus() {
1267
+ if (!this.#client) {
1268
+ throw new Error("PiCodingAgentAdapter is not started");
1269
+ }
1270
+ const [state, stats] = await Promise.all([this.#client.getState(), this.#client.getSessionStats()]);
1271
+ const contextUsage = stats.contextUsage;
1272
+ return {
1273
+ sessionId: state.sessionId ?? this.#agentSessionId,
1274
+ provider: state.model?.provider,
1275
+ modelId: state.model?.id,
1276
+ thinkingLevel: state.thinkingLevel,
1277
+ context: contextUsage ? {
1278
+ tokens: contextUsage.tokens ?? null,
1279
+ contextWindow: contextUsage.contextWindow ?? null,
1280
+ percent: contextUsage.percent ?? null
1281
+ } : void 0
1282
+ };
1283
+ }
1284
+ async getAvailableModels() {
1285
+ if (!this.#client) {
1286
+ throw new Error("PiCodingAgentAdapter is not started");
1287
+ }
1288
+ const [state, models] = await Promise.all([this.#client.getState(), this.#client.getAvailableModels()]);
1289
+ return models.filter((model) => typeof model.provider === "string" && typeof model.id === "string").map((model) => ({
1290
+ provider: model.provider,
1291
+ modelId: model.id,
1292
+ isCurrent: state.model?.provider === model.provider && state.model?.id === model.id
1293
+ }));
1294
+ }
1295
+ async setModel(target) {
1296
+ if (!this.#client) {
1297
+ throw new PiModelCommandError("agent.model.set.unavailable", "PiCodingAgentAdapter is not started");
1298
+ }
1299
+ const parsed = this.#parseModelTarget(target);
1300
+ const state = await this.#client.getState();
1301
+ if (state.isStreaming || state.isCompacting) {
1302
+ throw new PiModelCommandError("agent.model.busy", "Current session is busy, so the model cannot be switched. Please use /stop first.");
1303
+ }
1304
+ try {
1305
+ const result = await this.#client.setModel(parsed.provider, parsed.modelId);
1306
+ return {
1307
+ provider: result.provider ?? parsed.provider,
1308
+ modelId: result.id ?? parsed.modelId
1309
+ };
1310
+ } catch (error) {
1311
+ const message = error instanceof Error ? error.message : String(error);
1312
+ if (/Model not found:/i.test(message)) {
1313
+ throw new PiModelCommandError("agent.model.invalid", message);
1314
+ }
1315
+ throw new PiModelCommandError("agent.model.set.unavailable", message);
1316
+ }
1317
+ }
1318
+ #parseModelTarget(target) {
1319
+ const trimmed = target.trim();
1320
+ const slashIndex = trimmed.indexOf("/");
1321
+ if (slashIndex <= 0 || slashIndex === trimmed.length - 1) {
1322
+ throw new PiModelCommandError("agent.model.invalid", `Invalid model target: ${target}`);
1323
+ }
1324
+ const provider = trimmed.slice(0, slashIndex).trim();
1325
+ const modelId = trimmed.slice(slashIndex + 1).trim();
1326
+ if (!provider || !modelId) {
1327
+ throw new PiModelCommandError("agent.model.invalid", `Invalid model target: ${target}`);
1328
+ }
1329
+ return { provider, modelId };
1330
+ }
1075
1331
  async #drainInputQueue() {
1076
1332
  if (this.#processing) {
1077
1333
  return;
@@ -1183,7 +1439,7 @@ var PiCodingAgentAdapter = class {
1183
1439
  toolCallId,
1184
1440
  toolInput,
1185
1441
  toolLabel,
1186
- text: `Running ${toolName}`
1442
+ text: void 0
1187
1443
  });
1188
1444
  return;
1189
1445
  }
@@ -1204,7 +1460,7 @@ var PiCodingAgentAdapter = class {
1204
1460
  toolInput,
1205
1461
  toolLabel,
1206
1462
  partialResult: "partialResult" in rpcEvent ? rpcEvent.partialResult : void 0,
1207
- text: `Running ${toolName}`
1463
+ text: void 0
1208
1464
  });
1209
1465
  return;
1210
1466
  }
@@ -1222,7 +1478,7 @@ var PiCodingAgentAdapter = class {
1222
1478
  toolInput,
1223
1479
  toolLabel,
1224
1480
  result: "result" in rpcEvent ? rpcEvent.result : void 0,
1225
- text: isError ? void 0 : `Finished ${toolName}`
1481
+ text: void 0
1226
1482
  });
1227
1483
  if (toolCallId) {
1228
1484
  this.#toolLabelByCallId.delete(toolCallId);
@@ -1419,7 +1675,7 @@ function getTypedAgentModule(config) {
1419
1675
 
1420
1676
  // src/modules/client/utils/progress-renderer.ts
1421
1677
  var DEFAULT_COLLAPSE_THRESHOLD = 10;
1422
- var MAX_TOOL_LABEL_DISPLAY_LENGTH = 15;
1678
+ var MAX_TOOL_LABEL_DISPLAY_LENGTH = 24;
1423
1679
  var ProgressRenderer = class {
1424
1680
  #collapseThreshold;
1425
1681
  #t;
@@ -1531,11 +1787,6 @@ var ProgressRenderer = class {
1531
1787
  if (!normalizedText) {
1532
1788
  return `- ${this.#humanizeToolError(toolName)}`;
1533
1789
  }
1534
- const lowerText = normalizedText.toLowerCase();
1535
- const lowerToolName = toolName.toLowerCase();
1536
- if (lowerText === lowerToolName || lowerText === this.#t("progress.failed", { subject: toolName }).toLowerCase() || lowerText === this.#t("progress.running", { subject: toolName }).toLowerCase() || lowerText === this.#t("progress.finished", { subject: toolName }).toLowerCase()) {
1537
- return `- ${this.#humanizeToolError(toolName)}`;
1538
- }
1539
1790
  return `- ${this.#t("progress.failedWithDetail", { subject: toolName, detail: normalizedText })}`;
1540
1791
  }
1541
1792
  #humanizeToolError(toolName) {
@@ -1566,7 +1817,26 @@ function isHelpCommand(text2) {
1566
1817
  function resolveHelpMarkdown(text2, t) {
1567
1818
  return isHelpCommand(text2) ? t("client.helpMessage") : null;
1568
1819
  }
1820
+ function parseModelCommand(text2, clientSessionId) {
1821
+ const match = text2.match(/^\/(model|m)(?:\s+(.*))?$/i);
1822
+ if (!match) {
1823
+ return null;
1824
+ }
1825
+ const target = match[2]?.trim();
1826
+ if (!target) {
1827
+ return { type: "command.session.model.list", clientSessionId };
1828
+ }
1829
+ return {
1830
+ type: "command.session.model.set",
1831
+ clientSessionId,
1832
+ target
1833
+ };
1834
+ }
1569
1835
  function parseSlashCommand(text2, clientSessionId) {
1836
+ const modelCommand = parseModelCommand(text2, clientSessionId);
1837
+ if (modelCommand) {
1838
+ return modelCommand;
1839
+ }
1570
1840
  switch (text2.toLowerCase()) {
1571
1841
  case "/new":
1572
1842
  case "/n":
@@ -1577,11 +1847,87 @@ function parseSlashCommand(text2, clientSessionId) {
1577
1847
  case "/stop":
1578
1848
  case "/s":
1579
1849
  return { type: "command.session.stop", clientSessionId };
1850
+ case "/status":
1851
+ case "/st":
1852
+ return { type: "command.session.status", clientSessionId };
1580
1853
  default:
1581
1854
  return null;
1582
1855
  }
1583
1856
  }
1584
1857
 
1858
+ // src/modules/client/utils/status-markdown.ts
1859
+ function formatModel(status, t) {
1860
+ if (status.provider && status.modelId) {
1861
+ return `\`${status.provider}/${status.modelId}\``;
1862
+ }
1863
+ if (status.modelId) {
1864
+ return `\`${status.modelId}\``;
1865
+ }
1866
+ if (status.provider) {
1867
+ return `\`${status.provider}\``;
1868
+ }
1869
+ return t("client.statusUnavailableValue");
1870
+ }
1871
+ function formatContext(status, t) {
1872
+ const context = status.context;
1873
+ if (!context) {
1874
+ return t("client.statusUnavailableValue");
1875
+ }
1876
+ const { tokens, contextWindow, percent } = context;
1877
+ if (tokens == null || contextWindow == null || percent == null) {
1878
+ return t("client.statusUnavailableValue");
1879
+ }
1880
+ return `\`${tokens.toLocaleString()} / ${contextWindow.toLocaleString()} (${percent}%)\``;
1881
+ }
1882
+ function formatAvailableModel(model, t) {
1883
+ const label = `\`${model.provider}/${model.modelId}\``;
1884
+ return model.isCurrent ? `- ${label} \u2705 ${t("client.modelListCurrent")}` : `- ${label}`;
1885
+ }
1886
+ function formatErrorMarkdown(title, detail) {
1887
+ return [`**${title}**`, ...detail ? ["", detail] : []].join("\n");
1888
+ }
1889
+ function renderStatusMarkdown(event, t) {
1890
+ if (event.type === "agent.model.list") {
1891
+ return [
1892
+ `**${t("client.modelListTitle")}**`,
1893
+ "",
1894
+ ...event.models.length > 0 ? event.models.map((model) => formatAvailableModel(model, t)) : [`- ${t("client.statusUnavailableValue")}`],
1895
+ "",
1896
+ t("client.modelListUsage")
1897
+ ].join("\n");
1898
+ }
1899
+ if (event.type === "agent.model.updated") {
1900
+ return t("client.modelUpdated", { model: `${event.provider}/${event.modelId}` });
1901
+ }
1902
+ if (event.type === "agent.status.info") {
1903
+ return [
1904
+ `**${t("client.statusTitle")}**`,
1905
+ "",
1906
+ `- ${t("client.statusSessionId")}: \`${event.status.sessionId}\``,
1907
+ `- ${t("client.statusModel")}: ${formatModel(event.status, t)}`,
1908
+ `- ${t("client.statusThinkingLevel")}: ${event.status.thinkingLevel ? `\`${event.status.thinkingLevel}\`` : t("client.statusUnavailableValue")}`,
1909
+ `- ${t("client.statusContext")}: ${formatContext(event.status, t)}`
1910
+ ].join("\n");
1911
+ }
1912
+ if (event.type === "error") {
1913
+ switch (event.kind) {
1914
+ case "agent.status.unavailable":
1915
+ return formatErrorMarkdown(t("client.statusUnavailable"), event.detail);
1916
+ case "agent.model.list.unavailable":
1917
+ return formatErrorMarkdown(t("client.modelListUnavailable"), event.detail);
1918
+ case "agent.model.set.unavailable":
1919
+ return formatErrorMarkdown(t("client.modelSetUnavailable"), event.detail);
1920
+ case "agent.model.invalid":
1921
+ return formatErrorMarkdown(t("client.modelInvalid"), event.detail);
1922
+ case "agent.model.busy":
1923
+ return event.detail ?? t("client.modelBusy");
1924
+ default:
1925
+ return null;
1926
+ }
1927
+ }
1928
+ return null;
1929
+ }
1930
+
1585
1931
  // src/modules/client/feishu/adapter/feishu-client.ts
1586
1932
  import * as Lark from "@larksuiteoapi/node-sdk";
1587
1933
  import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
@@ -1981,10 +2327,14 @@ var FeishuIMAdapter = class _FeishuIMAdapter {
1981
2327
  return;
1982
2328
  }
1983
2329
  const normalizedText = text2.trim();
2330
+ this.#lastInboundMessageIdBySession.set(clientSessionId, messageId);
2331
+ this.#resetProgressState(clientSessionId);
2332
+ await this.#client?.startTyping(chatId, messageId);
1984
2333
  const helpMarkdown = resolveHelpMarkdown(normalizedText, this.#t);
1985
2334
  if (helpMarkdown) {
1986
2335
  this.#logger.info(`received local help command ${normalizedText} (session=${clientSessionId})`);
1987
2336
  await this.#client?.sendText(chatId, helpMarkdown, messageId);
2337
+ await this.#client?.stopTyping(chatId);
1988
2338
  return;
1989
2339
  }
1990
2340
  const commandEvent = parseSlashCommand(normalizedText, clientSessionId);
@@ -1993,9 +2343,6 @@ var FeishuIMAdapter = class _FeishuIMAdapter {
1993
2343
  await this.#onOutput(commandEvent);
1994
2344
  return;
1995
2345
  }
1996
- this.#lastInboundMessageIdBySession.set(clientSessionId, messageId);
1997
- this.#resetProgressState(clientSessionId);
1998
- await this.#client?.startTyping(chatId, messageId);
1999
2346
  this.#logger.info(`received user message (session=${clientSessionId}): ${normalizedText}`);
2000
2347
  await this.#onOutput({
2001
2348
  type: "user.message",
@@ -2041,6 +2388,13 @@ var FeishuIMAdapter = class _FeishuIMAdapter {
2041
2388
  try {
2042
2389
  const target = parseFeishuSessionId(event.clientSessionId);
2043
2390
  if (event.type !== "assistant.message") {
2391
+ const statusMarkdown = renderStatusMarkdown(event, this.#t);
2392
+ if (statusMarkdown) {
2393
+ const replyToMessageId2 = this.#lastInboundMessageIdBySession.get(event.clientSessionId);
2394
+ await this.#client.sendText(target.chatId, statusMarkdown, replyToMessageId2);
2395
+ await this.#client.stopTyping(target.chatId);
2396
+ continue;
2397
+ }
2044
2398
  await this.#handleProgressEvent(target.chatId, event);
2045
2399
  continue;
2046
2400
  }
@@ -2718,14 +3072,14 @@ var WecomIMAdapter = class {
2718
3072
  await this.#client?.sendText(chatId, helpMarkdown, messageId);
2719
3073
  return;
2720
3074
  }
3075
+ this.#lastInboundMessageIdBySession.set(clientSessionId, messageId);
3076
+ this.#resetProgressState(clientSessionId);
3077
+ await this.#announceStart(chatId, messageId, clientSessionId);
2721
3078
  const commandEvent = parseSlashCommand(normalizedText, clientSessionId);
2722
3079
  if (commandEvent) {
2723
3080
  await this.#onOutput(commandEvent);
2724
3081
  return;
2725
3082
  }
2726
- this.#lastInboundMessageIdBySession.set(clientSessionId, messageId);
2727
- this.#resetProgressState(clientSessionId);
2728
- await this.#announceStart(chatId, messageId, clientSessionId);
2729
3083
  await this.#onOutput({
2730
3084
  type: "user.message",
2731
3085
  clientSessionId,
@@ -2770,6 +3124,13 @@ var WecomIMAdapter = class {
2770
3124
  try {
2771
3125
  const target = parseWecomSessionId(event.clientSessionId);
2772
3126
  if (event.type !== "assistant.message") {
3127
+ const statusMarkdown = renderStatusMarkdown(event, this.#t);
3128
+ if (statusMarkdown) {
3129
+ const replyToMessageId2 = this.#lastInboundMessageIdBySession.get(event.clientSessionId);
3130
+ await this.#finishProgressMessage(target.chatId, event.clientSessionId, replyToMessageId2);
3131
+ await this.#client.sendText(target.chatId, statusMarkdown, replyToMessageId2);
3132
+ continue;
3133
+ }
2773
3134
  await this.#handleProgressEvent(target.chatId, event);
2774
3135
  continue;
2775
3136
  }
@@ -3243,19 +3604,22 @@ var WeixinIMAdapter = class {
3243
3604
  return;
3244
3605
  }
3245
3606
  const normalizedText = text2.trim();
3607
+ this.#resetProgressState(clientSessionId);
3608
+ await this.#client?.sendTyping(chatId);
3609
+ this.#startTypingHeartbeat(clientSessionId, chatId);
3246
3610
  const helpMarkdown = resolveHelpMarkdown(normalizedText, this.#t);
3247
3611
  if (helpMarkdown) {
3248
3612
  await this.#client?.sendText(chatId, helpMarkdown);
3613
+ this.#stopProgressTimer(clientSessionId);
3614
+ this.#stopTypingHeartbeat(clientSessionId);
3615
+ await this.#client?.stopTyping(chatId);
3249
3616
  return;
3250
3617
  }
3251
- this.#resetProgressState(clientSessionId);
3252
3618
  const commandEvent = parseSlashCommand(normalizedText, clientSessionId);
3253
3619
  if (commandEvent) {
3254
3620
  await this.#onOutput(commandEvent);
3255
3621
  return;
3256
3622
  }
3257
- await this.#client?.sendTyping(chatId);
3258
- this.#startTypingHeartbeat(clientSessionId, chatId);
3259
3623
  await this.#onOutput({
3260
3624
  type: "user.message",
3261
3625
  clientSessionId,
@@ -3311,6 +3675,14 @@ var WeixinIMAdapter = class {
3311
3675
  continue;
3312
3676
  }
3313
3677
  if (event.type !== "assistant.message") {
3678
+ const statusMarkdown = renderStatusMarkdown(event, this.#t);
3679
+ if (statusMarkdown) {
3680
+ await this.#sendTextWithProtection(target.chatId, statusMarkdown);
3681
+ this.#stopProgressTimer(event.clientSessionId);
3682
+ this.#stopTypingHeartbeat(event.clientSessionId);
3683
+ await this.#client.stopTyping(target.chatId);
3684
+ continue;
3685
+ }
3314
3686
  this.#recordProgressEvent(event);
3315
3687
  continue;
3316
3688
  }
package/dist/index.d.ts CHANGED
@@ -11,6 +11,16 @@ type ClientOutputEvent = {
11
11
  } | {
12
12
  type: "command.session.stop";
13
13
  clientSessionId: string;
14
+ } | {
15
+ type: "command.session.status";
16
+ clientSessionId: string;
17
+ } | {
18
+ type: "command.session.model.list";
19
+ clientSessionId: string;
20
+ } | {
21
+ type: "command.session.model.set";
22
+ clientSessionId: string;
23
+ target: string;
14
24
  };
15
25
  type AgentInputEvent = {
16
26
  type: "user.message";
@@ -24,6 +34,22 @@ interface OutboundAttachment {
24
34
  fileName?: string;
25
35
  caption?: string;
26
36
  }
37
+ interface AgentSessionStatus {
38
+ sessionId: string;
39
+ provider?: string;
40
+ modelId?: string;
41
+ thinkingLevel?: string;
42
+ context?: {
43
+ tokens: number | null;
44
+ contextWindow: number | null;
45
+ percent: number | null;
46
+ };
47
+ }
48
+ interface AgentAvailableModel {
49
+ provider: string;
50
+ modelId: string;
51
+ isCurrent: boolean;
52
+ }
27
53
  type ToolProgressPayload = {
28
54
  toolName: string;
29
55
  toolCallId?: string;
@@ -38,6 +64,20 @@ type AgentOutputPayload = {
38
64
  } | {
39
65
  type: "assistant.thinking";
40
66
  text?: string;
67
+ } | {
68
+ type: "agent.status.info";
69
+ status: AgentSessionStatus;
70
+ } | {
71
+ type: "agent.model.list";
72
+ models: AgentAvailableModel[];
73
+ } | {
74
+ type: "agent.model.updated";
75
+ provider: string;
76
+ modelId: string;
77
+ } | {
78
+ type: "error";
79
+ kind: string;
80
+ detail?: string;
41
81
  } | ({
42
82
  type: "assistant.tool.running";
43
83
  } & ToolProgressPayload) | ({
@@ -70,6 +110,12 @@ interface AgentAdapter {
70
110
  start(onOutput: (event: AgentOutputEvent) => Promise<void> | void): Promise<void>;
71
111
  stop(): Promise<void>;
72
112
  abort?(): Promise<void>;
113
+ getStatus?(): Promise<AgentSessionStatus>;
114
+ getAvailableModels?(): Promise<AgentAvailableModel[]>;
115
+ setModel?(target: string): Promise<{
116
+ provider: string;
117
+ modelId: string;
118
+ }>;
73
119
  input(event: AgentInputEvent): Promise<void>;
74
120
  isBusy(): Promise<boolean>;
75
121
  }
@@ -222,4 +268,4 @@ interface WeixinInboundMessage {
222
268
  raw?: unknown;
223
269
  }
224
270
 
225
- export type { AgentAdapter, AgentConfig, AgentInputEvent, AgentModule, AgentOutputEvent, AppConfig, AppDefaults, ChannelCommonConfig, ChannelCommonContext, ChannelConfig, ChannelRunner, ClientConfig, ClientInputEvent, ClientModule, ClientOutputEvent, ConfigAdapter, ConfigCollectContext, ConfigInputOptions, ConfigSelectOption, FeishuClientConfig, FeishuInboundMessage, GatewayCoreOptions, IMAdapter, LegacyAgentInputEvent, LocaleCode, OutboundAttachment, PiCodingAgentConfig, RunChannelOptions, SessionBindingStore, WecomClientConfig, WecomInboundMessage, WeixinClientConfig, WeixinInboundMessage };
271
+ export type { AgentAdapter, AgentAvailableModel, AgentConfig, AgentInputEvent, AgentModule, AgentOutputEvent, AgentSessionStatus, AppConfig, AppDefaults, ChannelCommonConfig, ChannelCommonContext, ChannelConfig, ChannelRunner, ClientConfig, ClientInputEvent, ClientModule, ClientOutputEvent, ConfigAdapter, ConfigCollectContext, ConfigInputOptions, ConfigSelectOption, FeishuClientConfig, FeishuInboundMessage, GatewayCoreOptions, IMAdapter, LegacyAgentInputEvent, LocaleCode, OutboundAttachment, PiCodingAgentConfig, RunChannelOptions, SessionBindingStore, WecomClientConfig, WecomInboundMessage, WeixinClientConfig, WeixinInboundMessage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hopgoldy/agent-bridge",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "IM to Agent bridge with dual-adapter architecture",
6
6
  "packageManager": "pnpm@10.33.0",