@vicvic-im/agent-bot 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/README.md CHANGED
@@ -75,10 +75,17 @@ Chat commands:
75
75
  | `/agent [name]` | List or switch the agent backend |
76
76
  | `/model [name]` | Show or switch the model (if the agent exposes it) |
77
77
  | `/cost` | Show context usage and optional cost |
78
+ | `/version` | Show the vbot version |
78
79
  | `/help` | Show commands |
79
80
 
80
81
  Unknown `/commands` are forwarded to the agent as prompts.
81
82
 
83
+ Output notes:
84
+
85
+ - `/status`, `/agent`, `/model`, `/cost` and `/help` render Markdown tables; the vicvic.im Web and RN clients already render GFM tables. `/status` starts with a one-line summary because conversation previews and push notifications only show the first line.
86
+ - `/model` with no argument reports the model the ACP session actually selected (`currentValue` mapped to its option name) and lists the available options. It shows `unknown (session not started)` before the first message instead of a misleading `default`.
87
+ - `/new` and `/model <name>` are rejected while a request is running (`Use /stop first`): resetting the session mid-turn drops the streamed reply and switches the model under a running turn.
88
+
82
89
  ## Configuration
83
90
 
84
91
  `vbot.json` in the current directory, or `~/.vbot/config.json` as a fallback:
@@ -119,13 +126,12 @@ The pairing token is a long-lived credential. Do not commit `vbot.json`; the fil
119
126
  - Session state is persisted; restarting `vbot` can resume the ACP session when the backend supports it.
120
127
  - `/cost` reports ACP `usage_update` values (`used` / `size` / optional `cost`), not Claude's dollar cost.
121
128
  - New config file (`vbot.json`) and pm2 process names (`vbot-*`); it does not read or migrate `ccbot.json`.
122
- - The App's onboarding still says `npm i -g @ccbot/cli`; this is a known gap for now.
129
+ - The App onboarding now points to `npm i -g @vicvic-im/agent-bot` and `vbot start`.
123
130
 
124
131
  ## Known limitations
125
132
 
126
133
  - No streaming/progress messages yet: only `Processing...` and the final reply.
127
134
  - Text only. Image and file messages are not supported by the vicvic.im bot conversation.
128
- - The server's offline notice still mentions `ccbot` (the server is unchanged in this phase).
129
135
  - Windows is not officially verified; the adapters and pm2 may need extra setup.
130
136
  - Chat CLI messages are English, matching the existing `@ccbot/cli` developer tooling; App UI i18n is unaffected.
131
137
 
@@ -1,4 +1,4 @@
1
- import type { AgentBackend, AgentCapabilities, AgentName, AgentRunRequest, AgentRunResult } from "./types.js";
1
+ import type { AgentBackend, AgentCapabilities, AgentModelState, AgentName, AgentRunRequest, AgentRunResult } from "./types.js";
2
2
  import type { AgentConfig } from "../config.js";
