iosm-cli 0.2.12 → 0.2.14

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 (102) hide show
  1. package/.npmignore +2 -0
  2. package/CHANGELOG.md +60 -0
  3. package/README.md +16 -4
  4. package/dist/cli/args.d.ts +1 -1
  5. package/dist/cli/args.d.ts.map +1 -1
  6. package/dist/cli/args.js +9 -2
  7. package/dist/cli/args.js.map +1 -1
  8. package/dist/core/agent-session.d.ts +2 -0
  9. package/dist/core/agent-session.d.ts.map +1 -1
  10. package/dist/core/agent-session.js +80 -1
  11. package/dist/core/agent-session.js.map +1 -1
  12. package/dist/core/auth-storage.d.ts.map +1 -1
  13. package/dist/core/auth-storage.js +17 -2
  14. package/dist/core/auth-storage.js.map +1 -1
  15. package/dist/core/background-processes.d.ts +11 -0
  16. package/dist/core/background-processes.d.ts.map +1 -1
  17. package/dist/core/background-processes.js +115 -9
  18. package/dist/core/background-processes.js.map +1 -1
  19. package/dist/core/command-dispatcher.d.ts +16 -0
  20. package/dist/core/command-dispatcher.d.ts.map +1 -0
  21. package/dist/core/command-dispatcher.js +678 -0
  22. package/dist/core/command-dispatcher.js.map +1 -0
  23. package/dist/core/model-registry.d.ts.map +1 -1
  24. package/dist/core/model-registry.js +13 -1
  25. package/dist/core/model-registry.js.map +1 -1
  26. package/dist/core/model-resolver.d.ts +2 -2
  27. package/dist/core/model-resolver.d.ts.map +1 -1
  28. package/dist/core/model-resolver.js +1 -2
  29. package/dist/core/model-resolver.js.map +1 -1
  30. package/dist/core/openrouter-model-catalog.d.ts +9 -0
  31. package/dist/core/openrouter-model-catalog.d.ts.map +1 -0
  32. package/dist/core/openrouter-model-catalog.js +139 -0
  33. package/dist/core/openrouter-model-catalog.js.map +1 -0
  34. package/dist/core/provider-policy.d.ts +7 -0
  35. package/dist/core/provider-policy.d.ts.map +1 -0
  36. package/dist/core/provider-policy.js +19 -0
  37. package/dist/core/provider-policy.js.map +1 -0
  38. package/dist/core/settings-manager.d.ts +27 -0
  39. package/dist/core/settings-manager.d.ts.map +1 -1
  40. package/dist/core/settings-manager.js +38 -0
  41. package/dist/core/settings-manager.js.map +1 -1
  42. package/dist/core/slash-commands.d.ts.map +1 -1
  43. package/dist/core/slash-commands.js +13 -2
  44. package/dist/core/slash-commands.js.map +1 -1
  45. package/dist/core/subagent-background-runs.d.ts +56 -0
  46. package/dist/core/subagent-background-runs.d.ts.map +1 -0
  47. package/dist/core/subagent-background-runs.js +275 -0
  48. package/dist/core/subagent-background-runs.js.map +1 -0
  49. package/dist/core/system-prompt.d.ts.map +1 -1
  50. package/dist/core/system-prompt.js +3 -0
  51. package/dist/core/system-prompt.js.map +1 -1
  52. package/dist/core/tools/task.d.ts.map +1 -1
  53. package/dist/core/tools/task.js +39 -35
  54. package/dist/core/tools/task.js.map +1 -1
  55. package/dist/core/usage-cost.d.ts +4 -0
  56. package/dist/core/usage-cost.d.ts.map +1 -0
  57. package/dist/core/usage-cost.js +28 -0
  58. package/dist/core/usage-cost.js.map +1 -0
  59. package/dist/main.d.ts.map +1 -1
  60. package/dist/main.js +16 -2
  61. package/dist/main.js.map +1 -1
  62. package/dist/modes/index.d.ts +1 -0
  63. package/dist/modes/index.d.ts.map +1 -1
  64. package/dist/modes/index.js +1 -0
  65. package/dist/modes/index.js.map +1 -1
  66. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  67. package/dist/modes/interactive/components/footer.js +7 -5
  68. package/dist/modes/interactive/components/footer.js.map +1 -1
  69. package/dist/modes/interactive/components/login-dialog.d.ts +1 -1
  70. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/login-dialog.js +1 -4
  72. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  73. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  74. package/dist/modes/interactive/components/oauth-selector.js +1 -2
  75. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  76. package/dist/modes/interactive/interactive-mode.d.ts +26 -0
  77. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  78. package/dist/modes/interactive/interactive-mode.js +899 -47
  79. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  80. package/dist/modes/rpc/rpc-client.d.ts +11 -1
  81. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  82. package/dist/modes/rpc/rpc-client.js +54 -0
  83. package/dist/modes/rpc/rpc-client.js.map +1 -1
  84. package/dist/modes/rpc/rpc-mode.d.ts +1 -1
  85. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  86. package/dist/modes/rpc/rpc-mode.js +87 -3
  87. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  88. package/dist/modes/rpc/rpc-types.d.ts +69 -0
  89. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  90. package/dist/modes/rpc/rpc-types.js.map +1 -1
  91. package/dist/modes/telegram/telegram-bridge-mode.d.ts +15 -0
  92. package/dist/modes/telegram/telegram-bridge-mode.d.ts.map +1 -0
  93. package/dist/modes/telegram/telegram-bridge-mode.js +2135 -0
  94. package/dist/modes/telegram/telegram-bridge-mode.js.map +1 -0
  95. package/docs/cli-reference.md +20 -3
  96. package/docs/configuration.md +27 -3
  97. package/docs/interactive-mode.md +15 -2
  98. package/docs/rpc-json-sdk.md +23 -0
  99. package/docs/sessions-traces-export.md +2 -2
  100. package/examples/extensions/README.md +1 -2
  101. package/package.json +4 -3
  102. package/examples/extensions/antigravity-image-gen.ts +0 -415
