jinzd-ai-cli 0.4.277 → 0.4.279

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 (34) hide show
  1. package/dist/{batch-2I5TPZA2.js → batch-6QZSWCBL.js} +2 -2
  2. package/dist/{chunk-ZP3RCZKM.js → chunk-2BKRWTEQ.js} +1 -1
  3. package/dist/{chunk-Z752LDYF.js → chunk-5XHEXRMN.js} +1 -1
  4. package/dist/{chunk-7BO5H2QI.js → chunk-6WMT7OQG.js} +1 -1
  5. package/dist/{chunk-HJVKCOAG.js → chunk-7L4TEK7Q.js} +419 -3
  6. package/dist/{chunk-7YYUITED.js → chunk-CMNXAJLH.js} +1 -1
  7. package/dist/{chunk-KIGKL3PZ.js → chunk-FQJFGGKN.js} +4 -4
  8. package/dist/{chunk-UWQCTTP4.js → chunk-IIXZ7IPN.js} +1 -1
  9. package/dist/{chunk-7IPMONKZ.js → chunk-JLOQBYU4.js} +3 -3
  10. package/dist/{chunk-I3PYD2YN.js → chunk-JS6VAPWM.js} +3 -3
  11. package/dist/{chunk-3VFOXEIL.js → chunk-NZPHEUJM.js} +1 -1
  12. package/dist/{chunk-HCF442WL.js → chunk-RJY3HJS4.js} +1 -1
  13. package/dist/{chunk-BYNNKJUC.js → chunk-SIVM2YQQ.js} +1 -1
  14. package/dist/{chunk-OCOGYIRY.js → chunk-UD3BVNC5.js} +1 -1
  15. package/dist/{ci-JFC45LIS.js → ci-F4ESOYI5.js} +4 -4
  16. package/dist/{ci-format-P3LIZHUR.js → ci-format-AIRW3TVQ.js} +2 -2
  17. package/dist/{constants-N3CHKZJY.js → constants-3VMMZJDS.js} +1 -1
  18. package/dist/{doctor-cli-Z3TGHTSX.js → doctor-cli-WVMEWMYE.js} +4 -4
  19. package/dist/electron-server.js +699 -101
  20. package/dist/{hub-NEWIFMQ3.js → hub-JJCCCU3O.js} +1 -1
  21. package/dist/index.js +243 -466
  22. package/dist/{persistent-memory-6Q5FDFJ7.js → persistent-memory-KSZKM4JG.js} +2 -2
  23. package/dist/{persistent-memory-ZFR5GI7D.js → persistent-memory-QZN4Q3W4.js} +2 -2
  24. package/dist/{pr-ZUGSWQ5R.js → pr-GFB5JC26.js} +4 -4
  25. package/dist/{run-tests-76M6AC3G.js → run-tests-EL54INOI.js} +2 -2
  26. package/dist/{run-tests-USV7IJ7X.js → run-tests-HMHHIG7Y.js} +2 -2
  27. package/dist/{server-R7D5RR36.js → server-6C7GITHY.js} +5 -5
  28. package/dist/{server-C523CIGN.js → server-WUSJWC3A.js} +324 -106
  29. package/dist/{task-orchestrator-G2MQDNGQ.js → task-orchestrator-C7RUUA2Q.js} +5 -5
  30. package/dist/{usage-EFHXXMYX.js → usage-CW6BT2UV.js} +2 -2
  31. package/dist/web/client/app.js +18 -3
  32. package/dist/web/client/app.js.br +0 -0
  33. package/dist/web/client/app.js.gz +0 -0
  34. package/package.json +1 -1
@@ -40,14 +40,14 @@ import {
40
40
  touchMemoryReferences,
41
41
  updateMemoryApproval,
42
42
  updateMemoryEntry
43
- } from "./chunk-7YYUITED.js";
43
+ } from "./chunk-CMNXAJLH.js";
44
44
  import {
45
45
  redactJson,
46
46
  scanString
47
47
  } from "./chunk-FSC6KEWU.js";