3
3
  export declare class AgentAbortError extends Error {
4
4
  constructor();
@@ -25,6 +25,7 @@ export declare class AcpAgent implements AgentBackend {
25
25
  constructor(name: AgentName, config: AgentConfig);
26
26
  capabilities(): AgentCapabilities;
27
27
  sessionId(): string | null;
28
+ currentModel(): AgentModelState | null;
28
29
  resetSession(): void;
29
30
  start(): Promise<void>;
30
31
  private doStart;
@@ -1 +1 @@
1
- {"version":3,"file":"acp.d.ts","sourceRoot":"","sources":["../../src/agents/acp.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,eAAe,EACf,cAAc,EAEf,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,qBAAa,eAAgB,SAAQ,KAAK;;CAKzC;AAED,qBAAa,eAAgB,SAAQ,KAAK;IAGtC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBADvB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM;CAK1B;AAWD,0CAA0C;AAC1C,qBAAa,QAAS,YAAW,YAAY;IAczC,QAAQ,CAAC,IAAI,EAAE,SAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAdzB,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,UAAU,CAAqC;IACvD,OAAO,CAAC,GAAG,CAAkC;IAC7C,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAuB;gBAGhC,IAAI,EAAE,SAAS,EACP,MAAM,EAAE,WAAW;IAGtC,YAAY,IAAI,iBAAiB;IASjC,SAAS,IAAI,MAAM,GAAG,IAAI;IAI1B,YAAY,IAAI,IAAI;IAId,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YASd,OAAO;YA8EP,aAAa;IA+CrB,GAAG,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;IAkEtD,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IASvB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkBzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB5B,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,UAAU;CAWnB"}
1
+ {"version":3,"file":"acp.d.ts","sourceRoot":"","sources":["../../src/agents/acp.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EAEjB,eAAe,EACf,SAAS,EACT,eAAe,EACf,cAAc,EAEf,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,qBAAa,eAAgB,SAAQ,KAAK;;CAKzC;AAED,qBAAa,eAAgB,SAAQ,KAAK;IAGtC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBADvB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM;CAK1B;AAyBD,0CAA0C;AAC1C,qBAAa,QAAS,YAAW,YAAY;IAczC,QAAQ,CAAC,IAAI,EAAE,SAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAdzB,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,UAAU,CAAqC;IACvD,OAAO,CAAC,GAAG,CAAkC;IAC7C,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAuB;gBAGhC,IAAI,EAAE,SAAS,EACP,MAAM,EAAE,WAAW;IAGtC,YAAY,IAAI,iBAAiB;IASjC,SAAS,IAAI,MAAM,GAAG,IAAI;IAI1B,YAAY,IAAI,eAAe,GAAG,IAAI;IAStC,YAAY,IAAI,IAAI;IAId,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YASd,OAAO;YA8EP,aAAa;IA+CrB,GAAG,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;IAkEtD,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IASvB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmBzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB5B,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,UAAU;CAWnB"}
@@ -45,6 +45,20 @@ export interface AgentRunResult {
45
45
  /** true 表示本次是新会话(用于提示用户上下文已重置)。 */
46
46
  newSession: boolean;
47
47
  }
48
+ /** 一个可选的模型(ACP session config 的 select 值)。 */
49
+ export interface AgentModelOption {
50
+ value: string;
51
+ label: string;
52
+ }
53
+ /** 当前模型状态,从 ACP session config 推导;未建立会话时为 null。 */
54
+ export interface AgentModelState {
55
+ /** 当前生效模型的 ACP 值(/model 切换时使用)。 */
56
+ value: string;
57
+ /** 当前生效模型的可读名(优先 option.name,回退 value)。 */
58
+ label: string;
59
+ /** 可选模型列表;后端不暴露时为空数组。 */
60
+ options: AgentModelOption[];
61
+ }
48
62
  export interface AgentCapabilities {
49
63
  resume: boolean;
50
64
  load: boolean;
@@ -68,5 +82,7 @@ export interface AgentBackend {
68
82
  capabilities(): AgentCapabilities;
69
83
  /** 当前 ACP session id(可能为空)。 */
70
84
  sessionId(): string | null;
85
+ /** 当前模型;后端未暴露模型配置项或会话未建立时返回 null。 */
86
+ currentModel(): AgentModelState | null;
71
87
  }
72
88
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/agents/types.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,IAAI,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,KAAK;IACpB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACnD;AAED,eAAO,MAAM,WAAW,EAAE,KAAwC,CAAC;AAEnE,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAEpC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,mCAAmC;IACnC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,2CAA2C;IAC3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,sCAAsC;IACtC,GAAG,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACvD,mBAAmB;IACnB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,+BAA+B;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,sCAAsC;IACtC,YAAY,IAAI,IAAI,CAAC;IACrB,kBAAkB;IAClB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,YAAY,IAAI,iBAAiB,CAAC;IAClC,+BAA+B;IAC/B,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC;CAC5B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/agents/types.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,IAAI,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,KAAK;IACpB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACnD;AAED,eAAO,MAAM,WAAW,EAAE,KAAwC,CAAC;AAEnE,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAEpC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,mCAAmC;IACnC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,2CAA2C;IAC3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,sCAAsC;IACtC,GAAG,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACvD,mBAAmB;IACnB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,+BAA+B;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,sCAAsC;IACtC,YAAY,IAAI,IAAI,CAAC;IACrB,kBAAkB;IAClB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,YAAY,IAAI,iBAAiB,CAAC;IAClC,+BAA+B;IAC/B,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC;IAC3B,qCAAqC;IACrC,YAAY,IAAI,eAAe,GAAG,IAAI,CAAC;CACxC"}
@@ -363,6 +363,11 @@ function releaseLock(token) {
363
363
  var import_node_child_process = require("child_process");
364
364
  var import_node_stream = require("stream");
365
365
  var acp = __toESM(require("@agentclientprotocol/sdk"), 1);
366
+
367
+ // src/version.ts
368
+ var VBOT_VERSION = "0.1.4";
369
+
370
+ // src/agents/acp.ts
366
371
  var AgentAbortError = class extends Error {
367
372
  constructor() {
368
373
  super("Aborted");
@@ -378,6 +383,14 @@ var AgentStartError = class extends Error {
378
383
  stderr;
379
384
  };
380
385
  var STDERR_LIMIT = 4e3;
386
+ function flattenSelectOptions(options) {
387
+ return options.flatMap(
388
+ (entry) => "group" in entry ? entry.options.map((option) => ({ value: option.value, label: option.name })) : [{ value: entry.value, label: entry.name }]
389
+ );
390
+ }
391
+ function isModelOption(option) {
392
+ return option.type === "select" && (option.category === "model" || option.id === "model");
393
+ }
381
394
  var AcpAgent = class {
382
395
  constructor(name, config) {
383
396
  this.name = name;
@@ -407,6 +420,14 @@ var AcpAgent = class {
407
420
  sessionId() {
408
421
  return this.session;
409
422
  }
423
+ currentModel() {
424
+ const option = this.configOptions.find(isModelOption);
425
+ if (!option || option.type !== "select") return null;
426
+ const value = String(option.currentValue);
427
+ const options = flattenSelectOptions(option.options);
428
+ const label = options.find((entry) => entry.value === value)?.label ?? value;
429
+ return { value, label, options };
430
+ }
410
431
  resetSession() {
411
432
  this.session = null;
412
433
  }
@@ -474,7 +495,7 @@ var AcpAgent = class {
474
495
  const init = await this.ctx.request(acp.methods.agent.initialize, {
475
496
  protocolVersion: 1,
476
497
  clientCapabilities: {},
477
- clientInfo: { name: "vbot", version: "0.1.0" }
498
+ clientInfo: { name: "vbot", version: VBOT_VERSION }
478
499
  });
479
500
  this.initCaps = init.agentCapabilities;
480
501
  } catch (error) {
@@ -593,15 +614,14 @@ var AcpAgent = class {
593
614
  this.pendingModel = model;
594
615
  return true;
595
616
  }
596
- const option = this.configOptions.find(
597
- (entry) => entry.type === "select" && (entry.id === "model" || entry.category === "model")
598
- );
617
+ const option = this.configOptions.find(isModelOption);
599
618
  if (!option) return false;
600
- await this.ctx.request(acp.methods.agent.session.setConfigOption, {
619
+ const response = await this.ctx.request(acp.methods.agent.session.setConfigOption, {
601
620
  sessionId: this.session,
602
621
  configId: option.id,
603
622
  value: model
604
623
  });
624
+ if (response?.configOptions?.length) this.configOptions = response.configOptions;
605
625
  this.pendingModel = null;
606
626
  return true;
607
627
  }
@@ -737,21 +757,29 @@ var BotQueue = class {
737
757
  }
738
758
  };
739
759
 
740
- // src/version.ts
741
- var VBOT_VERSION = "0.1.2";
742
-
743
760
  // src/core/dispatcher.ts
744
- var HELP_TEXT = `Available commands:
745
- /new \u2014 Start a new session
746
- /stop \u2014 Abort the current request
747
- /status \u2014 Show agent, session and queue status
748
- /agent [name] \u2014 Show or switch agent backend
749
- /model [name] \u2014 Show or switch model
750
- /cost \u2014 Show context usage and cost
751
- /version \u2014 Show vbot version
752
- /help \u2014 Show this help
753
-
754
- Other /commands are sent to the agent as prompts.`;
761
+ var import_node_os3 = require("os");
762
+ var HELP_ROWS = [
763
+ ["/new", "Start a new session"],
764
+ ["/stop", "Abort the current request"],
765
+ ["/status", "Show agent, session and queue status"],
766
+ ["/agent [name]", "Show or switch agent backend"],
767
+ ["/model [name]", "Show or switch model"],
768
+ ["/cost", "Show context usage and cost"],
769
+ ["/version", "Show vbot version"],
770
+ ["/help", "Show this help"]
771
+ ];
772
+ var HELP_FOOTER = "Other /commands are sent to the agent as prompts.";
773
+ var NEW_BUSY_TEXT = "Cannot reset the session while a request is running. Use /stop first.";
774
+ var MODEL_BUSY_TEXT = "Cannot switch model while a request is running. Use /stop first.";
775
+ var HELP_TEXT = [
776
+ table(
777
+ ["Command", "Description"],
778
+ HELP_ROWS.map(([command, description]) => [command, description])
779
+ ),
780
+ "",
781
+ HELP_FOOTER
782
+ ].join("\n");
755
783
  function createDispatcher(runtime) {
756
784
  return (text, reply) => {
757
785
  const send = (message) => {
@@ -765,11 +793,57 @@ function createDispatcher(runtime) {
765
793
  });
766
794
  };
767
795
  }
796
+ function escapeCell(value) {
797
+ return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
798
+ }
799
+ function table(headers, rows) {
800
+ const head = `| ${headers.map(escapeCell).join(" | ")} |`;
801
+ const separator = `| ${headers.map(() => "---").join(" | ")} |`;
802
+ const body = rows.map((row) => `| ${row.map(escapeCell).join(" | ")} |`);
803
+ return [head, separator, ...body].join("\n");
804
+ }
805
+ function formatNumber(value) {
806
+ if (!Number.isFinite(value)) return String(value);
807
+ const negative = value < 0;
808
+ const digits = Math.abs(Math.trunc(value)).toString();
809
+ let out = "";
810
+ for (let i = 0; i < digits.length; i += 1) {
811
+ if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
812
+ out += digits[i];
813
+ }
814
+ return negative ? "-" + out : out;
815
+ }
816
+ function percentOf(used, size) {
817
+ if (size <= 0) return null;
818
+ return `${(used / size * 100).toFixed(1)}%`;
819
+ }
820
+ function usageText(runtime) {
821
+ const { used, size } = runtime.state().usage;
822
+ if (size <= 0) return "not reported";
823
+ const base = `${formatNumber(used)} / ${formatNumber(size)}`;
824
+ const percent = percentOf(used, size);
825
+ return percent ? `${base} (${percent})` : base;
826
+ }
827
+ function compactPath(path) {
828
+ const home = (0, import_node_os3.homedir)();
829
+ if (!home) return path;
830
+ if (path === home) return "~";
831
+ const prefix = home + "/";
832
+ if (path.startsWith(prefix)) return "~" + path.slice(home.length);
833
+ return path;
834
+ }
835
+ function modelText(runtime) {
836
+ const current = runtime.agent().currentModel();
837
+ if (current) return current.label;
838
+ const configured = runtime.config().agent.model;
839
+ if (configured) return `${configured} (configured, not applied yet)`;
840
+ return "unknown (session not started)";
841
+ }
768
842
  function handleSlashCommand(runtime, text, send) {
769
843
  const [rawCommand, ...args] = text.split(/\s+/);
770
844
  switch (rawCommand.toLowerCase()) {
771
845
  case "/new":
772
- handleNew(runtime, send);
846
+ void handleNew(runtime, send);
773
847
  return null;
774
848
  case "/stop":
775
849
  handleStop(runtime, send);
@@ -781,7 +855,7 @@ function handleSlashCommand(runtime, text, send) {
781
855
  handleAgent(runtime, args, send);
782
856
  return null;
783
857
  case "/model":
784
- handleModel(runtime, args, send);
858
+ void handleModel(runtime, args, send);
785
859
  return null;
786
860
  case "/cost":
787
861
  send(costText(runtime));
@@ -796,12 +870,18 @@ function handleSlashCommand(runtime, text, send) {
796
870
  return text.slice(1);
797
871
  }
798
872
  }
799
- function handleNew(runtime, send) {
800
- runtime.agent().resetSession();
801
- const state = runtime.state();
802
- state.acpSessionId = null;
803
- state.usage = { used: 0, size: 0, cost: null };
804
- runtime.saveState();
873
+ async function handleNew(runtime, send) {
874
+ if (runtime.queue().status().busy) {
875
+ send(NEW_BUSY_TEXT);
876
+ return;
877
+ }
878
+ await runtime.serialize(async () => {
879
+ runtime.agent().resetSession();
880
+ const state = runtime.state();
881
+ state.acpSessionId = null;
882
+ state.usage = { used: 0, size: 0, cost: null };
883
+ runtime.saveState();
884
+ });
805
885
  send("Session reset. Starting a new conversation.");
806
886
  }
807
887
  function handleStop(runtime, send) {
@@ -817,12 +897,18 @@ function handleStop(runtime, send) {
817
897
  }
818
898
  function handleAgent(runtime, args, send) {
819
899
  const name = args.join(" ").trim();
900
+ const current = runtime.config().agent.name;
820
901
  if (!name) {
821
- const current = runtime.config().agent.name;
822
- const list = AGENT_NAMES.map((entry) => `${entry === current ? "*" : " "} ${entry}`).join("\n");
823
- send(`Current agent: ${current}
824
- Available agents:
825
- ${list}`);
902
+ send(
903
+ table(
904
+ ["Agent", "Backend", ""],
905
+ AGENT_NAMES.map((entry) => [
906
+ entry,
907
+ AGENTS[entry].label,
908
+ entry === current ? "\u2190 current" : ""
909
+ ])
910
+ )
911
+ );
826
912
  return;
827
913
  }
828
914
  if (!isAgentName(name)) {
@@ -838,56 +924,95 @@ ${list}`);
838
924
  (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
839
925
  );
840
926
  }
841
- function handleModel(runtime, args, send) {
927
+ async function handleModel(runtime, args, send) {
842
928
  const model = args.join(" ").trim();
843
929
  if (!model) {
844
- send(`Current model: ${runtime.config().agent.model ?? "default"}`);
930
+ send(modelReport(runtime));
931
+ return;
932
+ }
933
+ if (runtime.queue().status().busy) {
934
+ send(MODEL_BUSY_TEXT);
845
935
  return;
846
936
  }
847
937
  const config = runtime.config();
848
938
  const hadSession = runtime.agent().sessionId() !== null;
849
- config.agent.model = model;
850
- runtime.saveConfig();
851
- runtime.agent().setModel(model).then((supported) => {
852
- if (!supported) {
853
- send(
854
- `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
855
- );
856
- return;
857
- }
939
+ const supported = await runtime.serialize(async () => {
940
+ const accepted = await runtime.agent().setModel(model);
941
+ config.agent.model = model;
942
+ runtime.saveConfig();
943
+ return accepted;
944
+ });
945
+ const applied = runtime.agent().currentModel()?.label ?? model;
946
+ if (!supported) {
858
947
  send(
859
- hadSession ? `Model switched to: ${model}` : `Model set to: ${model}. It will apply when the session starts.`
948
+ `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
860
949
  );
861
- }).catch(
862
- (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
950
+ return;
951
+ }
952
+ send(
953
+ hadSession ? `Model switched to: ${applied}` : `Model set to: ${applied}. It will apply when the session starts.`
863
954
  );
864
955
  }
956
+ function modelReport(runtime) {
957
+ const current = runtime.agent().currentModel();
958
+ if (!current) {
959
+ const configured = runtime.config().agent.model;
960
+ const hint = configured ? `Configured: ${configured}
961
+
962
+ The agent has not reported a model yet; it becomes visible after the first message.` : "This agent does not expose a runtime model option, or the session has not started.";
963
+ return `Current: unknown
964
+
965
+ ${hint}`;
966
+ }
967
+ const lines = [`Current: ${current.label}`];
968
+ if (current.options.length > 0) {
969
+ lines.push(
970
+ "",
971
+ table(
972
+ ["Model", ""],
973
+ current.options.map((option) => [
974
+ option.label,
975
+ option.value === current.value ? "\u2190 current" : ""
976
+ ])
977
+ )
978
+ );
979
+ }
980
+ lines.push("", "Switch: /model <name>");
981
+ return lines.join("\n");
982
+ }
865
983
  function statusText(runtime) {
866
984
  const config = runtime.config();
867
985
  const agent = runtime.agent();
868
986
  const state = runtime.state();
869
987
  const queue = runtime.queue().status();
870
- const lines = [
871
- `Agent: ${config.agent.name} (${AGENTS[config.agent.name].label})`,
872
- `Session: ${agent.sessionId() ?? "none"}`,
873
- `Work dir: ${config.agent.cwd}`,
874
- `Model: ${config.agent.model ?? "default"}`,
875
- `Status: ${queue.busy ? "busy" : "idle"}`,
876
- `Queue: ${queue.queued}`,
877
- `Usage: ${state.usage.used}/${state.usage.size} tokens`,
878
- `Capabilities: ${capabilitiesText(agent.capabilities())}`
988
+ const { used, size } = state.usage;
989
+ const summary = [
990
+ `**${config.agent.name}**`,
991
+ queue.busy ? "busy" : "idle",
992
+ size > 0 ? `${formatNumber(used)} / ${formatNumber(size)}` : null
993
+ ].filter(Boolean).join(" \xB7 ");
994
+ const rows = [
995
+ ["Agent", `${config.agent.name} (${AGENTS[config.agent.name].label})`],
996
+ ["Session", agent.sessionId() ?? "none"],
997
+ ["Work dir", compactPath(config.agent.cwd)],
998
+ ["Model", modelText(runtime)],
999
+ ["Status", queue.busy ? "busy" : "idle"],
1000
+ ["Queue", String(queue.queued)],
1001
+ ["Usage", usageText(runtime)],
1002
+ ["Capabilities", capabilitiesText(agent.capabilities())]
879
1003
  ];
880
- return lines.join("\n");
1004
+ return [summary, "", table(["Field", "Value"], rows)].join("\n");
881
1005
  }
882
1006
  function costText(runtime) {
883
1007
  const usage = runtime.state().usage;
884
- const context = `Context: ${usage.used}/${usage.size} tokens`;
885
- if (!usage.cost) {
886
- return `${context}
887
- Cost: this agent does not report cost.`;
888
- }
889
- return `${context}
890
- Cost: ${usage.cost.amount} ${usage.cost.currency}`;
1008
+ const cost = usage.cost ? `${usage.cost.amount} ${usage.cost.currency}` : "not reported by this agent";
1009
+ return table(
1010
+ ["Field", "Value"],
1011
+ [
1012
+ ["Context", usageText(runtime)],
1013
+ ["Cost", cost]
1014
+ ]
1015
+ );
891
1016
  }
892
1017
  function capabilitiesText(caps) {
893
1018
  const enabled = [
@@ -1151,13 +1276,17 @@ async function startBot(configPath2) {
1151
1276
  saveConfig: () => saveConfig(config, configPath2)
1152
1277
  };
1153
1278
  holder.queue = new BotQueue((item, signal) => gate(() => processPrompt(ctx, item, signal)));
1154
- const runtime = createRuntime(ctx, (name) => {
1155
- if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1156
- switching = true;
1157
- return gate(() => switchAgent(ctx, name)).finally(() => {
1158
- switching = false;
1159
- });
1160
- });
1279
+ const runtime = createRuntime(
1280
+ ctx,
1281
+ (task) => gate(task),
1282
+ (name) => {
1283
+ if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1284
+ switching = true;
1285
+ return gate(() => switchAgent(ctx, name)).finally(() => {
1286
+ switching = false;
1287
+ });
1288
+ }
1289
+ );
1161
1290
  const dispatch = createDispatcher(runtime);
1162
1291
  let notifySignal = () => void 0;
1163
1292
  const wire = new VicvicWire({
@@ -1218,7 +1347,7 @@ async function runBot(configPath2) {
1218
1347
  const bot = await startBot(configPath2);
1219
1348
  await bot.finished;
1220
1349
  }
1221
- function createRuntime(ctx, switchAgent2) {
1350
+ function createRuntime(ctx, gate, switchAgent2) {
1222
1351
  return {
1223
1352
  config: () => ctx.config,
1224
1353
  agent: () => ctx.getAgent(),
@@ -1226,6 +1355,7 @@ function createRuntime(ctx, switchAgent2) {
1226
1355
  state: () => ctx.state,
1227
1356
  saveState: () => ctx.saveState(),
1228
1357
  saveConfig: () => ctx.saveConfig(),
1358
+ serialize: (task) => gate(task),
1229
1359
  switchAgent: switchAgent2
1230
1360
  };
1231
1361
  }