@@ -19,6 +19,8 @@ import { MAX_ORCHESTRATION_AGENTS, MAX_ORCHESTRATION_PARALLEL, MAX_SUBAGENT_DELE
19
19
  import { loadModelsDevProviderCatalog, } from "../../core/models-dev-provider-catalog.js";
20
20
  import { ModelRegistry } from "../../core/model-registry.js";
21
21
  import { MODELS_DEV_PROVIDERS } from "../../core/models-dev-providers.js";
22
+ import { loadOpenRouterProviderConfig } from "../../core/openrouter-model-catalog.js";
23
+ import { isProviderAllowed } from "../../core/provider-policy.js";
22
24
  import { resolveModelScope } from "../../core/model-resolver.js";
23
25
  import { getMcpCommandHelp, parseMcpAddCommand, parseMcpTargetCommand, } from "../../core/mcp/index.js";
24
26
  import { addMemoryEntry, getMemoryFilePath, readMemoryEntries, removeMemoryEntry, updateMemoryEntry, } from "../../core/memory.js";
@@ -28,12 +30,14 @@ import { buildProjectIndex, collectChangedFilesSince, ensureProjectIndex, loadPr
28
30
  import { SingularService, } from "../../core/singular.js";
29
31
  import { getDefaultSemanticSearchConfig, getSemanticConfigPath, getSemanticIndexDir, isLikelyEmbeddingModelId, listOllamaLocalModels, listOpenRouterEmbeddingModels, loadMergedSemanticConfig, readScopedSemanticConfig, SemanticConfigMissingError, SemanticIndexRequiredError, SemanticRebuildRequiredError, SemanticSearchRuntime, upsertScopedSemanticSearchConfig, } from "../../core/semantic/index.js";
30
32
  import { DefaultResourceLoader } from "../../core/resource-loader.js";
33
+ import { DefaultPackageManager } from "../../core/package-manager.js";
31
34
  import { createAgentSession } from "../../core/sdk.js";
32
35
  import { createTeamRun, getTeamRun, listTeamRuns } from "../../core/agent-teams.js";
33
36
  import { buildSwarmPlanFromSingular, buildSwarmPlanFromTask, runSwarmScheduler, SwarmStateStore, } from "../../core/swarm/index.js";
34
37
  import { loadCustomSubagents, resolveCustomSubagentReference, } from "../../core/subagents.js";
35
38
  import { getSubagentRun, listSubagentRuns } from "../../core/subagent-runs.js";
36
- import { getBackgroundProcess, listBackgroundProcesses, readBackgroundProcessLogTail, stopBackgroundProcess, } from "../../core/background-processes.js";
39
+ import { getBackgroundProcess, listBackgroundProcesses, pruneBackgroundProcesses, readBackgroundProcessLogTail, stopBackgroundProcess, } from "../../core/background-processes.js";
40
+ import { getSubagentBackgroundRun, listSubagentBackgroundRuns, pruneSubagentBackgroundRuns, readSubagentBackgroundRunLogTail, requestStopAllSubagentBackgroundRuns, requestStopSubagentBackgroundRun, } from "../../core/subagent-background-runs.js";
37
41
  import { SessionManager } from "../../core/session-manager.js";
38
42
  import { SettingsManager } from "../../core/settings-manager.js";
39
43
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
@@ -798,7 +802,6 @@ function resolveDoctorCliToolStatuses() {
798
802
  const OPENROUTER_PROVIDER_ID = "openrouter";
799
803
  const PROVIDER_DISPLAY_NAME_OVERRIDES = {
800
804
  "azure-openai-responses": "Azure OpenAI Responses",
801
- "google-antigravity": "Google Antigravity",
802
805
  "google-gemini-cli": "Google Gemini CLI",
803
806
  "kimi-coding": "Kimi Coding",
804
807
  "openai-codex": "OpenAI Codex",
@@ -996,8 +999,8 @@ export class InteractiveMode {
996
999
  this.asciiLogo = undefined;
997
1000
  // API-key provider labels cached for /login and status messages.
998
1001
  this.apiKeyProviderDisplayNames = new Map();
999
- this.modelsDevProviderCatalog = MODELS_DEV_PROVIDERS;
1000
- this.modelsDevProviderCatalogById = new Map(MODELS_DEV_PROVIDERS.map((provider) => [
1002
+ this.modelsDevProviderCatalog = MODELS_DEV_PROVIDERS.filter((provider) => isProviderAllowed(provider.id));
1003
+ this.modelsDevProviderCatalogById = new Map(MODELS_DEV_PROVIDERS.filter((provider) => isProviderAllowed(provider.id)).map((provider) => [
1001
1004
  provider.id,
1002
1005
  {
1003
1006
  ...provider,
@@ -1005,6 +1008,7 @@ export class InteractiveMode {
1005
1008
  },
1006
1009
  ]));
1007
1010
  this.modelsDevProviderCatalogRefreshPromise = undefined;
1011
+ this.openRouterModelCatalogRefreshPromise = undefined;
1008
1012
  // Custom header from extension (undefined = use built-in header)
1009
1013
  this.customHeader = undefined;
1010
1014
  // Active selector shown in editor container (used to restore UI after temporary dialogs)
@@ -3326,6 +3330,11 @@ export class InteractiveMode {
3326
3330
  await this.handleBackgroundProcessesSlashCommand(text);
3327
3331
  return;
3328
3332
  }
3333
+ if (text === "/extensions" || text.startsWith("/extensions ") || text === "/ext" || text.startsWith("/ext ")) {
3334
+ this.editor.setText("");
3335
+ await this.handleExtensionsSlashCommand(text);
3336
+ return;
3337
+ }
3329
3338
  if (text === "/team-runs" || text.startsWith("/team-runs ")) {
3330
3339
  this.editor.setText("");
3331
3340
  this.handleTeamRunsSlashCommand(text);
@@ -6423,8 +6432,8 @@ export class InteractiveMode {
6423
6432
  }
6424
6433
  this.modelsDevProviderCatalogRefreshPromise = (async () => {
6425
6434
  const catalog = await loadModelsDevProviderCatalog();
6426
- this.modelsDevProviderCatalogById = catalog;
6427
- this.modelsDevProviderCatalog = Array.from(catalog.values())
6435
+ this.modelsDevProviderCatalogById = new Map(Array.from(catalog.entries()).filter(([providerId]) => isProviderAllowed(providerId)));
6436
+ this.modelsDevProviderCatalog = Array.from(this.modelsDevProviderCatalogById.values())
6428
6437
  .map((provider) => ({
6429
6438
  id: provider.id,
6430
6439
  name: provider.name,
@@ -6433,8 +6442,8 @@ export class InteractiveMode {
6433
6442
  .sort((a, b) => a.name.localeCompare(b.name, "en") || a.id.localeCompare(b.id, "en"));
6434
6443
  })()
6435
6444
  .catch(() => {
6436
- this.modelsDevProviderCatalog = MODELS_DEV_PROVIDERS;
6437
- this.modelsDevProviderCatalogById = new Map(MODELS_DEV_PROVIDERS.map((provider) => [
6445
+ this.modelsDevProviderCatalog = MODELS_DEV_PROVIDERS.filter((provider) => isProviderAllowed(provider.id));
6446
+ this.modelsDevProviderCatalogById = new Map(MODELS_DEV_PROVIDERS.filter((provider) => isProviderAllowed(provider.id)).map((provider) => [
6438
6447
  provider.id,
6439
6448
  {
6440
6449
  ...provider,
@@ -6506,6 +6515,35 @@ export class InteractiveMode {
6506
6515
  return false;
6507
6516
  }
6508
6517
  }
6518
+ async hydrateOpenRouterModelsFromApi(options) {
6519
+ const forceRefresh = options?.forceRefresh === true;
6520
+ if (!forceRefresh && this.hasRegisteredProviderModels(OPENROUTER_PROVIDER_ID))
6521
+ return true;
6522
+ if (forceRefresh && this.isProviderConfiguredInModelsJson(OPENROUTER_PROVIDER_ID)) {
6523
+ return this.hasRegisteredProviderModels(OPENROUTER_PROVIDER_ID);
6524
+ }
6525
+ if (this.openRouterModelCatalogRefreshPromise) {
6526
+ return this.openRouterModelCatalogRefreshPromise;
6527
+ }
6528
+ this.openRouterModelCatalogRefreshPromise = (async () => {
6529
+ const apiKey = await this.session.modelRegistry.authStorage
6530
+ .getApiKey(OPENROUTER_PROVIDER_ID)
6531
+ .catch(() => undefined);
6532
+ const config = await loadOpenRouterProviderConfig({ apiKey });
6533
+ if (!config || !config.models || config.models.length === 0)
6534
+ return false;
6535
+ try {
6536
+ this.session.modelRegistry.registerProvider(OPENROUTER_PROVIDER_ID, config);
6537
+ return this.hasRegisteredProviderModels(OPENROUTER_PROVIDER_ID);
6538
+ }
6539
+ catch {
6540
+ return false;
6541
+ }
6542
+ })().finally(() => {
6543
+ this.openRouterModelCatalogRefreshPromise = undefined;
6544
+ });
6545
+ return this.openRouterModelCatalogRefreshPromise;
6546
+ }
6509
6547
  async hydrateProviderModelsFromModelsDev(providerId, options) {
6510
6548
  const forceRefresh = options?.forceRefresh === true;
6511
6549
  if (!forceRefresh && this.hasRegisteredProviderModels(providerId))
@@ -6513,6 +6551,11 @@ export class InteractiveMode {
6513
6551
  if (forceRefresh && this.isProviderConfiguredInModelsJson(providerId)) {
6514
6552
  return this.hasRegisteredProviderModels(providerId);
6515
6553
  }
6554
+ if (providerId === OPENROUTER_PROVIDER_ID) {
6555
+ const hydratedFromOpenRouter = await this.hydrateOpenRouterModelsFromApi({ forceRefresh });
6556
+ if (hydratedFromOpenRouter)
6557
+ return true;
6558
+ }
6516
6559
  if (!options?.skipCatalogRefresh) {
6517
6560
  await this.refreshModelsDevProviderCatalog();
6518
6561
  }
@@ -6531,8 +6574,11 @@ export class InteractiveMode {
6531
6574
  }
6532
6575
  }
6533
6576
  async hydrateMissingProviderModelsForSavedAuth(options) {
6534
- const savedProviders = this.session.modelRegistry.authStorage.list();
6535
- if (savedProviders.length === 0)
6577
+ const savedProviders = new Set(this.session.modelRegistry.authStorage.list());
6578
+ if (this.session.modelRegistry.authStorage.hasAuth(OPENROUTER_PROVIDER_ID)) {
6579
+ savedProviders.add(OPENROUTER_PROVIDER_ID);
6580
+ }
6581
+ if (savedProviders.size === 0)
6536
6582
  return;
6537
6583
  await this.refreshModelsDevProviderCatalog();
6538
6584
  const forceRefresh = options?.forceRefresh === true;
@@ -6568,11 +6614,15 @@ export class InteractiveMode {
6568
6614
  const providerNames = new Map();
6569
6615
  this.apiKeyProviderDisplayNames.clear();
6570
6616
  for (const model of this.session.modelRegistry.getAll()) {
6617
+ if (!isProviderAllowed(model.provider))
6618
+ continue;
6571
6619
  if (!providerNames.has(model.provider)) {
6572
6620
  providerNames.set(model.provider, toProviderDisplayName(model.provider));
6573
6621
  }
6574
6622
  }
6575
6623
  for (const provider of modelsDevProviders) {
6624
+ if (!isProviderAllowed(provider.id))
6625
+ continue;
6576
6626
  const fallbackName = toProviderDisplayName(provider.id);
6577
6627
  const current = providerNames.get(provider.id);
6578
6628
  if (!current || current === fallbackName) {
@@ -6580,6 +6630,8 @@ export class InteractiveMode {
6580
6630
  }
6581
6631
  }
6582
6632
  for (const providerId of this.session.modelRegistry.authStorage.list()) {
6633
+ if (!isProviderAllowed(providerId))
6634
+ continue;
6583
6635
  if (!providerNames.has(providerId)) {
6584
6636
  providerNames.set(providerId, toProviderDisplayName(providerId));
6585
6637
  }
@@ -6656,7 +6708,7 @@ export class InteractiveMode {
6656
6708
  // Providers that use callback servers (can paste redirect URL)
6657
6709
  const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
6658
6710
  // Create login dialog component
6659
- const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
6711
+ const dialog = new LoginDialogComponent(this.ui, providerName, (_success, _message) => {
6660
6712
  // Completion handled below
6661
6713
  });
6662
6714
  // Show dialog in editor container
@@ -12372,15 +12424,251 @@ export class InteractiveMode {
12372
12424
  this.ui.requestRender();
12373
12425
  }
12374
12426
  }
12427
+ getSubagentBackgroundUsage() {
12428
+ return "/subagent-runs bg [list|running|queued|done|error|cancelled] [limit: 1..200] | /subagent-runs bg status <id> | /subagent-runs bg logs <id> [lines: 1..1000] | /subagent-runs bg stop <id> | /subagent-runs bg stop-all | /subagent-runs bg prune [hours: 1..2160]";
12429
+ }
12430
+ getSubagentBackgroundStatusWeight(status) {
12431
+ switch (status) {
12432
+ case "running":
12433
+ return 0;
12434
+ case "queued":
12435
+ return 1;
12436
+ case "error":
12437
+ return 2;
12438
+ case "cancelled":
12439
+ return 3;
12440
+ case "done":
12441
+ return 4;
12442
+ default:
12443
+ return 5;
12444
+ }
12445
+ }
12446
+ formatSubagentBackgroundStatusLabel(status) {
12447
+ switch (status) {
12448
+ case "running":
12449
+ return "RUNNING";
12450
+ case "queued":
12451
+ return "QUEUED";
12452
+ case "done":
12453
+ return "DONE";
12454
+ case "error":
12455
+ return "ERROR";
12456
+ case "cancelled":
12457
+ return "CANCELLED";
12458
+ default:
12459
+ return "UNKNOWN";
12460
+ }
12461
+ }
12462
+ sortSubagentBackgroundRecords(records) {
12463
+ return [...records].sort((left, right) => {
12464
+ const byStatus = this.getSubagentBackgroundStatusWeight(left.status) - this.getSubagentBackgroundStatusWeight(right.status);
12465
+ if (byStatus !== 0)
12466
+ return byStatus;
12467
+ return right.createdAt.localeCompare(left.createdAt);
12468
+ });
12469
+ }
12470
+ formatSubagentBackgroundOptionLabel(record, index) {
12471
+ const statusLabel = this.formatSubagentBackgroundStatusLabel(record.status);
12472
+ const age = this.formatRelativeTime(record.createdAt);
12473
+ const runtime = this.formatDurationMs(record.startedAt, record.finishedAt);
12474
+ const profile = record.profile || "-";
12475
+ const agent = record.agent?.trim() ? record.agent : "-";
12476
+ const stopFlag = record.requestedStopAt ? " · stop requested" : "";
12477
+ const description = record.description.length > 80 ? `${record.description.slice(0, 77)}...` : record.description;
12478
+ return `${index + 1}. [${statusLabel}] ${record.runId} · profile=${profile} · agent=${agent} · age=${age} · runtime=${runtime}${stopFlag}\n ${description}`;
12479
+ }
12480
+ buildSubagentBackgroundReport(records) {
12481
+ const counts = {
12482
+ queued: 0,
12483
+ running: 0,
12484
+ done: 0,
12485
+ error: 0,
12486
+ cancelled: 0,
12487
+ };
12488
+ for (const record of records) {
12489
+ counts[record.status] += 1;
12490
+ }
12491
+ const header = `Summary: total=${records.length} · queued=${counts.queued} · running=${counts.running} · done=${counts.done} · error=${counts.error} · cancelled=${counts.cancelled}`;
12492
+ const items = records.map((record, index) => this.formatSubagentBackgroundOptionLabel(record, index));
12493
+ const hints = [
12494
+ "Quick actions:",
12495
+ "- /subagent-runs bg status <id>",
12496
+ "- /subagent-runs bg logs <id> [lines]",
12497
+ "- /subagent-runs bg stop <id>",
12498
+ "- /subagent-runs bg stop-all",
12499
+ "- /subagent-runs bg prune [hours]",
12500
+ ];
12501
+ return [header, "", ...items, "", ...hints].join("\n");
12502
+ }
12503
+ handleSubagentBackgroundRunsSlashCommand(args, cwd) {
12504
+ const usage = this.getSubagentBackgroundUsage();
12505
+ const firstArg = (args[0] ?? "").toLowerCase();
12506
+ const subcommand = firstArg || "list";
12507
+ const listFilters = {
12508
+ list: undefined,
12509
+ running: ["running"],
12510
+ queued: ["queued"],
12511
+ done: ["done"],
12512
+ error: ["error"],
12513
+ failed: ["error"],
12514
+ cancelled: ["cancelled"],
12515
+ };
12516
+ const hasListFilter = Object.prototype.hasOwnProperty.call(listFilters, subcommand);
12517
+ if (hasListFilter || /^\d+$/.test(subcommand)) {
12518
+ const limitRaw = subcommand === "list" ? args[1] : hasListFilter ? args[1] : subcommand;
12519
+ const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 20;
12520
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
12521
+ this.showWarning(`Usage: ${usage}`);
12522
+ return;
12523
+ }
12524
+ const records = this.sortSubagentBackgroundRecords(listSubagentBackgroundRuns(cwd, limit));
12525
+ const statusFilter = hasListFilter ? listFilters[subcommand] : undefined;
12526
+ const filtered = statusFilter && statusFilter.length > 0
12527
+ ? records.filter((record) => statusFilter.includes(record.status))
12528
+ : records;
12529
+ if (filtered.length === 0) {
12530
+ if (statusFilter && statusFilter.length > 0) {
12531
+ this.showStatus(`No background subagent runs found for "${subcommand}" filter.`);
12532
+ }
12533
+ else {
12534
+ this.showStatus("No background subagent runs found.");
12535
+ }
12536
+ return;
12537
+ }
12538
+ this.showCommandTextBlock("Subagent Background Runs", this.buildSubagentBackgroundReport(filtered));
12539
+ return;
12540
+ }
12541
+ if (subcommand === "status") {
12542
+ const runId = args[1];
12543
+ if (!runId) {
12544
+ this.showWarning(`Usage: ${usage}`);
12545
+ return;
12546
+ }
12547
+ const record = getSubagentBackgroundRun(cwd, runId);
12548
+ if (!record) {
12549
+ this.showWarning(`Background subagent run not found: ${runId}`);
12550
+ return;
12551
+ }
12552
+ const lines = [
12553
+ `Run: ${record.runId}`,
12554
+ `Status: ${this.formatSubagentBackgroundStatusLabel(record.status)}${record.requestedStopAt ? " (stop requested)" : ""}`,
12555
+ `Created: ${record.createdAt} (${this.formatRelativeTime(record.createdAt)})`,
12556
+ `Started: ${record.startedAt ?? "-"}`,
12557
+ `Runtime: ${this.formatDurationMs(record.startedAt, record.finishedAt)}`,
12558
+ `Finished: ${record.finishedAt ?? "-"}`,
12559
+ `Profile: ${record.profile}`,
12560
+ `Agent: ${record.agent ?? "-"}`,
12561
+ `Model: ${record.model ?? "-"}`,
12562
+ `Cwd: ${record.cwd}`,
12563
+ `Requested stop: ${record.requestedStopAt ?? "-"}`,
12564
+ `Status file: ${record.metaPath}`,
12565
+ `Log file: ${record.logPath}`,
12566
+ `Transcript: ${record.transcriptPath ?? "-"}`,
12567
+ record.error ? `Error: ${record.error}` : "",
12568
+ "",
12569
+ "Quick actions:",
12570
+ `- /subagent-runs bg logs ${record.runId} 120`,
12571
+ `- /subagent-runs bg stop ${record.runId}`,
12572
+ ].filter((line) => line.length > 0);
12573
+ this.showCommandTextBlock("Subagent Background Status", lines.join("\n"));
12574
+ return;
12575
+ }
12576
+ if (subcommand === "logs") {
12577
+ const runId = args[1];
12578
+ if (!runId) {
12579
+ this.showWarning(`Usage: ${usage}`);
12580
+ return;
12581
+ }
12582
+ const linesRaw = args[2];
12583
+ const tailLines = linesRaw ? Number.parseInt(linesRaw, 10) : 120;
12584
+ if (!Number.isInteger(tailLines) || tailLines < 1 || tailLines > 1000) {
12585
+ this.showWarning(`Usage: ${usage}`);
12586
+ return;
12587
+ }
12588
+ const record = getSubagentBackgroundRun(cwd, runId);
12589
+ if (!record) {
12590
+ this.showWarning(`Background subagent run not found: ${runId}`);
12591
+ return;
12592
+ }
12593
+ const tail = readSubagentBackgroundRunLogTail(cwd, runId, tailLines);
12594
+ if (tail === undefined) {
12595
+ this.showWarning(`Background subagent run not found: ${runId}`);
12596
+ return;
12597
+ }
12598
+ const body = tail.trim().length > 0 ? tail : "(no output yet)";
12599
+ this.showCommandTextBlock("Subagent Background Logs", [
12600
+ `Run: ${record.runId} · status=${this.formatSubagentBackgroundStatusLabel(record.status)} · tail=${tailLines} lines`,
12601
+ `Runtime: ${this.formatDurationMs(record.startedAt, record.finishedAt)} · stop_requested=${record.requestedStopAt ? "yes" : "no"}`,
12602
+ `Description: ${record.description}`,
12603
+ "",
12604
+ body,
12605
+ ].join("\n"));
12606
+ return;
12607
+ }
12608
+ if (subcommand === "stop" || subcommand === "cancel" || subcommand === "kill") {
12609
+ const runId = args[1];
12610
+ if (!runId) {
12611
+ this.showWarning(`Usage: ${usage}`);
12612
+ return;
12613
+ }
12614
+ const updated = requestStopSubagentBackgroundRun(cwd, runId);
12615
+ if (!updated) {
12616
+ this.showWarning(`Background subagent run not found: ${runId}`);
12617
+ return;
12618
+ }
12619
+ this.showStatus(`Stop requested for subagent run ${updated.runId} (status=${this.formatSubagentBackgroundStatusLabel(updated.status)}).`);
12620
+ return;
12621
+ }
12622
+ if (subcommand === "stop-all") {
12623
+ const result = requestStopAllSubagentBackgroundRuns(cwd);
12624
+ if (result.requested === 0) {
12625
+ this.showStatus("No running or queued background subagent runs found.");
12626
+ return;
12627
+ }
12628
+ const lines = [
12629
+ `Stop-all requested for ${result.requested} run(s).`,
12630
+ "",
12631
+ ...result.requestedIds.map((id, index) => `${index + 1}. ${id}`),
12632
+ ];
12633
+ this.showCommandTextBlock("Subagent Background Stop-All", lines.join("\n"));
12634
+ return;
12635
+ }
12636
+ if (subcommand === "prune") {
12637
+ const hoursRaw = args[1];
12638
+ const hours = hoursRaw ? Number.parseInt(hoursRaw, 10) : 24;
12639
+ if (!Number.isInteger(hours) || hours < 1 || hours > 2160) {
12640
+ this.showWarning(`Usage: ${usage}`);
12641
+ return;
12642
+ }
12643
+ const result = pruneSubagentBackgroundRuns(cwd, hours);
12644
+ const lines = [
12645
+ `Threshold: ${result.thresholdHours}h`,
12646
+ `Removed: ${result.removed}`,
12647
+ `Skipped running/queued: ${result.skippedRunning}`,
12648
+ `Skipped recent: ${result.skippedRecent}`,
12649
+ ];
12650
+ if (result.removedIds.length > 0) {
12651
+ lines.push("", ...result.removedIds.map((id, index) => `${index + 1}. ${id}`));
12652
+ }
12653
+ this.showCommandTextBlock("Subagent Background Prune", lines.join("\n"));
12654
+ return;
12655
+ }
12656
+ this.showWarning(`Usage: ${usage}`);
12657
+ }
12375
12658
  handleSubagentRunsSlashCommand(text) {
12376
12659
  const args = this.parseSlashArgs(text).slice(1);
12660
+ const cwd = this.sessionManager.getCwd();
12661
+ const firstArg = (args[0] ?? "").toLowerCase();
12662
+ if (firstArg === "bg" || firstArg === "background") {
12663
+ this.handleSubagentBackgroundRunsSlashCommand(args.slice(1), cwd);
12664
+ return;
12665
+ }
12377
12666
  const limitRaw = args[0];
12378
12667
  const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 20;
12379
12668
  if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
12380
- this.showWarning("Usage: /subagent-runs [limit: 1..200]");
12669
+ this.showWarning("Usage: /subagent-runs [limit: 1..200] | /subagent-runs bg ...");
12381
12670
  return;
12382
12671
  }
12383
- const cwd = this.sessionManager.getCwd();
12384
12672
  const runs = listSubagentRuns(cwd, limit);
12385
12673
  if (runs.length === 0) {
12386
12674
  this.showStatus("No subagent runs found.");
@@ -12461,54 +12749,273 @@ export class InteractiveMode {
12461
12749
  source: "interactive",
12462
12750
  });
12463
12751
  }
12752
+ getBackgroundCommandUsage() {
12753
+ return "/bg [list|running|done|error|terminated|unknown] [limit: 1..200] | /bg status [id] | /bg logs [id] [lines: 1..1000] | /bg stop [id] | /bg stop-all | /bg prune [hours: 1..2160]";
12754
+ }
12755
+ canUseInteractiveSelectors() {
12756
+ return !!this.ui && !!this.editorContainer;
12757
+ }
12758
+ getBackgroundStatusWeight(status) {
12759
+ switch (status) {
12760
+ case "running":
12761
+ return 0;
12762
+ case "unknown":
12763
+ return 1;
12764
+ case "error":
12765
+ return 2;
12766
+ case "done":
12767
+ return 3;
12768
+ case "terminated":
12769
+ return 4;
12770
+ default:
12771
+ return 5;
12772
+ }
12773
+ }
12774
+ formatBackgroundStatusLabel(status) {
12775
+ switch (status) {
12776
+ case "running":
12777
+ return "RUNNING";
12778
+ case "done":
12779
+ return "DONE";
12780
+ case "error":
12781
+ return "ERROR";
12782
+ case "terminated":
12783
+ return "TERMINATED";
12784
+ case "unknown":
12785
+ return "UNKNOWN";
12786
+ default:
12787
+ return "UNKNOWN";
12788
+ }
12789
+ }
12790
+ formatRelativeTime(timestamp) {
12791
+ if (!timestamp)
12792
+ return "-";
12793
+ const parsed = Date.parse(timestamp);
12794
+ if (!Number.isFinite(parsed))
12795
+ return timestamp;
12796
+ const diffMs = Date.now() - parsed;
12797
+ if (diffMs < 0)
12798
+ return "just now";
12799
+ const diffSec = Math.floor(diffMs / 1000);
12800
+ if (diffSec < 60)
12801
+ return `${Math.max(1, diffSec)}s ago`;
12802
+ const diffMin = Math.floor(diffSec / 60);
12803
+ if (diffMin < 60)
12804
+ return `${diffMin}m ago`;
12805
+ const diffHours = Math.floor(diffMin / 60);
12806
+ if (diffHours < 24)
12807
+ return `${diffHours}h ago`;
12808
+ const diffDays = Math.floor(diffHours / 24);
12809
+ return `${diffDays}d ago`;
12810
+ }
12811
+ formatDurationMs(startedAt, finishedAt) {
12812
+ if (!startedAt)
12813
+ return "-";
12814
+ const started = Date.parse(startedAt);
12815
+ if (!Number.isFinite(started))
12816
+ return "-";
12817
+ const end = finishedAt ? Date.parse(finishedAt) : Date.now();
12818
+ if (!Number.isFinite(end) || end < started)
12819
+ return "-";
12820
+ const diffSec = Math.max(0, Math.floor((end - started) / 1000));
12821
+ const hours = Math.floor(diffSec / 3600);
12822
+ const minutes = Math.floor((diffSec % 3600) / 60);
12823
+ const seconds = diffSec % 60;
12824
+ if (hours > 0)
12825
+ return `${hours}h ${minutes}m`;
12826
+ if (minutes > 0)
12827
+ return `${minutes}m ${seconds}s`;
12828
+ return `${seconds}s`;
12829
+ }
12830
+ sortBackgroundRecords(records) {
12831
+ return [...records].sort((left, right) => {
12832
+ const byStatus = this.getBackgroundStatusWeight(left.status) - this.getBackgroundStatusWeight(right.status);
12833
+ if (byStatus !== 0)
12834
+ return byStatus;
12835
+ return right.createdAt.localeCompare(left.createdAt);
12836
+ });
12837
+ }
12464
12838
  formatBackgroundProcessOptionLabel(record, index) {
12465
- const status = `status=${record.status}`;
12466
- const pid = `pid=${record.pid}`;
12467
- const created = record.createdAt ? `created=${record.createdAt}` : "";
12468
- const commandPreview = record.command.length > 64 ? `${record.command.slice(0, 61)}...` : record.command;
12469
- return `${index + 1}. ${record.id} · ${status} · ${pid}${created ? ` · ${created}` : ""} · ${commandPreview}`;
12470
- }
12471
- async pickBackgroundProcessId(cwd, actionLabel) {
12472
- const records = listBackgroundProcesses(cwd, 30);
12839
+ const statusLabel = this.formatBackgroundStatusLabel(record.status);
12840
+ const age = this.formatRelativeTime(record.createdAt);
12841
+ const runtime = this.formatDurationMs(record.startedAt, record.finishedAt);
12842
+ const source = record.source ?? "-";
12843
+ const commandPreview = record.command.length > 72 ? `${record.command.slice(0, 69)}...` : record.command;
12844
+ const stopFlag = record.requestedStopAt ? " · stop requested" : "";
12845
+ return `${index + 1}. [${statusLabel}] ${record.id} · pid=${record.pid} · age=${age} · runtime=${runtime} · source=${source}${stopFlag}\n ${commandPreview}`;
12846
+ }
12847
+ buildBackgroundProcessesReport(records) {
12848
+ const counts = {
12849
+ running: 0,
12850
+ done: 0,
12851
+ error: 0,
12852
+ terminated: 0,
12853
+ unknown: 0,
12854
+ };
12855
+ for (const record of records) {
12856
+ counts[record.status] += 1;
12857
+ }
12858
+ const header = `Summary: total=${records.length} · running=${counts.running} · done=${counts.done} · error=${counts.error} · terminated=${counts.terminated} · unknown=${counts.unknown}`;
12859
+ const items = records.map((record, index) => this.formatBackgroundProcessOptionLabel(record, index));
12860
+ const hints = [
12861
+ "Quick actions:",
12862
+ "- /bg status <id>",
12863
+ "- /bg logs <id> [lines]",
12864
+ "- /bg stop <id>",
12865
+ "- /bg stop-all",
12866
+ "- /bg prune [hours]",
12867
+ ];
12868
+ return [header, "", ...items, "", ...hints].join("\n");
12869
+ }
12870
+ async runBackgroundMenu(cwd) {
12871
+ if (!this.canUseInteractiveSelectors())
12872
+ return false;
12873
+ const records = this.sortBackgroundRecords(listBackgroundProcesses(cwd, 60));
12874
+ const runningCount = records.filter((record) => record.status === "running").length;
12875
+ const selected = await this.showExtensionSelector(`/bg menu · running=${runningCount} · total=${records.length}`, [
12876
+ "List all processes",
12877
+ "List running only",
12878
+ "Show process status",
12879
+ "Show process logs",
12880
+ "Stop process",
12881
+ "Stop all running processes",
12882
+ "Prune old completed records",
12883
+ "Help",
12884
+ "Cancel",
12885
+ ]);
12886
+ if (!selected || selected === "Cancel") {
12887
+ this.showStatus("/bg menu cancelled.");
12888
+ return true;
12889
+ }
12890
+ if (selected === "List all processes") {
12891
+ await this.handleBackgroundProcessesSlashCommand("/bg list");
12892
+ return true;
12893
+ }
12894
+ if (selected === "List running only") {
12895
+ await this.handleBackgroundProcessesSlashCommand("/bg running");
12896
+ return true;
12897
+ }
12898
+ if (selected === "Show process status") {
12899
+ await this.handleBackgroundProcessesSlashCommand("/bg status");
12900
+ return true;
12901
+ }
12902
+ if (selected === "Show process logs") {
12903
+ await this.handleBackgroundProcessesSlashCommand("/bg logs");
12904
+ return true;
12905
+ }
12906
+ if (selected === "Stop process") {
12907
+ await this.handleBackgroundProcessesSlashCommand("/bg stop");
12908
+ return true;
12909
+ }
12910
+ if (selected === "Stop all running processes") {
12911
+ await this.handleBackgroundProcessesSlashCommand("/bg stop-all");
12912
+ return true;
12913
+ }
12914
+ if (selected === "Prune old completed records") {
12915
+ await this.handleBackgroundProcessesSlashCommand("/bg prune");
12916
+ return true;
12917
+ }
12918
+ if (selected === "Help") {
12919
+ await this.handleBackgroundProcessesSlashCommand("/bg help");
12920
+ return true;
12921
+ }
12922
+ return true;
12923
+ }
12924
+ async pickBackgroundProcessId(cwd, actionLabel, options) {
12925
+ const limit = options?.limit ?? 50;
12926
+ const records = this.sortBackgroundRecords(listBackgroundProcesses(cwd, limit));
12473
12927
  if (records.length === 0) {
12474
12928
  this.showStatus("No background processes found.");
12475
12929
  return undefined;
12476
12930
  }
12477
- const options = records.map((record, index) => this.formatBackgroundProcessOptionLabel(record, index));
12478
- const selected = await this.showExtensionSelector(`/bg ${actionLabel}: select process`, options);
12931
+ const preferredStatuses = options?.preferredStatuses;
12932
+ let filtered = records;
12933
+ if (preferredStatuses && preferredStatuses.length > 0) {
12934
+ const allowed = new Set(preferredStatuses);
12935
+ const preferred = records.filter((record) => allowed.has(record.status));
12936
+ if (preferred.length > 0)
12937
+ filtered = preferred;
12938
+ }
12939
+ const pickerOptions = filtered.map((record, index) => this.formatBackgroundProcessOptionLabel(record, index));
12940
+ const selected = await this.showExtensionSelector(`/bg ${actionLabel}: select process`, pickerOptions);
12479
12941
  if (!selected) {
12480
12942
  this.showStatus(`/bg ${actionLabel} cancelled.`);
12481
12943
  return undefined;
12482
12944
  }
12483
- const selectedIndex = options.indexOf(selected);
12484
- return selectedIndex >= 0 ? records[selectedIndex]?.id : undefined;
12945
+ const selectedIndex = pickerOptions.indexOf(selected);
12946
+ return selectedIndex >= 0 ? filtered[selectedIndex]?.id : undefined;
12947
+ }
12948
+ async stopAllBackgroundProcesses(cwd) {
12949
+ const runningRecords = this.sortBackgroundRecords(listBackgroundProcesses(cwd, 200)).filter((record) => record.status === "running");
12950
+ if (runningRecords.length === 0) {
12951
+ this.showStatus("No running background processes found.");
12952
+ return;
12953
+ }
12954
+ if (this.canUseInteractiveSelectors()) {
12955
+ const confirmed = await this.showExtensionConfirm("Stop all background processes?", `Send stop signal to ${runningRecords.length} running process(es).`);
12956
+ if (!confirmed) {
12957
+ this.showStatus("/bg stop-all cancelled.");
12958
+ return;
12959
+ }
12960
+ }
12961
+ const updatedRecords = [];
12962
+ for (const record of runningRecords) {
12963
+ const updated = stopBackgroundProcess(cwd, record.id);
12964
+ if (updated)
12965
+ updatedRecords.push(updated);
12966
+ }
12967
+ const stillRunning = updatedRecords.filter((record) => record.status === "running");
12968
+ const lines = [
12969
+ `Stop-all requested for ${runningRecords.length} process(es).`,
12970
+ `Still running: ${stillRunning.length}`,
12971
+ "",
12972
+ ...updatedRecords.map((record, index) => `${index + 1}. ${record.id} · status=${record.status} · requested_stop=${record.requestedStopAt ?? "-"}`),
12973
+ ];
12974
+ this.showCommandTextBlock("Background Stop-All", lines.join("\n"));
12485
12975
  }
12486
12976
  async handleBackgroundProcessesSlashCommand(text) {
12487
12977
  const args = this.parseSlashArgs(text).slice(1);
12488
12978
  const cwd = this.sessionManager.getCwd();
12979
+ const usage = this.getBackgroundCommandUsage();
12980
+ if (args.length === 0) {
12981
+ const handledByMenu = await this.runBackgroundMenu(cwd);
12982
+ if (handledByMenu)
12983
+ return;
12984
+ }
12489
12985
  const firstArg = (args[0] ?? "").toLowerCase();
12490
12986
  const subcommand = firstArg || "list";
12491
- if (subcommand === "list" || /^\d+$/.test(subcommand)) {
12492
- const limitRaw = subcommand === "list" ? args[1] : subcommand;
12987
+ const listFilters = {
12988
+ list: undefined,
12989
+ running: ["running"],
12990
+ done: ["done"],
12991
+ error: ["error"],
12992
+ failed: ["error"],
12993
+ terminated: ["terminated"],
12994
+ unknown: ["unknown"],
12995
+ };
12996
+ const hasListFilter = Object.prototype.hasOwnProperty.call(listFilters, subcommand);
12997
+ if (hasListFilter || /^\d+$/.test(subcommand)) {
12998
+ const limitRaw = subcommand === "list" ? args[1] : hasListFilter ? args[1] : subcommand;
12493
12999
  const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 20;
12494
13000
  if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
12495
- this.showWarning("Usage: /bg [list [limit: 1..200]] | /bg status [id] | /bg logs [id] [lines] | /bg stop [id]");
13001
+ this.showWarning(`Usage: ${usage}`);
12496
13002
  return;
12497
13003
  }
12498
- const records = listBackgroundProcesses(cwd, limit);
12499
- if (records.length === 0) {
12500
- this.showStatus("No background processes found.");
13004
+ const records = this.sortBackgroundRecords(listBackgroundProcesses(cwd, limit));
13005
+ const statusFilter = hasListFilter ? listFilters[subcommand] : undefined;
13006
+ const filtered = statusFilter && statusFilter.length > 0
13007
+ ? records.filter((record) => statusFilter.includes(record.status))
13008
+ : records;
13009
+ if (filtered.length === 0) {
13010
+ if (statusFilter && statusFilter.length > 0) {
13011
+ this.showStatus(`No background processes found for "${subcommand}" filter.`);
13012
+ }
13013
+ else {
13014
+ this.showStatus("No background processes found.");
13015
+ }
12501
13016
  return;
12502
13017
  }
12503
- const lines = records.map((record, index) => {
12504
- const status = `status=${record.status}`;
12505
- const pid = `pid=${record.pid}`;
12506
- const created = record.createdAt ? ` · ${record.createdAt}` : "";
12507
- const finished = record.finishedAt ? ` · finished=${record.finishedAt}` : "";
12508
- const exitCode = typeof record.exitCode === "number" ? ` · exit=${record.exitCode}` : "";
12509
- return `${index + 1}. ${record.id} · ${status} · ${pid}${created}${finished}${exitCode}\n ${record.command}`;
12510
- });
12511
- this.showCommandTextBlock("Background Processes", lines.join("\n"));
13018
+ this.showCommandTextBlock("Background Processes", this.buildBackgroundProcessesReport(filtered));
12512
13019
  return;
12513
13020
  }
12514
13021
  if (subcommand === "status") {
@@ -12525,10 +13032,11 @@ export class InteractiveMode {
12525
13032
  }
12526
13033
  const lines = [
12527
13034
  `ID: ${record.id}`,
12528
- `Status: ${record.status}`,
13035
+ `Status: ${this.formatBackgroundStatusLabel(record.status)}${record.requestedStopAt ? " (stop requested)" : ""}`,
12529
13036
  `PID: ${record.pid}`,
12530
- `Created: ${record.createdAt}`,
13037
+ `Created: ${record.createdAt} (${this.formatRelativeTime(record.createdAt)})`,
12531
13038
  `Started: ${record.startedAt}`,
13039
+ `Runtime: ${this.formatDurationMs(record.startedAt, record.finishedAt)}`,
12532
13040
  `Finished: ${record.finishedAt ?? "-"}`,
12533
13041
  `Exit code: ${typeof record.exitCode === "number" ? record.exitCode : "-"}`,
12534
13042
  `Cwd: ${record.cwd}`,
@@ -12537,6 +13045,10 @@ export class InteractiveMode {
12537
13045
  `Status file: ${record.metaPath}`,
12538
13046
  `Log file: ${record.logPath}`,
12539
13047
  "",
13048
+ "Quick actions:",
13049
+ `- /bg logs ${record.id} 120`,
13050
+ `- /bg stop ${record.id}`,
13051
+ "",
12540
13052
  "Command:",
12541
13053
  record.command,
12542
13054
  ];
@@ -12546,7 +13058,9 @@ export class InteractiveMode {
12546
13058
  if (subcommand === "logs") {
12547
13059
  let id = args[1];
12548
13060
  if (!id) {
12549
- id = await this.pickBackgroundProcessId(cwd, "logs");
13061
+ id = await this.pickBackgroundProcessId(cwd, "logs", {
13062
+ preferredStatuses: ["running", "unknown", "error", "done", "terminated"],
13063
+ });
12550
13064
  }
12551
13065
  if (!id)
12552
13066
  return;
@@ -12567,30 +13081,368 @@ export class InteractiveMode {
12567
13081
  return;
12568
13082
  }
12569
13083
  const body = tail.trim().length > 0 ? tail : "(no output yet)";
12570
- this.showCommandTextBlock("Background Logs", [`ID: ${record.id} · status=${record.status} · tail=${tailLines} lines`, "", body].join("\n"));
13084
+ this.showCommandTextBlock("Background Logs", [
13085
+ `ID: ${record.id} · status=${this.formatBackgroundStatusLabel(record.status)} · tail=${tailLines} lines`,
13086
+ `Runtime: ${this.formatDurationMs(record.startedAt, record.finishedAt)} · stop_requested=${record.requestedStopAt ? "yes" : "no"}`,
13087
+ `Command: ${record.command.length > 120 ? `${record.command.slice(0, 117)}...` : record.command}`,
13088
+ "",
13089
+ body,
13090
+ ].join("\n"));
12571
13091
  return;
12572
13092
  }
12573
13093
  if (subcommand === "stop" || subcommand === "kill" || subcommand === "cancel") {
12574
13094
  let id = args[1];
12575
13095
  if (!id) {
12576
- id = await this.pickBackgroundProcessId(cwd, "stop");
13096
+ id = await this.pickBackgroundProcessId(cwd, "stop", {
13097
+ preferredStatuses: ["running", "unknown", "error", "done", "terminated"],
13098
+ });
12577
13099
  }
12578
13100
  if (!id)
12579
13101
  return;
13102
+ const current = getBackgroundProcess(cwd, id);
13103
+ if (!current) {
13104
+ this.showWarning(`Background process not found: ${id}`);
13105
+ return;
13106
+ }
13107
+ if (current.status !== "running") {
13108
+ this.showStatus(`Background process ${current.id} is already ${current.status}.`);
13109
+ return;
13110
+ }
13111
+ if (this.canUseInteractiveSelectors()) {
13112
+ const confirmed = await this.showExtensionConfirm("Stop background process?", `${current.id}\n${current.command.length > 140 ? `${current.command.slice(0, 137)}...` : current.command}`);
13113
+ if (!confirmed) {
13114
+ this.showStatus(`/bg stop cancelled for ${current.id}.`);
13115
+ return;
13116
+ }
13117
+ }
12580
13118
  const record = stopBackgroundProcess(cwd, id);
12581
13119
  if (!record) {
12582
13120
  this.showWarning(`Background process not found: ${id}`);
12583
13121
  return;
12584
13122
  }
12585
13123
  if (record.status === "running") {
12586
- this.showWarning(`Stop signal sent to ${record.id}, but process still appears running.`);
13124
+ this.showWarning(`Stop requested for ${record.id}, waiting for graceful shutdown. Use /bg status ${record.id} to monitor.`);
12587
13125
  }
12588
13126
  else {
12589
13127
  this.showStatus(`Background process ${record.id} stopped (${record.status}).`);
12590
13128
  }
12591
13129
  return;
12592
13130
  }
12593
- this.showWarning("Usage: /bg [list [limit: 1..200]] | /bg status [id] | /bg logs [id] [lines] | /bg stop [id]");
13131
+ if (subcommand === "stop-all" || subcommand === "kill-all" || subcommand === "cancel-all") {
13132
+ await this.stopAllBackgroundProcesses(cwd);
13133
+ return;
13134
+ }
13135
+ if (subcommand === "prune" || subcommand === "cleanup") {
13136
+ const hoursRaw = args[1];
13137
+ const hours = hoursRaw ? Number.parseInt(hoursRaw, 10) : 168;
13138
+ if (!Number.isInteger(hours) || hours < 1 || hours > 2160) {
13139
+ this.showWarning("Usage: /bg prune [hours: 1..2160]");
13140
+ return;
13141
+ }
13142
+ const result = pruneBackgroundProcesses(cwd, { maxAgeHours: hours });
13143
+ const preview = result.removedIds.length > 0 ? `\n${result.removedIds.slice(0, 10).map((id) => `- ${id}`).join("\n")}` : "";
13144
+ const moreSuffix = result.removedIds.length > 10 ? `\n- ... (+${result.removedIds.length - 10} more)` : "";
13145
+ this.showCommandTextBlock("Background Prune", [
13146
+ `Threshold: older than ${result.thresholdHours}h`,
13147
+ `Removed: ${result.removed}`,
13148
+ `Skipped running: ${result.skippedRunning}`,
13149
+ `Skipped recent: ${result.skippedRecent}`,
13150
+ preview ? "" : undefined,
13151
+ preview || undefined,
13152
+ moreSuffix || undefined,
13153
+ ]
13154
+ .filter((line) => typeof line === "string")
13155
+ .join("\n"));
13156
+ return;
13157
+ }
13158
+ if (subcommand === "help") {
13159
+ this.showCommandTextBlock("Background Help", [
13160
+ "Background command usage:",
13161
+ usage,
13162
+ "",
13163
+ "Examples:",
13164
+ "- ! npm run dev &",
13165
+ "- /bg",
13166
+ "- /bg running",
13167
+ "- /bg status bg_123",
13168
+ "- /bg logs bg_123 200",
13169
+ "- /bg stop bg_123",
13170
+ "- /bg stop-all",
13171
+ "- /bg prune 72",
13172
+ ].join("\n"));
13173
+ return;
13174
+ }
13175
+ this.showWarning(`Usage: ${usage}`);
13176
+ }
13177
+ getExtensionsCommandUsage() {
13178
+ return "/extensions (/ext) [list|install <source>|update [source]|remove <source>|enable <target>|disable <target>|help] [--global]";
13179
+ }
13180
+ createInteractivePackageManager(cwd) {
13181
+ return new DefaultPackageManager({
13182
+ cwd,
13183
+ agentDir: getAgentDir(),
13184
+ settingsManager: this.settingsManager,
13185
+ });
13186
+ }
13187
+ normalizeExtensionOverrideTarget(target) {
13188
+ const trimmed = target.trim();
13189
+ if (!trimmed)
13190
+ return "";
13191
+ if (path.isAbsolute(trimmed)) {
13192
+ return path.resolve(trimmed);
13193
+ }
13194
+ let normalized = trimmed.replace(/\\/g, "/");
13195
+ if (normalized.startsWith("./"))
13196
+ normalized = normalized.slice(2);
13197
+ if (!normalized.startsWith("extensions/")) {
13198
+ normalized = `extensions/${normalized}`;
13199
+ }
13200
+ return normalized;
13201
+ }
13202
+ setExtensionOverride(scope, target, enabled) {
13203
+ const normalizedTarget = this.normalizeExtensionOverrideTarget(target);
13204
+ if (!normalizedTarget)
13205
+ return { updated: false, token: undefined };
13206
+ const current = scope === "project"
13207
+ ? [...(this.settingsManager.getProjectSettings().extensions ?? [])]
13208
+ : [...(this.settingsManager.getGlobalSettings().extensions ?? [])];
13209
+ const normalizeEntryTarget = (entry) => {
13210
+ const trimmed = entry.trim();
13211
+ if (trimmed.startsWith("+") || trimmed.startsWith("-")) {
13212
+ return this.normalizeExtensionOverrideTarget(trimmed.slice(1));
13213
+ }
13214
+ return "";
13215
+ };
13216
+ const filtered = current.filter((entry) => normalizeEntryTarget(entry) !== normalizedTarget);
13217
+ const token = `${enabled ? "+" : "-"}${normalizedTarget}`;
13218
+ const next = [...filtered, token];
13219
+ if (JSON.stringify(next) === JSON.stringify(current)) {
13220
+ return { updated: false, token };
13221
+ }
13222
+ if (scope === "project") {
13223
+ this.settingsManager.setProjectExtensionPaths(next);
13224
+ }
13225
+ else {
13226
+ this.settingsManager.setExtensionPaths(next);
13227
+ }
13228
+ return { updated: true, token };
13229
+ }
13230
+ togglePackageExtensionSource(scope, source, enabled) {
13231
+ const settings = scope === "project" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings();
13232
+ const packages = [...(settings.packages ?? [])];
13233
+ const index = packages.findIndex((pkg) => (typeof pkg === "string" ? pkg : pkg.source) === source);
13234
+ if (index < 0) {
13235
+ return { matched: false, changed: false, message: "" };
13236
+ }
13237
+ const entry = packages[index];
13238
+ let nextEntry = entry;
13239
+ if (!enabled) {
13240
+ if (typeof entry === "string") {
13241
+ nextEntry = { source: entry, extensions: [] };
13242
+ }
13243
+ else if (Array.isArray(entry.extensions) && entry.extensions.length === 0) {
13244
+ return { matched: true, changed: false, message: `Extension source already disabled: ${source}` };
13245
+ }
13246
+ else {
13247
+ nextEntry = { ...entry, extensions: [] };
13248
+ }
13249
+ }
13250
+ else if (typeof entry === "string") {
13251
+ return { matched: true, changed: false, message: `Extension source already enabled: ${source}` };
13252
+ }
13253
+ else if (!Array.isArray(entry.extensions) || entry.extensions.length > 0) {
13254
+ return { matched: true, changed: false, message: `Extension source already enabled: ${source}` };
13255
+ }
13256
+ else {
13257
+ const { extensions: _extensions, ...rest } = entry;
13258
+ nextEntry = Object.keys(rest).length === 1 ? rest.source : rest;
13259
+ }
13260
+ packages[index] = nextEntry;
13261
+ if (scope === "project") {
13262
+ this.settingsManager.setProjectPackages(packages);
13263
+ }
13264
+ else {
13265
+ this.settingsManager.setPackages(packages);
13266
+ }
13267
+ return {
13268
+ matched: true,
13269
+ changed: true,
13270
+ message: `${enabled ? "Enabled" : "Disabled"} extension source (${scope}): ${source}`,
13271
+ };
13272
+ }
13273
+ async showExtensionsList(scopeFilter) {
13274
+ const cwd = this.sessionManager.getCwd();
13275
+ const packageManager = this.createInteractivePackageManager(cwd);
13276
+ const resolved = await packageManager.resolve();
13277
+ const packageLines = [];
13278
+ const projectPackages = this.settingsManager.getProjectSettings().packages ?? [];
13279
+ const globalPackages = this.settingsManager.getGlobalSettings().packages ?? [];
13280
+ const includeProject = !scopeFilter || scopeFilter === "project";
13281
+ const includeGlobal = !scopeFilter || scopeFilter === "user";
13282
+ if (includeProject) {
13283
+ packageLines.push("Project package sources:");
13284
+ if (projectPackages.length === 0) {
13285
+ packageLines.push(" (none)");
13286
+ }
13287
+ else {
13288
+ for (const pkg of projectPackages) {
13289
+ const source = typeof pkg === "string" ? pkg : pkg.source;
13290
+ const status = typeof pkg === "object" && Array.isArray(pkg.extensions) && pkg.extensions.length === 0
13291
+ ? "disabled"
13292
+ : "enabled";
13293
+ packageLines.push(` - [${status}] ${source}`);
13294
+ }
13295
+ }
13296
+ }
13297
+ if (includeGlobal) {
13298
+ if (packageLines.length > 0)
13299
+ packageLines.push("");
13300
+ packageLines.push("User package sources:");
13301
+ if (globalPackages.length === 0) {
13302
+ packageLines.push(" (none)");
13303
+ }
13304
+ else {
13305
+ for (const pkg of globalPackages) {
13306
+ const source = typeof pkg === "string" ? pkg : pkg.source;
13307
+ const status = typeof pkg === "object" && Array.isArray(pkg.extensions) && pkg.extensions.length === 0
13308
+ ? "disabled"
13309
+ : "enabled";
13310
+ packageLines.push(` - [${status}] ${source}`);
13311
+ }
13312
+ }
13313
+ }
13314
+ const extensions = resolved.extensions
13315
+ .filter((resource) => !scopeFilter || resource.metadata.scope === scopeFilter)
13316
+ .sort((a, b) => {
13317
+ if (a.metadata.scope !== b.metadata.scope)
13318
+ return a.metadata.scope.localeCompare(b.metadata.scope, "en");
13319
+ return a.path.localeCompare(b.path, "en");
13320
+ });
13321
+ const enabledCount = extensions.filter((resource) => resource.enabled).length;
13322
+ const disabledCount = extensions.length - enabledCount;
13323
+ const extensionLines = extensions.length === 0
13324
+ ? [" (none)"]
13325
+ : extensions.map((resource) => {
13326
+ const relative = path.relative(cwd, resource.path);
13327
+ const displayPath = !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : resource.path;
13328
+ const status = resource.enabled ? "enabled" : "disabled";
13329
+ return ` - [${status}] ${displayPath} · scope=${resource.metadata.scope} · source=${resource.metadata.source}`;
13330
+ });
13331
+ const lines = [
13332
+ `Extension resources: total=${extensions.length} · enabled=${enabledCount} · disabled=${disabledCount}`,
13333
+ "",
13334
+ ...extensionLines,
13335
+ "",
13336
+ ...packageLines,
13337
+ "",
13338
+ "Commands:",
13339
+ "- /extensions install <source> [--global]",
13340
+ "- /extensions update [source]",
13341
+ "- /extensions remove <source> [--global]",
13342
+ "- /extensions disable <source-or-path> [--global]",
13343
+ "- /extensions enable <source-or-path> [--global]",
13344
+ ];
13345
+ this.showCommandTextBlock("Extensions", lines.join("\n"));
13346
+ }
13347
+ async handleExtensionsSlashCommand(text) {
13348
+ const args = this.parseSlashArgs(text).slice(1);
13349
+ const usage = this.getExtensionsCommandUsage();
13350
+ const filteredArgs = args.filter((arg) => arg !== "/extensions" && arg !== "/ext");
13351
+ let scope = "project";
13352
+ const normalizedArgs = [];
13353
+ for (const arg of filteredArgs) {
13354
+ if (arg === "--global" || arg === "-g") {
13355
+ scope = "user";
13356
+ continue;
13357
+ }
13358
+ if (arg === "--project") {
13359
+ scope = "project";
13360
+ continue;
13361
+ }
13362
+ normalizedArgs.push(arg);
13363
+ }
13364
+ const subcommand = (normalizedArgs[0] ?? "list").toLowerCase();
13365
+ const subArgs = normalizedArgs.slice(1);
13366
+ const cwd = this.sessionManager.getCwd();
13367
+ const packageManager = this.createInteractivePackageManager(cwd);
13368
+ if (subcommand === "help") {
13369
+ this.showCommandTextBlock("Extensions Help", [
13370
+ "Extension lifecycle usage:",
13371
+ usage,
13372
+ "",
13373
+ "Examples:",
13374
+ "- /extensions list",
13375
+ "- /extensions list --global",
13376
+ "- /extensions install npm:@org/iosm-extension",
13377
+ "- /extensions install ./local-extension --global",
13378
+ "- /extensions update",
13379
+ "- /extensions remove npm:@org/iosm-extension",
13380
+ "- /extensions disable extensions/my-tool.ts",
13381
+ "- /ext enable npm:@org/iosm-extension --global",
13382
+ ].join("\n"));
13383
+ return;
13384
+ }
13385
+ if (subcommand === "list") {
13386
+ await this.showExtensionsList(scope);
13387
+ return;
13388
+ }
13389
+ if (subcommand === "install") {
13390
+ const source = subArgs[0];
13391
+ if (!source) {
13392
+ this.showWarning(`Usage: ${usage}`);
13393
+ return;
13394
+ }
13395
+ await packageManager.install(source, { local: scope === "project" });
13396
+ packageManager.addSourceToSettings(source, { local: scope === "project" });
13397
+ await this.reloadAfterMemoryMutation(`Installed extension source (${scope}): ${source}`, "Extension source installed but reload failed");
13398
+ return;
13399
+ }
13400
+ if (subcommand === "update") {
13401
+ const source = subArgs[0];
13402
+ await packageManager.update(source);
13403
+ await this.reloadAfterMemoryMutation(source ? `Updated extension source: ${source}` : "Updated extension sources", "Extension sources updated but reload failed");
13404
+ return;
13405
+ }
13406
+ if (subcommand === "remove" || subcommand === "uninstall") {
13407
+ const source = subArgs[0];
13408
+ if (!source) {
13409
+ this.showWarning(`Usage: ${usage}`);
13410
+ return;
13411
+ }
13412
+ await packageManager.remove(source, { local: scope === "project" });
13413
+ const removed = packageManager.removeSourceFromSettings(source, { local: scope === "project" });
13414
+ if (!removed) {
13415
+ this.showWarning(`No matching extension source found in ${scope} scope: ${source}`);
13416
+ return;
13417
+ }
13418
+ await this.reloadAfterMemoryMutation(`Removed extension source (${scope}): ${source}`, "Extension source removed but reload failed");
13419
+ return;
13420
+ }
13421
+ if (subcommand === "enable" || subcommand === "disable") {
13422
+ const target = subArgs[0];
13423
+ if (!target) {
13424
+ this.showWarning(`Usage: ${usage}`);
13425
+ return;
13426
+ }
13427
+ const desiredEnabled = subcommand === "enable";
13428
+ const packageToggle = this.togglePackageExtensionSource(scope, target, desiredEnabled);
13429
+ if (packageToggle.matched) {
13430
+ if (!packageToggle.changed) {
13431
+ this.showStatus(packageToggle.message);
13432
+ return;
13433
+ }
13434
+ await this.reloadAfterMemoryMutation(packageToggle.message, "Extension source toggle applied but reload failed");
13435
+ return;
13436
+ }
13437
+ const override = this.setExtensionOverride(scope, target, desiredEnabled);
13438
+ if (!override.updated) {
13439
+ this.showStatus(`No extension override changes were needed for: ${target}`);
13440
+ return;
13441
+ }
13442
+ await this.reloadAfterMemoryMutation(`${desiredEnabled ? "Enabled" : "Disabled"} extension path (${scope}) via override: ${override.token}`, "Extension override applied but reload failed");
13443
+ return;
13444
+ }
13445
+ this.showWarning(`Usage: ${usage}`);
12594
13446
  }
12595
13447
  handleTeamRunsSlashCommand(text) {
12596
13448
  const args = this.parseSlashArgs(text).slice(1);