48
48
  import {
49
49
  runTestsTool
50
- } from "./chunk-BYNNKJUC.js";
50
+ } from "./chunk-SIVM2YQQ.js";
51
51
  import {
52
52
  AGENTIC_BEHAVIOR_GUIDELINE,
53
53
  APP_NAME,
@@ -80,7 +80,7 @@ import {
80
80
  SUBAGENT_MAX_ROUNDS_LIMIT,
81
81
  VERSION,
82
82
  buildUserIdentityPrompt
83
- } from "./chunk-Z752LDYF.js";
83
+ } from "./chunk-5XHEXRMN.js";
84
84
  import {
85
85
  hasSemanticIndex,
86
86
  semanticSearch
@@ -15543,48 +15543,259 @@ function applyThinkCommand(sub, state2) {
15543
15543
  return stay(`Unknown /think option "${sub}" \u2014 expected on | off | low | high | max | status`);
15544
15544
  }
15545
15545
 
15546
+ // src/core/provider-command.ts
15547
+ function preservedSuffix(messageCount) {
15548
+ return messageCount > 0 ? ` (conversation preserved: ${messageCount} messages)` : "";
15549
+ }
15550
+ function resolveProviderSwitch(input) {
15551
+ const id = input.targetId;
15552
+ if (!input.configured) {
15553
+ return {
15554
+ status: "not-configured",
15555
+ message: `Provider '${id}' is not configured. Run: aicli config`
15556
+ };
15557
+ }
15558
+ if (input.availableModels.length === 0) {
15559
+ return {
15560
+ status: "no-models",
15561
+ message: `Provider '${id}' has no available models. ` + (id === "ollama" ? "Make sure Ollama is running (`ollama serve`) and has models (`ollama pull <model>`)." : "Check provider configuration.")
15562
+ };
15563
+ }
15564
+ if (id === input.currentProvider) {
15565
+ return { status: "unchanged", message: `Already using provider: ${id}` };
15566
+ }
15567
+ const model = input.configuredDefaultModel || input.providerDefaultModel;
15568
+ return {
15569
+ status: "switched",
15570
+ providerId: id,
15571
+ model,
15572
+ message: `Switched to provider: ${id} (${model})` + preservedSuffix(input.messageCount)
15573
+ };
15574
+ }
15575
+ function parseModelCommand(args, currentProvider) {
15576
+ const raw = args[0];
15577
+ if (!raw) return { kind: "select" };
15578
+ const sub = raw.toLowerCase();
15579
+ if (sub === "refresh") return { kind: "refresh", providerId: args[1] ?? currentProvider };
15580
+ if (sub === "cache") {
15581
+ if ((args[1] ?? "").toLowerCase() === "clear") return { kind: "cache-clear", providerId: args[2] };
15582
+ return { kind: "cache-show" };
15583
+ }
15584
+ return { kind: "set", modelId: raw };
15585
+ }
15586
+ function resolveModelSwitch(input) {
15587
+ if (input.targetModel === input.currentModel) {
15588
+ return { status: "unchanged", message: `Already using model: ${input.targetModel}` };
15589
+ }
15590
+ const known = input.knownModels.includes(input.targetModel);
15591
+ return {
15592
+ status: "switched",
15593
+ model: input.targetModel,
15594
+ message: `Switched to model: ${input.targetModel}` + preservedSuffix(input.messageCount),
15595
+ warning: known ? void 0 : `'${input.targetModel}' is not in the known model list for ${input.provider}. If the provider rejects it, check the spelling or run /model refresh ${input.provider}.`
15596
+ };
15597
+ }
15598
+ var PLAN_MODE_TOOL_HINT = [
15599
+ "read_file \xB7 list_dir \xB7 grep_files \xB7 glob_files",
15600
+ "web_fetch \xB7 google_search \xB7 ask_user \xB7 write_todos"
15601
+ ];
15602
+ function applyPlanCommand(sub, active) {
15603
+ const key = (sub ?? "enter").toLowerCase() === "toggle" ? active ? "exit" : "enter" : (sub ?? "enter").toLowerCase();
15604
+ if (key === "enter") {
15605
+ if (active) {
15606
+ return {
15607
+ planMode: true,
15608
+ changed: false,
15609
+ status: "already-active",
15610
+ summary: "Already in Plan Mode. Use /plan execute to start executing, or /plan exit to leave.",
15611
+ details: []
15612
+ };
15613
+ }
15614
+ return {
15615
+ planMode: true,
15616
+ changed: true,
15617
+ status: "entered",
15618
+ summary: "\u{1F4CB} Plan Mode activated",
15619
+ details: [
15620
+ "AI can now ONLY use read-only tools:",
15621
+ ...PLAN_MODE_TOOL_HINT,
15622
+ "Describe your task and the AI will analyze the codebase and create a detailed implementation plan.",
15623
+ "When ready: /plan execute | To cancel: /plan exit"
15624
+ ]
15625
+ };
15626
+ }
15627
+ if (key === "execute") {
15628
+ if (!active) {
15629
+ return { planMode: false, changed: false, status: "not-active", summary: "Not in Plan Mode. Enter first with /plan", details: [] };
15630
+ }
15631
+ return {
15632
+ planMode: false,
15633
+ changed: true,
15634
+ status: "executing",
15635
+ summary: "Plan Mode ended \u2014 switching to execute mode. AI now has full tool access.",
15636
+ details: []
15637
+ };
15638
+ }
15639
+ if (key === "exit" || key === "cancel") {
15640
+ if (!active) {
15641
+ return { planMode: false, changed: false, status: "not-active", summary: "Not in Plan Mode.", details: [] };
15642
+ }
15643
+ return { planMode: false, changed: true, status: "cancelled", summary: "Plan Mode cancelled. Returning to normal mode.", details: [] };
15644
+ }
15645
+ if (key === "status" || key === "show") {
15646
+ return {
15647
+ planMode: active,
15648
+ changed: false,
15649
+ status: "status",
15650
+ summary: `Plan Mode: ${active ? "ACTIVE \u{1F4CB}" : "inactive"}`,
15651
+ details: active ? ["Tools restricted to read-only set.", "Use /plan execute to start executing, or /plan exit to cancel."] : []
15652
+ };
15653
+ }
15654
+ return {
15655
+ planMode: active,
15656
+ changed: false,
15657
+ status: "unknown",
15658
+ summary: `Unknown /plan option "${sub}" \u2014 expected execute | exit | status`,
15659
+ details: []
15660
+ };
15661
+ }
15662
+ function parseRouteCommand(args) {
15663
+ const sub = (args[0] ?? "show").toLowerCase();
15664
+ if (sub === "on" || sub === "enable") return { kind: "enable" };
15665
+ if (sub === "off" || sub === "disable") return { kind: "disable" };
15666
+ if (sub === "test") {
15667
+ const message = args.slice(1).join(" ").trim();
15668
+ return message ? { kind: "test", message } : { kind: "usage", message: "Usage: /route test <message>" };
15669
+ }
15670
+ if (sub === "show" || sub === "status") return { kind: "show" };
15671
+ return { kind: "unknown", message: `Unknown subcommand: ${sub}. Usage: /route [on|off|show|test <message>]` };
15672
+ }
15673
+ function routingEmptyRulesNote(routing) {
15674
+ if (routing && routing.rules.length > 0) return null;
15675
+ return 'No rules configured yet. Add rules under `routing.rules` in ~/.aicli/config.json.\nExample: { match: { tag: "fast" }, model: "claude-haiku-4-5" }';
15676
+ }
15677
+ function describeRoutingRule(rule, index) {
15678
+ const parts = [];
15679
+ if (rule.match.tag) parts.push(`tag=#${rule.match.tag}`);
15680
+ if (rule.match.contains && rule.match.contains.length > 0) {
15681
+ const shown = rule.match.contains.slice(0, 3).join(", ");
15682
+ parts.push(`contains=[${shown}${rule.match.contains.length > 3 ? ", \u2026" : ""}]`);
15683
+ }
15684
+ if (typeof rule.match.maxLength === "number") parts.push(`maxLen=${rule.match.maxLength}`);
15685
+ if (typeof rule.match.minLength === "number") parts.push(`minLen=${rule.match.minLength}`);
15686
+ const empty = parts.length === 0;
15687
+ return {
15688
+ index,
15689
+ name: rule.name,
15690
+ condition: empty ? "(empty \u2014 never matches)" : parts.join(" & "),
15691
+ model: rule.model,
15692
+ empty
15693
+ };
15694
+ }
15695
+ function describeRouting(input) {
15696
+ const rules = input.routing?.rules ?? [];
15697
+ return {
15698
+ enabled: input.routing?.enabled === true,
15699
+ provider: input.provider,
15700
+ currentModel: input.currentModel,
15701
+ fallback: input.routing?.fallback,
15702
+ rules: rules.map((r, i) => describeRoutingRule(r, i)),
15703
+ noRulesNote: rules.length === 0 ? "(no rules configured \u2014 edit ~/.aicli/config.json `routing.rules`)" : null,
15704
+ hint: "Commands: /route on | off | test <msg> | show"
15705
+ };
15706
+ }
15707
+ function routeDecisionRows(message, currentModel, decision) {
15708
+ const rows = [
15709
+ { label: "Input", value: message },
15710
+ { label: "Current", value: currentModel },
15711
+ { label: "Decision", value: `${decision.model} ${decision.overridden ? "\u2192 ROUTED" : "(unchanged)"}` },
15712
+ { label: "Reason", value: decision.reason }
15713
+ ];
15714
+ if (typeof decision.ruleIdx === "number") rows.push({ label: "Rule", value: `#${decision.ruleIdx}` });
15715
+ return rows;
15716
+ }
15717
+
15546
15718
  // src/web/commands/provider-commands.ts
15719
+ function syncSessionProvider(ctx) {
15720
+ const session = ctx.sessions.current;
15721
+ if (!session) return;
15722
+ session.updateProvider(ctx.currentProvider, ctx.currentModel);
15723
+ ctx.saveIfNeeded();
15724
+ }
15547
15725
  async function handleProvider(args, ctx) {
15548
15726
  const id = args[0];
15549
15727
  if (!id) {
15550
15728
  ctx.send({ type: "error", message: "Usage: /provider <id>" });
15551
15729
  return;
15552
15730
  }
15553
- const p = ctx.providers.get(id);
15554
- if (!p) {
15555
- ctx.send({ type: "error", message: `Provider "${id}" not available` });
15731
+ const configured = ctx.providers.has(id);
15732
+ let availableModels = [];
15733
+ let providerDefaultModel = "";
15734
+ if (configured) {
15735
+ const target = ctx.providers.get(id);
15736
+ providerDefaultModel = target.info.defaultModel;
15737
+ availableModels = target.info.models.map((m) => m.id);
15738
+ if (availableModels.length === 0) {
15739
+ try {
15740
+ availableModels = (await target.listModels()).map((m) => m.id);
15741
+ } catch {
15742
+ availableModels = [];
15743
+ }
15744
+ }
15745
+ }
15746
+ const outcome = resolveProviderSwitch({
15747
+ targetId: id,
15748
+ currentProvider: ctx.currentProvider,
15749
+ configured,
15750
+ availableModels,
15751
+ configuredDefaultModel: ctx.config.get("defaultModels")[id],
15752
+ providerDefaultModel,
15753
+ messageCount: ctx.sessions.current?.messages.length ?? 0
15754
+ });
15755
+ if (outcome.status === "not-configured" || outcome.status === "no-models") {
15756
+ ctx.send({ type: "error", message: outcome.message });
15556
15757
  return;
15557
15758
  }
15558
- ctx.currentProvider = id;
15559
- ctx.currentModel = p.info.defaultModel;
15759
+ if (outcome.status === "unchanged") {
15760
+ ctx.send({ type: "info", message: outcome.message });
15761
+ return;
15762
+ }
15763
+ ctx.currentProvider = outcome.providerId;
15764
+ ctx.currentModel = outcome.model;
15560
15765
  ctx.updateContextWindow();
15561
- ctx.send({ type: "info", message: `Switched to provider: ${id} (${ctx.currentModel})` });
15766
+ syncSessionProvider(ctx);
15767
+ ctx.send({ type: "info", message: outcome.message });
15562
15768
  ctx.sendStatus();
15563
15769
  }
15564
15770
  async function handleModel(args, ctx) {
15565
- const sub = args[0];
15566
- if (!sub) {
15771
+ const intent = parseModelCommand(args, ctx.currentProvider);
15772
+ if (intent.kind === "select") {
15567
15773
  ctx.send({ type: "error", message: "Usage: /model <id>|refresh [provider]|cache [clear [provider]]" });
15568
15774
  return;
15569
15775
  }
15570
- if (sub === "refresh") {
15571
- const providerId = args[1] ?? ctx.currentProvider;
15776
+ if (intent.kind === "refresh") {
15777
+ if (!ctx.providers.has(intent.providerId)) {
15778
+ ctx.send({ type: "error", message: `Provider is not configured: ${intent.providerId}` });
15779
+ return;
15780
+ }
15572
15781
  try {
15573
- const models2 = await ctx.providers.refreshModels(providerId);
15574
- ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${providerId}.` });
15782
+ const models2 = await ctx.providers.refreshModels(intent.providerId);
15783
+ ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${intent.providerId}.` });
15575
15784
  ctx.sendStatus();
15576
15785
  } catch (err) {
15577
15786
  ctx.send({ type: "error", message: `Model refresh failed: ${err instanceof Error ? err.message : String(err)}` });
15578
15787
  }
15579
15788
  return;
15580
15789
  }
15581
- if (sub === "cache") {
15582
- if ((args[1] ?? "").toLowerCase() === "clear") {
15583
- const providerId = args[2];
15584
- ctx.providers.clearModelCache(providerId);
15585
- ctx.send({ type: "info", message: providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache." });
15586
- return;
15587
- }
15790
+ if (intent.kind === "cache-clear") {
15791
+ ctx.providers.clearModelCache(intent.providerId);
15792
+ ctx.send({
15793
+ type: "info",
15794
+ message: intent.providerId ? `Cleared model cache for ${intent.providerId}.` : "Cleared model cache."
15795
+ });
15796
+ return;
15797
+ }
15798
+ if (intent.kind === "cache-show") {
15588
15799
  const rows = ctx.providers.getModelCacheStatus();
15589
15800
  ctx.send({
15590
15801
  type: "info",
@@ -15592,16 +15803,25 @@ async function handleModel(args, ctx) {
15592
15803
  });
15593
15804
  return;
15594
15805
  }
15595
- const modelId = sub;
15596
15806
  const models = await ctx.providers.listModels(ctx.currentProvider);
15597
- const found = models.find((m) => m.id === modelId);
15598
- if (!found) {
15599
- ctx.send({ type: "error", message: `Model "${modelId}" not found` });
15807
+ const outcome = resolveModelSwitch({
15808
+ targetModel: intent.modelId,
15809
+ currentModel: ctx.currentModel,
15810
+ provider: ctx.currentProvider,
15811
+ knownModels: models.map((m) => m.id),
15812
+ messageCount: ctx.sessions.current?.messages.length ?? 0
15813
+ });
15814
+ if (outcome.status === "unchanged") {
15815
+ ctx.send({ type: "info", message: outcome.message });
15600
15816
  return;
15601
15817
  }
15602
- ctx.currentModel = modelId;
15818
+ ctx.currentModel = outcome.model;
15603
15819
  ctx.updateContextWindow();
15604
- ctx.send({ type: "info", message: `Switched to model: ${modelId}` });
15820
+ syncSessionProvider(ctx);
15821
+ ctx.send({
15822
+ type: "info",
15823
+ message: outcome.warning ? `${outcome.message} \u2014 ${outcome.warning}` : outcome.message
15824
+ });
15605
15825
  ctx.sendStatus();
15606
15826
  }
15607
15827
  async function handleThink(args, ctx) {
@@ -15611,7 +15831,7 @@ async function handleThink(args, ctx) {
15611
15831
  runtimeEffort: ctx.runtimeThinkingEffort,
15612
15832
  configuredThinking: modelParams?.thinking,
15613
15833
  configuredEffort: modelParams?.thinkingEffort,
15614
- supportsEffort: ctx.providers.get(ctx.currentProvider)?.supportsReasoningEffort ?? false,
15834
+ supportsEffort: ctx.providers.has(ctx.currentProvider) ? ctx.providers.get(ctx.currentProvider).supportsReasoningEffort : false,
15615
15835
  provider: ctx.currentProvider
15616
15836
  });
15617
15837
  if (outcome.changed) {
@@ -15625,24 +15845,59 @@ async function handleThink(args, ctx) {
15625
15845
  if (outcome.changed) ctx.sendStatus();
15626
15846
  }
15627
15847
  async function handlePlan(args, ctx) {
15628
- const sub = args[0];
15629
- if (sub === "exit" || sub === "cancel") {
15630
- ctx.planMode = false;
15631
- ctx.send({ type: "info", message: "Plan mode OFF." });
15632
- } else if (sub === "execute") {
15633
- ctx.planMode = false;
15634
- ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
15635
- } else if (sub === "status" || sub === "show") {
15636
- ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
15848
+ const outcome = applyPlanCommand(args[0], ctx.planMode);
15849
+ if (outcome.changed) ctx.planMode = outcome.planMode;
15850
+ ctx.send({
15851
+ type: outcome.status === "unknown" ? "error" : "info",
15852
+ message: [outcome.summary, ...outcome.details].join(" \u2014 ")
15853
+ });
15854
+ if (outcome.changed) ctx.sendStatus();
15855
+ }
15856
+ async function handleRoute(args, ctx) {
15857
+ const intent = parseRouteCommand(args);
15858
+ const routing = ctx.config.get("routing");
15859
+ if (intent.kind === "enable" || intent.kind === "disable") {
15860
+ const on = intent.kind === "enable";
15861
+ ctx.config.setByPath("routing.enabled", on ? "true" : "false");
15862
+ const note = on ? routingEmptyRulesNote(routing) : null;
15863
+ ctx.send({
15864
+ type: "info",
15865
+ message: `Smart model routing ${on ? "enabled" : "disabled"}.` + (note ? `
15866
+ ${note}` : "")
15867
+ });
15868
+ ctx.sendStatus();
15637
15869
  return;
15638
- } else if (sub !== void 0) {
15639
- ctx.send({ type: "error", message: `Unknown /plan option "${sub}" \u2014 expected execute | exit | status` });
15870
+ }
15871
+ if (intent.kind === "usage" || intent.kind === "unknown") {
15872
+ ctx.send({ type: "error", message: intent.message });
15640
15873
  return;
15874
+ }
15875
+ if (intent.kind === "test") {
15876
+ const decision = ctx.computeRoutingDecision(intent.message);
15877
+ ctx.send({
15878
+ type: "info",
15879
+ message: routeDecisionRows(intent.message, ctx.currentModel, decision).map((row) => `${row.label}: ${row.value}`).join("\n")
15880
+ });
15881
+ return;
15882
+ }
15883
+ const view = describeRouting({ routing, provider: ctx.currentProvider, currentModel: ctx.currentModel });
15884
+ const lines = [
15885
+ "Smart Model Routing",
15886
+ `Status: ${view.enabled ? "enabled" : "disabled"}`,
15887
+ `Provider: ${view.provider}`,
15888
+ `Current: ${view.currentModel}`
15889
+ ];
15890
+ if (view.fallback) lines.push(`Fallback: ${view.fallback}`);
15891
+ if (view.noRulesNote) {
15892
+ lines.push(view.noRulesNote);
15641
15893
  } else {
15642
- ctx.planMode = !ctx.planMode;
15643
- ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
15894
+ lines.push("Rules (evaluated top-to-bottom):");
15895
+ for (const rule of view.rules) {
15896
+ lines.push(` #${rule.index} ${rule.name ? `${rule.name} ` : ""}${rule.condition} \u2192 ${rule.model}`);
15897
+ }
15644
15898
  }
15645
- ctx.sendStatus();
15899
+ lines.push(view.hint);
15900
+ ctx.send({ type: "info", message: lines.join("\n") });
15646
15901
  }
15647
15902
 
15648
15903
  // src/core/session-cost.ts
@@ -16152,6 +16407,232 @@ function lastAssistantText(messages) {
16152
16407
  return void 0;
16153
16408
  }
16154
16409
 
16410
+ // src/core/session-command.ts
16411
+ var DEFAULT_SESSION_LIST_LIMIT = 50;
16412
+ function parseSessionListArgs(args) {
16413
+ let limit = DEFAULT_SESSION_LIST_LIMIT;
16414
+ let all = false;
16415
+ const filter = {};
16416
+ const warnings = [];
16417
+ let search = null;
16418
+ let days = null;
16419
+ let since = null;
16420
+ let sort = { field: "updated", order: "desc" };
16421
+ let json = false;
16422
+ let stale = null;
16423
+ let verbose = false;
16424
+ for (let i = 1; i < args.length; i++) {
16425
+ const a = args[i];
16426
+ if (a === "--all" || a === "-a") {
16427
+ all = true;
16428
+ } else if ((a === "--limit" || a === "-n") && i + 1 < args.length) {
16429
+ const n = Number(args[i + 1]);
16430
+ if (Number.isFinite(n) && n > 0) {
16431
+ limit = n;
16432
+ i++;
16433
+ }
16434
+ } else if (a === "--filter" && i + 1 < args.length) {
16435
+ const val = args[++i];
16436
+ const eq = val.indexOf("=");
16437
+ if (eq > 0) {
16438
+ const key = val.slice(0, eq);
16439
+ const vals = val.slice(eq + 1).split(",").map((s) => s.trim().toLowerCase());
16440
+ if (key === "provider") filter.provider = vals;
16441
+ else if (key === "model") filter.model = vals;
16442
+ else if (key === "messages") {
16443
+ const n = Number(vals[0]);
16444
+ if (Number.isFinite(n) && n >= 0) filter.messages = n;
16445
+ else warnings.push(`Invalid filter value for "messages": expected a non-negative integer, got "${vals[0]}"`);
16446
+ } else {
16447
+ warnings.push(`Unknown filter key: "${key}". Supported keys: provider, model, messages`);
16448
+ }
16449
+ } else {
16450
+ warnings.push(`Invalid --filter syntax: "${val}". Expected key=value`);
16451
+ }
16452
+ } else if ((a === "--search" || a === "-s") && i + 1 < args.length) {
16453
+ search = args[++i];
16454
+ } else if (a === "--days" && i + 1 < args.length) {
16455
+ const n = Number(args[++i]);
16456
+ if (Number.isFinite(n) && n > 0) days = n;
16457
+ } else if (a === "--since" && i + 1 < args.length) {
16458
+ since = args[++i];
16459
+ } else if (a === "--sort" && i + 1 < args.length) {
16460
+ const sv = args[++i];
16461
+ const colon = sv.indexOf(":");
16462
+ const field = colon > 0 ? sv.slice(0, colon) : sv;
16463
+ const order = colon > 0 ? sv.slice(colon + 1) : "desc";
16464
+ if (field === "updated" || field === "created" || field === "messages" || field === "size") {
16465
+ sort = { field, order: order === "asc" ? "asc" : "desc" };
16466
+ }
16467
+ } else if (a === "--json") {
16468
+ json = true;
16469
+ } else if (a === "--verbose" || a === "-v") {
16470
+ verbose = true;
16471
+ } else if (a === "--stale" && i + 1 < args.length) {
16472
+ const n = Number(args[++i]);
16473
+ if (Number.isFinite(n) && n > 0) stale = n;
16474
+ }
16475
+ }
16476
+ if (all) limit = -1;
16477
+ return { limit, all, filter, search, days, since, sort, json, stale, verbose, warnings };
16478
+ }
16479
+ function sessionListNeedsFileSize(opts) {
16480
+ return opts.verbose || opts.sort.field === "size";
16481
+ }
16482
+ function selectSessions(sessions, opts, now = Date.now()) {
16483
+ let matched = sessions;
16484
+ const hasFilters = !!opts.filter.provider || !!opts.filter.model || opts.filter.messages !== void 0;
16485
+ const hasSearch = !!opts.search;
16486
+ const hasTimeFilter = opts.days !== null || opts.since !== null;
16487
+ const hasStale = opts.stale !== null;
16488
+ if (hasFilters) {
16489
+ const p = opts.filter.provider;
16490
+ const m = opts.filter.model;
16491
+ const msg = opts.filter.messages;
16492
+ matched = matched.filter((s) => {
16493
+ if (p && !p.includes(s.provider.toLowerCase())) return false;
16494
+ if (m && !m.some((kw) => s.model.toLowerCase().includes(kw))) return false;
16495
+ if (msg !== void 0 && s.messageCount !== msg) return false;
16496
+ return true;
16497
+ });
16498
+ }
16499
+ if (hasSearch) {
16500
+ const q = opts.search.toLowerCase();
16501
+ matched = matched.filter(
16502
+ (s) => (s.title ?? "").toLowerCase().includes(q) || s.id.toLowerCase().includes(q)
16503
+ );
16504
+ }
16505
+ if (hasTimeFilter) {
16506
+ let cutoff = 0;
16507
+ if (opts.since) cutoff = new Date(opts.since).getTime();
16508
+ else if (opts.days !== null) cutoff = now - opts.days * 24 * 60 * 60 * 1e3;
16509
+ if (!Number.isNaN(cutoff)) {
16510
+ matched = matched.filter((s) => s.updated.getTime() >= cutoff);
16511
+ }
16512
+ }
16513
+ const staleCutoff = hasStale ? now - opts.stale * 24 * 60 * 60 * 1e3 : null;
16514
+ if (staleCutoff !== null) {
16515
+ matched = matched.filter((s) => s.updated.getTime() < staleCutoff);
16516
+ }
16517
+ const defaultSort = opts.sort.field === "updated" && opts.sort.order === "desc";
16518
+ if (!defaultSort) {
16519
+ const mul = opts.sort.order === "asc" ? 1 : -1;
16520
+ matched = [...matched].sort((a, b) => {
16521
+ switch (opts.sort.field) {
16522
+ case "created":
16523
+ return (a.created.getTime() - b.created.getTime()) * mul;
16524
+ case "messages":
16525
+ return (a.messageCount - b.messageCount) * mul;
16526
+ case "size":
16527
+ return ((a.fileSize ?? 0) - (b.fileSize ?? 0)) * mul;
16528
+ default:
16529
+ return (a.updated.getTime() - b.updated.getTime()) * mul;
16530
+ }
16531
+ });
16532
+ }
16533
+ const filterTags = [];
16534
+ if (hasFilters) filterTags.push("filtered");
16535
+ if (hasSearch) filterTags.push("searched");
16536
+ if (hasTimeFilter) filterTags.push("time-limited");
16537
+ if (hasStale) filterTags.push(`stale >${opts.stale}d`);
16538
+ return {
16539
+ matched,
16540
+ shown: opts.limit > 0 ? matched.slice(0, opts.limit) : matched,
16541
+ totalCount: matched.length,
16542
+ totalMessages: matched.reduce((sum, s) => sum + s.messageCount, 0),
16543
+ staleCutoff,
16544
+ filterTags,
16545
+ needsFileSize: sessionListNeedsFileSize(opts)
16546
+ };
16547
+ }
16548
+ function sessionListSummary(view) {
16549
+ const tag = view.filterTags.length > 0 ? ` (${view.filterTags.join(", ")})` : "";
16550
+ return `${view.shown.length} of ${view.totalCount} sessions${tag} \xB7 ${view.totalMessages} messages total`;
16551
+ }
16552
+ function sessionListJsonRow(s, includeFileSize) {
16553
+ return {
16554
+ id: s.id,
16555
+ title: s.title ?? null,
16556
+ provider: s.provider,
16557
+ model: s.model,
16558
+ messageCount: s.messageCount,
16559
+ created: s.created.toISOString(),
16560
+ updated: s.updated.toISOString(),
16561
+ ...includeFileSize ? { fileSize: s.fileSize } : {},
16562
+ ...s.branchCount !== void 0 ? { branchCount: s.branchCount, activeBranch: s.activeBranch } : {}
16563
+ };
16564
+ }
16565
+ function resolveSessionRef(sessions, prefix, policy) {
16566
+ const matches2 = sessions.filter((s) => s.id.startsWith(prefix));
16567
+ if (matches2.length === 0) {
16568
+ return { status: "not-found", message: `Session '${prefix}' not found.` };
16569
+ }
16570
+ if (matches2.length > 1 && policy === "strict") {
16571
+ return {
16572
+ status: "ambiguous",
16573
+ matches: matches2,
16574
+ message: `Ambiguous prefix '${prefix}' matches ${matches2.length} sessions \u2014 be more specific:`
16575
+ };
16576
+ }
16577
+ return {
16578
+ status: "ok",
16579
+ match: matches2[0],
16580
+ ambiguousWith: matches2.length > 1 ? matches2 : [],
16581
+ message: matches2.length > 1 ? `\u26A0 Ambiguous prefix '${prefix}' matches ${matches2.length} sessions \u2014 loading most recent:` : void 0
16582
+ };
16583
+ }
16584
+ function formatSessionSummaryLine(s, titleWidth = 60) {
16585
+ const title = s.title ?? "(untitled)";
16586
+ const shown = title.length > titleWidth ? `${title.slice(0, titleWidth - 1)}\u2026` : title;
16587
+ return `${s.id.slice(0, 12)} ${s.provider}/${s.model} ${String(s.messageCount).padStart(4)} msgs ${s.updated.toISOString().slice(0, 10)} ${shown}`;
16588
+ }
16589
+ function parseBatchDeleteArgs(args) {
16590
+ let messages;
16591
+ let dryRun = false;
16592
+ for (let i = 1; i < args.length; i++) {
16593
+ if (args[i] === "--filter" && i + 1 < args.length) {
16594
+ const val = args[++i];
16595
+ const eq = val.indexOf("=");
16596
+ if (eq <= 0) {
16597
+ return { dryRun, error: `Invalid --filter syntax: "${val}". Expected key=value (e.g., messages=0)` };
16598
+ }
16599
+ const key = val.slice(0, eq);
16600
+ const vals = val.slice(eq + 1).split(",").map((s) => s.trim().toLowerCase());
16601
+ if (key !== "messages") {
16602
+ return { dryRun, error: `Unsupported filter key for delete: "${key}". Only "messages" is supported.` };
16603
+ }
16604
+ const n = Number(vals[0]);
16605
+ if (!Number.isFinite(n) || n < 0) {
16606
+ return { dryRun, error: `Invalid filter value for "messages": expected a non-negative integer, got "${vals[0]}"` };
16607
+ }
16608
+ messages = n;
16609
+ } else if (args[i] === "--dry-run") {
16610
+ dryRun = true;
16611
+ }
16612
+ }
16613
+ if (messages === void 0) {
16614
+ return { dryRun, error: "Usage: /session delete --filter messages=N [--dry-run]" };
16615
+ }
16616
+ return { messages, dryRun };
16617
+ }
16618
+ function parseCleanArgs(args) {
16619
+ let staleDays = 90;
16620
+ let dryRun = false;
16621
+ for (let i = 1; i < args.length; i++) {
16622
+ if (args[i] === "--stale" && i + 1 < args.length) {
16623
+ const n = Number(args[++i]);
16624
+ if (Number.isFinite(n) && n > 0) staleDays = n;
16625
+ } else if (args[i] === "--dry-run") {
16626
+ dryRun = true;
16627
+ }
16628
+ }
16629
+ return { staleDays, dryRun };
16630
+ }
16631
+ function selectStaleSessions(sessions, staleDays, now = Date.now()) {
16632
+ const cutoff = now - staleDays * 24 * 60 * 60 * 1e3;
16633
+ return sessions.filter((s) => s.updated.getTime() < cutoff);
16634
+ }
16635
+
16155
16636
  // src/web/commands/session-commands.ts
16156
16637
  async function handleClear(_args, ctx) {
16157
16638
  ctx.saveIfNeeded();
@@ -16175,7 +16656,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
16175
16656
  Cost: ${formatCost(cost)}` : "";
16176
16657
  let memoryLine = "";
16177
16658
  try {
16178
- const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-ZFR5GI7D.js");
16659
+ const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-QZN4Q3W4.js");
16179
16660
  const pending = countPendingMemories2(ctx.config.getConfigDir());
16180
16661
  if (pending > 0) memoryLine = `
16181
16662
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -16218,11 +16699,17 @@ async function handleSession(args, ctx) {
16218
16699
  ctx.sendSessionList();
16219
16700
  return;
16220
16701
  }
16221
- const list = ctx.sessions.listSessions();
16222
- const found = list.find((s) => s.id.startsWith(targetId));
16223
- if (found) {
16702
+ const ref = resolveSessionRef(ctx.sessions.listSessions(), targetId, "most-recent");
16703
+ if (ref.status === "ok") {
16704
+ const found = ref.match;
16224
16705
  ctx.sessions.loadSession(found.id);
16225
16706
  ctx.resetWebSessionUsage();
16707
+ if (ref.message) {
16708
+ ctx.send({
16709
+ type: "info",
16710
+ message: [ref.message, ...ref.ambiguousWith.slice(0, 5).map((m) => ` ${formatSessionSummaryLine(m)}`)].join("\n")
16711
+ });
16712
+ }
16226
16713
  ctx.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
16227
16714
  const loadedSession = ctx.sessions.current;
16228
16715
  if (loadedSession && loadedSession.messages.length > 0) {
@@ -16252,52 +16739,81 @@ async function handleSession(args, ctx) {
16252
16739
  return;
16253
16740
  }
16254
16741
  if (sub === "list") {
16255
- ctx.sendSessionList();
16742
+ if (args.length <= 1) {
16743
+ ctx.sendSessionList();
16744
+ return;
16745
+ }
16746
+ const opts = parseSessionListArgs(args);
16747
+ const all = ctx.sessions.listSessions({ includeFileSize: sessionListNeedsFileSize(opts) });
16748
+ if (all.length === 0) {
16749
+ ctx.send({ type: "info", message: "No saved sessions." });
16750
+ return;
16751
+ }
16752
+ const view = selectSessions(all, opts);
16753
+ if (view.totalCount === 0) {
16754
+ ctx.send({
16755
+ type: "info",
16756
+ message: [...opts.warnings.map((w) => `\u26A0 ${w}`), "No sessions match the filter criteria."].join("\n")
16757
+ });
16758
+ return;
16759
+ }
16760
+ if (opts.json) {
16761
+ ctx.send({
16762
+ type: "info",
16763
+ message: JSON.stringify(view.shown.map((s) => sessionListJsonRow(s, view.needsFileSize)), null, 2)
16764
+ });
16765
+ return;
16766
+ }
16767
+ const lines = [
16768
+ ...opts.warnings.map((w) => `\u26A0 ${w}`),
16769
+ ...view.shown.map((s) => formatSessionSummaryLine(s)),
16770
+ `\u2500\u2500 ${sessionListSummary(view)}`
16771
+ ];
16772
+ if (view.shown.length < view.totalCount) lines.push("Use --all to show all, or --limit N to change count.");
16773
+ ctx.send({ type: "info", message: lines.join("\n") });
16256
16774
  return;
16257
16775
  }
16258
16776
  if (sub === "delete" && args[1]) {
16259
16777
  const targetId = args[1];
16260
16778
  if (targetId === "--filter") {
16261
- let filterMessages;
16262
- for (let i = 1; i < args.length; i++) {
16263
- if (args[i] === "--filter" && i + 1 < args.length) {
16264
- const val = args[i + 1];
16265
- i++;
16266
- const eq = val.indexOf("=");
16267
- if (eq > 0) {
16268
- const key = val.slice(0, eq);
16269
- const vals = val.slice(eq + 1).split(",").map((s) => s.trim().toLowerCase());
16270
- if (key === "messages") {
16271
- const n = Number(vals[0]);
16272
- if (Number.isFinite(n) && n >= 0) filterMessages = n;
16273
- }
16274
- }
16275
- }
16276
- }
16277
- if (filterMessages === void 0) {
16278
- ctx.send({ type: "error", message: "Usage: /session delete --filter messages=N" });
16779
+ const opts = parseBatchDeleteArgs(args);
16780
+ if (opts.error) {
16781
+ ctx.send({ type: "error", message: opts.error });
16279
16782
  return;
16280
16783
  }
16281
- const list2 = ctx.sessions.listSessions();
16282
- const matches2 = list2.filter((s) => s.messageCount === filterMessages);
16784
+ const matches2 = ctx.sessions.listSessions().filter((s) => s.messageCount === opts.messages);
16283
16785
  if (matches2.length === 0) {
16284
- ctx.send({ type: "info", message: `No sessions with ${filterMessages} message(s) found.` });
16786
+ ctx.send({ type: "info", message: `No sessions with ${opts.messages} message(s) found.` });
16787
+ return;
16788
+ }
16789
+ const preview = [
16790
+ `Found ${matches2.length} session(s) with ${opts.messages} message(s):`,
16791
+ ...matches2.slice(0, 20).map((s) => ` ${formatSessionSummaryLine(s)}`),
16792
+ ...matches2.length > 20 ? [` ... and ${matches2.length - 20} more`] : []
16793
+ ];
16794
+ if (opts.dryRun) {
16795
+ ctx.send({
16796
+ type: "info",
16797
+ message: [...preview, "--dry-run: no sessions were deleted. Remove --dry-run to actually delete."].join("\n")
16798
+ });
16285
16799
  return;
16286
16800
  }
16287
16801
  let deleted = 0;
16288
16802
  let failed = 0;
16289
16803
  for (const s of matches2) {
16290
- if (ctx.sessions.deleteSession(s.id)) {
16291
- deleted++;
16292
- } else {
16293
- failed++;
16294
- }
16804
+ if (ctx.sessions.deleteSession(s.id)) deleted++;
16805
+ else failed++;
16295
16806
  }
16296
16807
  if (!ctx.sessions.current) {
16297
- ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
16808
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
16809
+ ctx.unsavedSessions.set(created.id, created);
16298
16810
  ctx.resetWebSessionUsage();
16299
16811
  }
16300
- ctx.send({ type: "info", message: `Deleted ${deleted} session(s) with ${filterMessages} message(s)${failed > 0 ? `, ${failed} failed` : ""}.` });
16812
+ ctx.send({
16813
+ type: "info",
16814
+ message: [...preview, `Deleted ${deleted} session(s)${failed > 0 ? `, ${failed} failed` : ""}.`].join("\n")
16815
+ });
16816
+ ctx.sendStatus();
16301
16817
  ctx.sendSessionList();
16302
16818
  return;
16303
16819
  }
@@ -16306,20 +16822,79 @@ async function handleSession(args, ctx) {
16306
16822
  ctx.unsavedSessions.delete(cachedKey);
16307
16823
  if (ctx.sessions.current?.id === cachedKey) {
16308
16824
  ctx.sessions.clearCurrent();
16825
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
16826
+ ctx.unsavedSessions.set(created.id, created);
16827
+ ctx.resetWebSessionUsage();
16828
+ ctx.sendStatus();
16309
16829
  }
16310
16830
  ctx.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
16311
16831
  ctx.sendSessionList();
16312
16832
  return;
16313
16833
  }
16314
- const list = ctx.sessions.listSessions();
16315
- const found = list.find((s) => s.id.startsWith(targetId));
16316
- if (found) {
16317
- ctx.sessions.deleteSession(found.id);
16318
- ctx.send({ type: "info", message: `Deleted session: ${found.id.slice(0, 8)}` });
16319
- ctx.sendSessionList();
16320
- } else {
16834
+ const ref = resolveSessionRef(ctx.sessions.listSessions(), targetId, "strict");
16835
+ if (ref.status === "not-found") {
16321
16836
  ctx.send({ type: "error", message: `Session not found: ${targetId}` });
16837
+ return;
16838
+ }
16839
+ if (ref.status === "ambiguous") {
16840
+ ctx.send({
16841
+ type: "error",
16842
+ message: [ref.message, ...ref.matches.slice(0, 10).map((m) => ` ${formatSessionSummaryLine(m)}`)].join("\n")
16843
+ });
16844
+ return;
16845
+ }
16846
+ const wasCurrent = ctx.sessions.current?.id === ref.match.id;
16847
+ if (!ctx.sessions.deleteSession(ref.match.id)) {
16848
+ ctx.send({ type: "error", message: "Delete failed \u2014 session file not found on disk." });
16849
+ return;
16322
16850
  }
16851
+ ctx.send({ type: "info", message: `Deleted session: ${ref.match.id.slice(0, 8)}` });
16852
+ if (wasCurrent) {
16853
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
16854
+ ctx.unsavedSessions.set(created.id, created);
16855
+ ctx.resetWebSessionUsage();
16856
+ ctx.send({ type: "info", message: "Current session was deleted \u2014 started a fresh session." });
16857
+ ctx.sendStatus();
16858
+ }
16859
+ ctx.sendSessionList();
16860
+ return;
16861
+ }
16862
+ if (sub === "clean") {
16863
+ const opts = parseCleanArgs(args);
16864
+ const stale = selectStaleSessions(ctx.sessions.listSessions(), opts.staleDays);
16865
+ if (stale.length === 0) {
16866
+ ctx.send({ type: "info", message: `No sessions older than ${opts.staleDays} days found.` });
16867
+ return;
16868
+ }
16869
+ const preview = [
16870
+ `Found ${stale.length} session(s) not updated in ${opts.staleDays}+ days:`,
16871
+ ...stale.slice(0, 20).map((s) => ` ${formatSessionSummaryLine(s)}`),
16872
+ ...stale.length > 20 ? [` ... and ${stale.length - 20} more`] : []
16873
+ ];
16874
+ if (opts.dryRun) {
16875
+ ctx.send({
16876
+ type: "info",
16877
+ message: [...preview, "--dry-run: no sessions were deleted. Remove --dry-run to actually delete."].join("\n")
16878
+ });
16879
+ return;
16880
+ }
16881
+ let deleted = 0;
16882
+ let failed = 0;
16883
+ for (const s of stale) {
16884
+ if (ctx.sessions.deleteSession(s.id)) deleted++;
16885
+ else failed++;
16886
+ }
16887
+ if (!ctx.sessions.current) {
16888
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
16889
+ ctx.unsavedSessions.set(created.id, created);
16890
+ ctx.resetWebSessionUsage();
16891
+ ctx.sendStatus();
16892
+ }
16893
+ ctx.send({
16894
+ type: "info",
16895
+ message: [...preview, `Cleaned up: ${deleted} deleted${failed > 0 ? `, ${failed} failed` : ""}.`].join("\n")
16896
+ });
16897
+ ctx.sendSessionList();
16323
16898
  return;
16324
16899
  }
16325
16900
  if (sub === "batch-delete") {
@@ -16343,6 +16918,12 @@ async function handleSession(args, ctx) {
16343
16918
  deleted++;
16344
16919
  }
16345
16920
  }
16921
+ if (!ctx.sessions.current) {
16922
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
16923
+ ctx.unsavedSessions.set(created.id, created);
16924
+ ctx.resetWebSessionUsage();
16925
+ ctx.sendStatus();
16926
+ }
16346
16927
  ctx.send({ type: "info", message: `Deleted ${deleted} session(s).` });
16347
16928
  ctx.sendSessionList();
16348
16929
  return;
@@ -16356,26 +16937,40 @@ async function handleSession(args, ctx) {
16356
16937
  }
16357
16938
  const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
16358
16939
  if (cachedKey) {
16359
- const session = ctx.unsavedSessions.get(cachedKey);
16360
- session.title = newTitle;
16940
+ const session2 = ctx.unsavedSessions.get(cachedKey);
16941
+ session2.title = newTitle;
16361
16942
  ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
16362
16943
  ctx.sendSessionList();
16363
16944
  return;
16364
16945
  }
16365
- const list = ctx.sessions.listSessions();
16366
- const found = list.find((s) => s.id.startsWith(targetId));
16367
- if (found) {
16368
- const session = ctx.sessions.loadSession(found.id);
16369
- session.title = newTitle;
16370
- ctx.sessions.save();
16371
- ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
16372
- ctx.sendSessionList();
16373
- } else {
16946
+ const ref = resolveSessionRef(ctx.sessions.listSessions(), targetId, "strict");
16947
+ if (ref.status === "not-found") {
16374
16948
  ctx.send({ type: "error", message: `Session not found: ${targetId}` });
16949
+ return;
16950
+ }
16951
+ if (ref.status === "ambiguous") {
16952
+ ctx.send({
16953
+ type: "error",
16954
+ message: [ref.message, ...ref.matches.slice(0, 10).map((m) => ` ${formatSessionSummaryLine(m)}`)].join("\n")
16955
+ });
16956
+ return;
16375
16957
  }
16958
+ const priorId = ctx.sessions.current?.id ?? null;
16959
+ const isCurrent = priorId === ref.match.id;
16960
+ const session = isCurrent ? ctx.sessions.current : ctx.sessions.loadSession(ref.match.id);
16961
+ session.title = newTitle;
16962
+ ctx.sessions.save();
16963
+ if (!isCurrent && priorId) {
16964
+ ctx.sessions.loadSession(priorId);
16965
+ }
16966
+ ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
16967
+ ctx.sendSessionList();
16376
16968
  return;
16377
16969
  }
16378
- ctx.send({ type: "info", message: "Usage: /session new | list | load <id> | delete <id> | delete --filter messages=N | rename <id> <title>" });
16970
+ ctx.send({
16971
+ type: "info",
16972
+ message: "Usage: /session new | list [--filter k=v] [--search kw] [--limit N|--all] [--sort f[:o]] [--json] | load <id> | delete <id> | delete --filter messages=N [--dry-run] | clean --stale N [--dry-run] | rename <id> <title>"
16973
+ });
16379
16974
  }
16380
16975
  async function handleExport(args, ctx) {
16381
16976
  const format = args[0] === "json" ? "json" : "md";
@@ -16988,7 +17583,7 @@ async function handleTest(args, ctx) {
16988
17583
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
16989
17584
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
16990
17585
  }
16991
- const runTests = ctx.runTests ?? (await import("./run-tests-USV7IJ7X.js")).executeTests;
17586
+ const runTests = ctx.runTests ?? (await import("./run-tests-HMHHIG7Y.js")).executeTests;
16992
17587
  const report = await runTests(testArgs);
16993
17588
  ctx.send({ type: "info", message: report });
16994
17589
  } catch (err) {
@@ -17324,7 +17919,7 @@ async function handleMemory(args, ctx) {
17324
17919
  ctx.handleMemoryManage(sub, args[1], sub === "expire" ? args[2] : void 0);
17325
17920
  } else if (sub === "export") {
17326
17921
  if (args[1] === "md") {
17327
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-ZFR5GI7D.js");
17922
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-QZN4Q3W4.js");
17328
17923
  ctx.send({
17329
17924
  type: "export_data",
17330
17925
  format: "md",
@@ -17766,8 +18361,9 @@ async function handleHelp(_args, ctx) {
17766
18361
  " /clear \u2014 Clear conversation & start new session",
17767
18362
  " /compact [hint] \u2014 Compress conversation history",
17768
18363
  " /think [on|off] \u2014 Toggle extended thinking mode",
17769
- " /plan [enter|exit] \u2014 Toggle read-only planning mode",
17770
- " /session new|list|load|delete <id> \u2014 Session management",
18364
+ " /plan [execute|exit|status] \u2014 Read-only planning mode (bare /plan enters it)",
18365
+ " /route [on|off|show|test <msg>] \u2014 Smart model routing",
18366
+ " /session new|list [filters]|load <id>|delete <id>|delete --filter messages=N [--dry-run]|clean --stale N [--dry-run]|rename <id> <title> \u2014 Session management",
17771
18367
  " /system [prompt|clear] \u2014 Set/view/reset system prompt",
17772
18368
  " /context [status|reload] \u2014 Show/reload context layers",
17773
18369
  " /status \u2014 Show session info & token usage",
@@ -17903,8 +18499,9 @@ var CLI_ONLY_WEB_COMMANDS = /* @__PURE__ */ new Map([
17903
18499
  ["snapshot", "Dev-state snapshots are written when the terminal REPL exits."],
17904
18500
  ["hooks", "Hook trust is granted per machine from the terminal."],
17905
18501
  ["hook", "Hook trust is granted per machine from the terminal."],
17906
- // 以下两条并非「终端才有意义」,而是 Web 侧尚未实现——如实说明,别假装是设计。
17907
- ["route", "Smart model routing is not applied to Web sessions yet (config.routing is REPL-only)."],
18502
+ // 以下一条并非「终端才有意义」,而是 Web 侧尚未实现——如实说明,别假装是设计。
18503
+ // (`route` 曾在这里,理由写的是「Web 尚未应用智能路由」;v0.4.269 让 routing 在 Web
18504
+ // 上真正生效之后那句话就不再成立,命令已在 Web 实现,见 commands/provider-commands.ts。)
17908
18505
  ["security", "Redaction status and toggles are terminal-only for now; /status shows the current profile."]
17909
18506
  ]);
17910
18507
  var webCommandHandlers = {
@@ -17912,6 +18509,7 @@ var webCommandHandlers = {
17912
18509
  model: handleModel,
17913
18510
  think: handleThink,
17914
18511
  plan: handlePlan,
18512
+ route: handleRoute,
17915
18513
  clear: handleClear,
17916
18514
  compact: handleCompact,
17917
18515
  status: handleStatus,