@xmoon76/dsh-pi-tui 0.4.8 → 0.4.9

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.
@@ -1,4 +1,4 @@
1
- import { n as TUI_STARTUP_SERVICE } from "./startup-Cd07dCyh.mjs";
1
+ import { n as TUI_STARTUP_SERVICE } from "./startup-xTIOW90U.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { StringDecoder } from "node:string_decoder";
@@ -383,10 +383,26 @@ function requestHeaderOf(agent) {
383
383
  */
384
384
  var DirectModelSelectionOwner = class {
385
385
  installed = /* @__PURE__ */ new WeakMap();
386
+ /** The per-Agent model-selection / image prompt-admission chain (rc.2
387
+ * `ApiSessionAgentController.serializeImageAdmission`). Overlapping model
388
+ * selections and image-bearing prompt admissions therefore apply in call
389
+ * order instead of racing each other. */
390
+ admissionChains = /* @__PURE__ */ new WeakMap();
386
391
  defaultModel;
387
392
  constructor(defaultModel) {
388
393
  this.defaultModel = defaultModel;
389
394
  }
395
+ /**
396
+ * Run one operation inside the Agent's serialization window. The chain
397
+ * swallows each settled result so a failed operation never poisons the
398
+ * queue, while the returned promise still rejects to its own caller.
399
+ */
400
+ serializeImageAdmission(agent, operation) {
401
+ const live = agent;
402
+ const result = (this.admissionChains.get(live) ?? Promise.resolve()).then(operation);
403
+ this.admissionChains.set(live, result.then(() => void 0, () => void 0));
404
+ return result;
405
+ }
390
406
  /** Read the current process default without retaining its object identity. */
391
407
  defaultSelection() {
392
408
  return agentSelection(selectionValue(this.defaultModel.currentSelection()));
@@ -24828,7 +24844,7 @@ const selectListTheme = {
24828
24844
  groupHeader: (text) => chalk.bold.hex(currentPalette.textMuted)(text)
24829
24845
  };
24830
24846
  /**
24831
- * Status-dot colour for a background-job status (dsh-web StateDot parity:
24847
+ * Status-dot colour for a job status (dsh-web StateDot parity:
24832
24848
  * running = ongoing/primary, stopping/killed = warning, completed = done/dim,
24833
24849
  * failed/timed-out/lost = error). Unknown statuses fall back to the
24834
24850
  * muted token so a future wire status never crashes the renderer.
@@ -26304,7 +26320,7 @@ const APP_KEYBINDINGS = {
26304
26320
  "tasks.scope.toggle": {
26305
26321
  id: "tasks.scope.toggle",
26306
26322
  defaultKeys: ["a"],
26307
- description: "Toggle Active and All task scope",
26323
+ description: "Toggle Active and Tracked task scope",
26308
26324
  category: "Tasks",
26309
26325
  scope: "tasks",
26310
26326
  configurable: false
@@ -27834,7 +27850,7 @@ var TaskBrowserPanel = class {
27834
27850
  const active = real.filter(isTaskItemActive).length;
27835
27851
  const done = real.filter((item) => item.status === "completed").length;
27836
27852
  const failed = real.filter((item) => isTaskItemFailure(item.status)).length;
27837
- const chips = [`[${this.scope.toUpperCase()}]`];
27853
+ const chips = [`[${this.scope === "all" ? "TRACKED" : "ACTIVE"}]`];
27838
27854
  if (this.activeType !== null) chips.push(`[${this.activeType}]`);
27839
27855
  if (this.getFilter() !== "") chips.push(`[search: ${this.getFilter()}]`);
27840
27856
  const stats = [`${active} active`];
@@ -28659,12 +28675,12 @@ const modelItem = {
28659
28675
  /**
28660
28676
  * The Task Center badge. Legacy snapshots (which predate totals) retain the
28661
28677
  * old wording for embedders; the runtime-backed snapshot uses independent
28662
- * running/total job and agent counts and a persistent failure marker.
28678
+ * active/tracked job and agent counts and a persistent failure marker.
28663
28679
  */
28664
28680
  const tasksItem = {
28665
28681
  id: "tasks",
28666
28682
  label: "Tasks",
28667
- description: "The Task Center running/total badge with failure attention and the ↓ view hint.",
28683
+ description: "The Task Center active/tracked badge with failure attention and the ↓ view hint.",
28668
28684
  defaultZone: "left",
28669
28685
  defaultImportance: 85,
28670
28686
  formats: ["badge"],
@@ -28718,9 +28734,9 @@ const tasksItem = {
28718
28734
  if (failed > 0) parts.push(`! ${failed} failed`);
28719
28735
  if (tasks > 0 && agents > 0) {
28720
28736
  parts.push(`● ${agents}/${totalAgents} agents`);
28721
- parts.push(`${tasks}/${totalJobs} jobs`);
28737
+ parts.push(`${tasks}/${totalJobs} tracked jobs`);
28722
28738
  } else if (agents > 0) parts.push(`● ${agents}/${totalAgents} agents`);
28723
- else if (tasks > 0) parts.push(`● ${tasks}/${totalJobs} jobs`);
28739
+ else if (tasks > 0) parts.push(`● ${tasks}/${totalJobs} tracked jobs`);
28724
28740
  return { spans: [{
28725
28741
  text: `[${parts.join(" · ")}${hint}]`,
28726
28742
  tone
@@ -45241,7 +45257,7 @@ var TuiApp = class TuiApp {
45241
45257
  pendingUserRows = [];
45242
45258
  /** Activity of the same pending-input subject shown in queueItems. */
45243
45259
  queueRunning = true;
45244
- /** Whether any background task is running/stopping. */
45260
+ /** Whether any job/subagent is running/stopping. */
45245
45261
  tasksActive = false;
45246
45262
  /** Independent Task Center counts; footer consumes this through status. */
45247
45263
  taskSummary = {
@@ -52757,7 +52773,7 @@ var TuiApp = class TuiApp {
52757
52773
  /**
52758
52774
  * The task-browser trigger semantic — ONE definition shared by the ↓
52759
52775
  * routing gate, the viewer's parent-lock, and the footer's `↓ view`
52760
- * hint: active background tasks or failure attention, no overlay entries, an EMPTY VISIBLE
52776
+ * hint: active jobs/subagents or failure attention, no overlay entries, an EMPTY VISIBLE
52761
52777
  * seat editor in PROMPT mode. The visible seat decides (a shell-mode
52762
52778
  * empty body is composing a command; a plugin replacement editor
52763
52779
  * contributes its own text/mode) — the hidden host editor's draft is
@@ -54863,10 +54879,11 @@ var TuiApp = class TuiApp {
54863
54879
  };
54864
54880
  }
54865
54881
  /**
54866
- * Replace the background-job snapshot used by the footer badge. The
54882
+ * Replace the JobRegistry snapshot used by the footer badge. The
54867
54883
  * runtime-backed caller supplies terminal records too; the rich summary
54868
- * separates active and total counts. Non-empty active/attention sets arm
54869
- * the footer ↓ Task Center trigger.
54884
+ * separates active and tracked counts (the tracked denominator is the
54885
+ * registry's CURRENT roster, never a session lifetime total).
54886
+ * Non-empty active/attention sets arm the footer ↓ Task Center trigger.
54870
54887
  * @param tasks - job records (id + label + lifecycle status), empty to hide.
54871
54888
  */
54872
54889
  setTasks(tasks) {
@@ -54895,7 +54912,7 @@ var TuiApp = class TuiApp {
54895
54912
  this.projectActivity();
54896
54913
  this.syncExtensionState();
54897
54914
  }
54898
- /** Whether background tasks or unacknowledged failures are available. */
54915
+ /** Whether active jobs/subagents or unacknowledged failures are available. */
54899
54916
  isTasksActive() {
54900
54917
  return this.tasksActive;
54901
54918
  }
@@ -56124,6 +56141,36 @@ var TuiApp = class TuiApp {
56124
56141
  });
56125
56142
  return () => handle?.hide();
56126
56143
  }
56144
+ /**
56145
+ * Host the shared Plugin Manager panel (P1-A) as a standalone overlay. The
56146
+ * panel/controller are runner-owned and outlive this overlay: hiding the
56147
+ * panel never cancels an active install (plan §10.5/§17). `onClosed` fires
56148
+ * on every hide path so the runner can drop its active-host reference.
56149
+ * Returns the closer.
56150
+ */
56151
+ openPluginManagerPanel(panel, onClosed) {
56152
+ if (this.disposed) return () => {};
56153
+ const configuredWidth = 76;
56154
+ const configuredMaxHeight = 28;
56155
+ const geometryOf = () => {
56156
+ const width = Math.max(1, Math.min(this.terminal.columns, configuredWidth));
56157
+ const maxHeight = Math.max(1, Math.min(this.terminal.rows, configuredMaxHeight));
56158
+ return {
56159
+ width,
56160
+ maxHeight,
56161
+ key: `${this.terminal.columns}:${this.terminal.rows}:${width}:${maxHeight}`
56162
+ };
56163
+ };
56164
+ const budgetAware = panel;
56165
+ const frame = new ResponsiveOverlayFrame(panel, geometryOf, (geometry) => {
56166
+ budgetAware.setMaxRows?.(Math.max(1, geometry.maxHeight - 2));
56167
+ }, onClosed);
56168
+ const handle = this.showOverlayOnHost(frame, {
56169
+ width: configuredWidth,
56170
+ maxHeight: configuredMaxHeight
56171
+ });
56172
+ return () => handle.hide();
56173
+ }
56127
56174
  /** Track an action-first editor even when it is nested inside the
56128
56175
  * SettingsList submenu rather than mounted as a standalone overlay. The
56129
56176
  * returned unregister callback is idempotent. */
@@ -59292,6 +59339,13 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59292
59339
  batcher;
59293
59340
  hostVersion;
59294
59341
  /**
59342
+ * INTERNAL owner → owning Loader entry id projection (P1-A1.4): recorded at
59343
+ * every contribution registration from the caller fiber's Loader entry. It
59344
+ * is read only by the Plugin Manager presentation classification and never
59345
+ * exposes a Cordis object.
59346
+ */
59347
+ ownerEntryIds = /* @__PURE__ */ new Map();
59348
+ /**
59295
59349
  * P1-1: the CURRENT surface's render sink (attached by the runner once
59296
59350
  * per surface generation). Registry invalidations (ledger, commands,
59297
59351
  * themes, autocomplete, settings, keybindings, renderers, editors) all
@@ -59558,6 +59612,7 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59558
59612
  if (!isSlotName(slot)) throw new Error(`unknown extension slot "${slot}" (known: ${slotNames().join(", ")})`);
59559
59613
  const caller = this.ctx;
59560
59614
  const owner = `${caller.fiber.uid}:${caller.fiber.name}`;
59615
+ this.recordOwnerEntry(owner, caller.fiber);
59561
59616
  const stableOwner = slot === "chrome.footer.item" ? caller.fiber.name : void 0;
59562
59617
  if (slot === "chrome.footer.item" && (spec.id.includes("/") || /[\u0000-\u001f\u007f-\u009f]/.test(spec.id))) throw new Error(`chrome.footer.item registration id must not contain "/" or control characters (owner "${owner}", id "${stripControlChars(spec.id)}")`);
59563
59618
  if (slot === "chrome.footer.item") for (const record of this.ledger.snapshot("chrome.footer.item").records) {
@@ -59598,6 +59653,7 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59598
59653
  registerCommand(contribution) {
59599
59654
  const caller = this.ctx;
59600
59655
  const owner = `${caller.fiber.uid}:${caller.fiber.name}`;
59656
+ this.recordOwnerEntry(owner, caller.fiber);
59601
59657
  const outcome = this.commands.register(contribution, owner);
59602
59658
  if (outcome.kind === "conflict") {
59603
59659
  const detail = outcome.nearSynonym === void 0 ? `owner "${outcome.existingOwner}" already holds it` : `a near-synonym of "${contribution.name}" (${outcome.nearSynonym}, owned by "${outcome.existingOwner}") — the AGENTS near-synonym rule forbids the ambiguity`;
@@ -59631,6 +59687,7 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59631
59687
  registerTheme(contribution) {
59632
59688
  const caller = this.ctx;
59633
59689
  const owner = `${caller.fiber.uid}:${caller.fiber.name}`;
59690
+ this.recordOwnerEntry(owner, caller.fiber);
59634
59691
  const stableOwner = caller.fiber.name;
59635
59692
  const selectableValue = ThemeRegistry.selectableValue(stableOwner, contribution.id);
59636
59693
  const handle = this.themes.register(contribution, owner, stableOwner);
@@ -59726,6 +59783,7 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59726
59783
  registerAutocomplete(contribution) {
59727
59784
  const caller = this.ctx;
59728
59785
  const owner = `${caller.fiber.uid}:${caller.fiber.name}`;
59786
+ this.recordOwnerEntry(owner, caller.fiber);
59729
59787
  const handle = this.autocomplete.register(contribution, owner);
59730
59788
  this.trackRegistryHealth("autocomplete", contribution.id, owner);
59731
59789
  let dispose;
@@ -59755,6 +59813,7 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59755
59813
  registerSetting(contribution) {
59756
59814
  const caller = this.ctx;
59757
59815
  const owner = `${caller.fiber.uid}:${caller.fiber.name}`;
59816
+ this.recordOwnerEntry(owner, caller.fiber);
59758
59817
  const handle = this.settings.register(contribution, owner);
59759
59818
  this.trackRegistryHealth("setting", contribution.id, owner);
59760
59819
  let dispose;
@@ -59788,6 +59847,7 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59788
59847
  registerKeybinding(contribution) {
59789
59848
  const caller = this.ctx;
59790
59849
  const owner = `${caller.fiber.uid}:${caller.fiber.name}`;
59850
+ this.recordOwnerEntry(owner, caller.fiber);
59791
59851
  const handle = this.keybindings.register(contribution, owner);
59792
59852
  this.trackRegistryHealth("keybinding", contribution.id, owner);
59793
59853
  let dispose;
@@ -59818,6 +59878,7 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59818
59878
  registerMessageRenderer(contribution) {
59819
59879
  const caller = this.ctx;
59820
59880
  const owner = `${caller.fiber.uid}:${caller.fiber.name}`;
59881
+ this.recordOwnerEntry(owner, caller.fiber);
59821
59882
  const handle = this.renderers.registerMessageRenderer(contribution, owner);
59822
59883
  this.ledger.trackHealth("transcript.message.renderer", contribution.id, owner);
59823
59884
  let dispose;
@@ -59847,6 +59908,7 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59847
59908
  registerToolRenderer(contribution) {
59848
59909
  const caller = this.ctx;
59849
59910
  const owner = `${caller.fiber.uid}:${caller.fiber.name}`;
59911
+ this.recordOwnerEntry(owner, caller.fiber);
59850
59912
  const handle = this.renderers.registerToolRenderer(contribution, owner);
59851
59913
  this.ledger.trackHealth("transcript.tool.renderer", contribution.id, owner);
59852
59914
  let dispose;
@@ -59939,6 +60001,7 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59939
60001
  registerEditor(contribution) {
59940
60002
  const caller = this.ctx;
59941
60003
  const owner = `${caller.fiber.uid}:${caller.fiber.name}`;
60004
+ this.recordOwnerEntry(owner, caller.fiber);
59942
60005
  const handle = this.editors.register(contribution, owner);
59943
60006
  this.trackRegistryHealth("editor", contribution.id, owner);
59944
60007
  let dispose;
@@ -59966,6 +60029,27 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59966
60029
  return this.ledger;
59967
60030
  }
59968
60031
  /**
60032
+ * INTERNAL read-only projection (P1-A1.4): owner → owning Loader entry id.
60033
+ * The Plugin Manager surface uses it to associate a live TUI extension
60034
+ * contribution with the official PluginManager entry that loaded it,
60035
+ * exactly and without name guessing. It is NOT a second inventory, NOT an
60036
+ * authority, and NEVER part of the public Extension API.
60037
+ */
60038
+ _ownerEntryIds() {
60039
+ return this.ownerEntryIds;
60040
+ }
60041
+ /**
60042
+ * Record the owning Loader entry id of one contribution owner. The Loader
60043
+ * stamps `fiber.entry = fiber.parent[Entry.key]` at import time, so a
60044
+ * plugin loaded from a composition row carries the exact id the official
60045
+ * plugin inventory reports as `entryId`. A fiber with no Loader entry
60046
+ * (a programmatically created context) records nothing and falls back.
60047
+ */
60048
+ recordOwnerEntry(owner, fiber) {
60049
+ const id = fiber.entry?.id;
60050
+ if (typeof id === "string" && id !== "") this.ownerEntryIds.set(owner, id);
60051
+ }
60052
+ /**
59969
60053
  * Register a normalized input capture (plan §5). Caller-fiber-owned:
59970
60054
  * owner unload removes the capture. A duplicate id or a second live
59971
60055
  * exclusive capture is an explicit error (the registry's rules — never
@@ -59975,6 +60059,7 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
59975
60059
  _advancedCaptureInput(spec) {
59976
60060
  const caller = this.ctx;
59977
60061
  const owner = `${caller.fiber.uid}:${caller.fiber.name}`;
60062
+ this.recordOwnerEntry(owner, caller.fiber);
59978
60063
  const handle = this.advancedInputs.register(spec, owner);
59979
60064
  let dispose;
59980
60065
  try {
@@ -60225,6 +60310,7 @@ var PiTuiExtensionServiceImpl = class PiTuiExtensionServiceImpl extends Service
60225
60310
  _unstableCaptureRaw(spec) {
60226
60311
  const caller = this.ctx;
60227
60312
  const owner = `${caller.fiber.uid}:${caller.fiber.name}`;
60313
+ this.recordOwnerEntry(owner, caller.fiber);
60228
60314
  const handle = this.unstableInputs.register(spec, owner);
60229
60315
  let dispose;
60230
60316
  try {
@@ -66360,41 +66446,13 @@ function registerTuiCommands(runner, initial) {
66360
66446
  });
66361
66447
  return [...byName.values()];
66362
66448
  };
66363
- let presetSelectionEnabled;
66364
- let presetSelectionOwner;
66365
- let presetSelectionRequest = 0;
66366
- /** Register a roster read that feeds the PRESENTATION cache. The token is
66367
- * taken at START, so a read that merely SETTLES later can never overwrite a
66368
- * newer one — every reader (probe, typed `/preset`, picker) participates.
66369
- * Mutation ownership is SEPARATE: an operation that already passed its own
66370
- * subject/token fences still dispatches even when this cache commit loses. */
66371
- const beginPresetSelectionRead = () => ++presetSelectionRequest;
66372
- const currentPresetOwner = () => ({
66373
- generation: runner.sessionGeneration,
66374
- sessionId: runner.liveAgent?.session.id
66375
- });
66376
- const samePresetOwner = (left, right) => left.generation === right.generation && left.sessionId === right.sessionId;
66377
66449
  /**
66378
66450
  * The TUI-owned `/preset` command IDENTITY — the official
66379
66451
  * discovery/presentation identity (0.1.6 `definitionId`), never an
66380
- * authorization fact. Visibility is keyed on it so a scoped same-name
66381
- * `/preset` contributed by a preset/plugin (which selects its own full
66382
- * descriptor and does not inherit this identity) keeps its own presentation.
66452
+ * authorization fact.
66383
66453
  */
66384
66454
  const TUI_PRESET_COMMAND_DEFINITION_ID = CommandDefinitionId("@xmoon76/dsh-pi-tui/preset");
66385
66455
  /**
66386
- * Whether one effective command is visible in the preset-selection surface.
66387
- * Only the TUI's OWN `/preset` identity honors the Host mode-selection
66388
- * policy; a same-name command with a DIFFERENT definitionId is semantically
66389
- * unrelated and stays visible. A value belonging to another owner is
66390
- * unknown: keep the affordance rather than inherit a stale `false`.
66391
- */
66392
- const presetCommandVisible = (command) => {
66393
- if (command.definitionId !== TUI_PRESET_COMMAND_DEFINITION_ID) return true;
66394
- if (presetSelectionOwner === void 0 || !samePresetOwner(presetSelectionOwner, currentPresetOwner())) return true;
66395
- return presetSelectionEnabled !== false;
66396
- };
66397
- /**
66398
66456
  * One collision notice per contribution IDENTITY and failure GENERATION
66399
66457
  * (the key is dropped when the collision recovers): a new owner reusing a
66400
66458
  * released name, or the same contribution colliding again after the host
@@ -66418,7 +66476,7 @@ function registerTuiCommands(runner, initial) {
66418
66476
  leadingInput: command.input !== void 0,
66419
66477
  attachments: command.input?.attachments === true
66420
66478
  }]));
66421
- const display = options.display === "none" ? [] : [...mergeContributions(sorted)].filter(presetCommandVisible).sort((left, right) => left.name < right.name ? -1 : 1);
66479
+ const display = options.display === "none" ? [] : [...mergeContributions(sorted)].sort((left, right) => left.name < right.name ? -1 : 1);
66422
66480
  const extensionAutocomplete = runner.extensions?.autocomplete;
66423
66481
  app.setCommandCompletions(display.map((command) => ({
66424
66482
  name: command.name,
@@ -66509,48 +66567,6 @@ function registerTuiCommands(runner, initial) {
66509
66567
  const liveAgent = runner.liveAgent;
66510
66568
  installCompletionsContained(liveAgent === void 0 ? mergeGlobalAndSavedScoped() : commands.list(liveAgent).map(commandSummaryOf));
66511
66569
  };
66512
- /**
66513
- * Commit one roster read to the PRESENTATION cache while `request` is still
66514
- * the newest registered read, returning whether it committed. This owns ONLY
66515
- * the display cache: a `false` return means a later-started presentation read
66516
- * already owns the cache and the value must not repaint it — it must NEVER
66517
- * cancel the caller's own user operation. A user mutation decides from the
66518
- * roster IT read: the official Web keeps `loadGeneration` (presentation) and
66519
- * `select()` (mutation ownership) separate, and the Host
66520
- * `agentPresets.select` owns the blank-session/mount/commit authority.
66521
- */
66522
- const commitPresetVisibility = (enabled, owner, request) => {
66523
- if (request !== presetSelectionRequest) return false;
66524
- if (presetSelectionOwner !== void 0 && samePresetOwner(presetSelectionOwner, owner) && presetSelectionEnabled === enabled) return true;
66525
- presetSelectionEnabled = enabled;
66526
- presetSelectionOwner = owner;
66527
- refreshCompletions();
66528
- return true;
66529
- };
66530
- /**
66531
- * Seed/refresh the presentation capability from the CURRENT owner. The read
66532
- * is fenced by request order AND owner identity: a roster that settles after
66533
- * a newer read or a switch must never rewrite the new Session's command
66534
- * surface. A superseded or aborted read is a cancellation (debug diagnostics
66535
- * only); any other failure keeps the current presentation and is recorded by
66536
- * runOwned. A rosterless deployment (`available() === false`) is unknown and
66537
- * keeps `/preset` visible (its handler still refuses execution).
66538
- */
66539
- const refreshPresetSelectionVisibility = () => {
66540
- const presets = runner.catalog.presets;
66541
- if (!presets.available()) return;
66542
- const owner = currentPresetOwner();
66543
- const request = beginPresetSelectionRead();
66544
- runOwned("preset selection visibility", async () => {
66545
- const roster = await presets.roster(runner.signal);
66546
- if (!samePresetOwner(owner, currentPresetOwner())) return;
66547
- commitPresetVisibility(roster.modeSelectionEnabled, owner, request);
66548
- }, {
66549
- diag: runner.diag,
66550
- sessionId: () => runner.liveAgent?.session.id,
66551
- isCancellation: (error) => error instanceof SupersededReadError || runner.signal.aborted
66552
- });
66553
- };
66554
66570
  let commandCommitDepth = 0;
66555
66571
  let commandCommitDirty = false;
66556
66572
  /** Run one bulk command commit; registry changes inside are coalesced. */
@@ -66872,7 +66888,14 @@ function registerTuiCommands(runner, initial) {
66872
66888
  description: row.description,
66873
66889
  currentValue: row.currentValue,
66874
66890
  ...row.values.length > 0 ? { values: [...row.values] } : {}
66875
- }))
66891
+ })),
66892
+ {
66893
+ id: "plugins",
66894
+ label: "Plugins",
66895
+ description: "Manage DSH plugins and TUI extensions",
66896
+ currentValue: "Manage…",
66897
+ submenu: (_currentValue, done) => runner.createPluginManagerSubmenu(done)
66898
+ }
66876
66899
  ], (id, value, revert, navigate) => {
66877
66900
  if (id === "approval") {
66878
66901
  if ((value === "ask" || value === "never") && liveAgent !== void 0) {
@@ -67634,15 +67657,6 @@ function registerTuiCommands(runner, initial) {
67634
67657
  const releasePin = pinDraftAttachments(line, runner.imageStore, runner.fileStore);
67635
67658
  let userMessage;
67636
67659
  try {
67637
- userMessage = await runner.prepareDraftMessage(line);
67638
- skillSignal.throwIfAborted();
67639
- if (!sessionUnchanged({
67640
- agent,
67641
- generation
67642
- }, runner.liveAgent, runner.sessionGeneration)) return {
67643
- kind: "error",
67644
- text: "the session changed while loading the skill — try again"
67645
- };
67646
67660
  if (runner.sessionTransitionPending()) {
67647
67661
  const merged = mergeDraft(app.getDraft(), line);
67648
67662
  app.setEditorText(merged);
@@ -67652,57 +67666,78 @@ function registerTuiCommands(runner, initial) {
67652
67666
  text: merged === line ? "a session transition is in progress — try again in a moment" : "the draft changed while transitioning — review it before submitting again"
67653
67667
  };
67654
67668
  }
67655
- try {
67656
- const outcome = await runner.withSessionWriter(agent.session.id, async () => {
67657
- skillSignal.throwIfAborted();
67658
- if (!sessionUnchanged({
67659
- agent,
67660
- generation
67661
- }, runner.liveAgent, runner.sessionGeneration)) return void 0;
67662
- if (delivery !== "steer" && hostLoadsSkillBody) return runner.sessionWriter.prompt(agent.session.id, userMessage, "queue");
67663
- if (fallbackBody !== void 0) {
67664
- const first = await runner.sessionWriter.prompt(agent.session.id, userMessage, "steer");
67665
- if (first.kind !== "committed") return first;
67666
- try {
67667
- consumeDraftAttachments(line, runner.imageStore, runner.fileStore);
67668
- const body = await runner.sessionWriter.prompt(agent.session.id, fallbackBody, "steer");
67669
- if (body.kind !== "committed") recordCommandDraftDisposition(commandId, "suppressed");
67670
- return body;
67671
- } catch (error) {
67672
- recordCommandDraftDisposition(commandId, "suppressed");
67673
- throw error;
67674
- }
67675
- }
67676
- return runner.sessionWriter.prompt(agent.session.id, userMessage, "steer");
67677
- });
67678
- if (outcome === void 0) return {
67679
- kind: "error",
67680
- text: "the session changed while loading the skill — try again"
67669
+ const admission = await runner.withSessionWriter(agent.session.id, () => runner.withPromptAdmission(agent, line, async () => {
67670
+ skillSignal.throwIfAborted();
67671
+ if (!sessionUnchanged({
67672
+ agent,
67673
+ generation
67674
+ }, runner.liveAgent, runner.sessionGeneration)) return { kind: "stale" };
67675
+ userMessage = await runner.prepareDraftMessage(line);
67676
+ skillSignal.throwIfAborted();
67677
+ if (!sessionUnchanged({
67678
+ agent,
67679
+ generation
67680
+ }, runner.liveAgent, runner.sessionGeneration)) return { kind: "stale" };
67681
+ if (delivery !== "steer" && hostLoadsSkillBody) return {
67682
+ kind: "written",
67683
+ outcome: await runner.sessionWriter.prompt(agent.session.id, userMessage, "queue")
67681
67684
  };
67682
- if (outcome.kind !== "committed") {
67683
- if (outcome.kind === "indeterminate") throw new IndeterminateSkillWriteError();
67684
- if (outcome.kind === "cancelled") throw cancellationError("skill write cancelled");
67685
- return {
67686
- kind: "error",
67687
- text: outcome.kind === "rejected" ? outcome.error.message : outcome.reason
67688
- };
67689
- }
67690
- } catch (error) {
67691
- if (error instanceof TransitionInProgressError) {
67692
- const merged = mergeDraft(app.getDraft(), line);
67693
- app.setEditorText(merged);
67694
- recordCommandDraftDisposition(commandId, "restored");
67695
- return {
67696
- kind: "error",
67697
- text: merged === line ? "a session transition is in progress — try again in a moment" : "the draft changed while transitioning — review it before submitting again"
67685
+ if (fallbackBody !== void 0) {
67686
+ const first = await runner.sessionWriter.prompt(agent.session.id, userMessage, "steer");
67687
+ if (first.kind !== "committed") return {
67688
+ kind: "written",
67689
+ outcome: first
67698
67690
  };
67691
+ try {
67692
+ consumeDraftAttachments(line, runner.imageStore, runner.fileStore);
67693
+ const body = await runner.sessionWriter.prompt(agent.session.id, fallbackBody, "steer");
67694
+ if (body.kind !== "committed") recordCommandDraftDisposition(commandId, "suppressed");
67695
+ return {
67696
+ kind: "written",
67697
+ outcome: body
67698
+ };
67699
+ } catch (error) {
67700
+ recordCommandDraftDisposition(commandId, "suppressed");
67701
+ throw error;
67702
+ }
67699
67703
  }
67700
- throw error;
67704
+ return {
67705
+ kind: "written",
67706
+ outcome: await runner.sessionWriter.prompt(agent.session.id, userMessage, "steer")
67707
+ };
67708
+ }));
67709
+ if (admission.kind === "stale") return {
67710
+ kind: "error",
67711
+ text: "the session changed while loading the skill — try again"
67712
+ };
67713
+ const outcome = admission.outcome;
67714
+ if (outcome === void 0) return {
67715
+ kind: "error",
67716
+ text: "the session changed while loading the skill — try again"
67717
+ };
67718
+ if (outcome.kind !== "committed") {
67719
+ if (outcome.kind === "indeterminate") throw new IndeterminateSkillWriteError();
67720
+ if (outcome.kind === "cancelled") throw cancellationError("skill write cancelled");
67721
+ return {
67722
+ kind: "error",
67723
+ text: outcome.kind === "rejected" ? outcome.error.message : outcome.reason
67724
+ };
67725
+ }
67726
+ } catch (error) {
67727
+ if (error instanceof TransitionInProgressError) {
67728
+ const merged = mergeDraft(app.getDraft(), line);
67729
+ app.setEditorText(merged);
67730
+ recordCommandDraftDisposition(commandId, "restored");
67731
+ return {
67732
+ kind: "error",
67733
+ text: merged === line ? "a session transition is in progress — try again in a moment" : "the draft changed while transitioning — review it before submitting again"
67734
+ };
67701
67735
  }
67702
- consumeDraftAttachments(line, runner.imageStore, runner.fileStore);
67736
+ throw error;
67703
67737
  } finally {
67704
67738
  releasePin();
67705
67739
  }
67740
+ consumeDraftAttachments(line, runner.imageStore, runner.fileStore);
67706
67741
  return {
67707
67742
  kind: "success",
67708
67743
  text: "skill " + name + " loaded"
@@ -67769,7 +67804,6 @@ function registerTuiCommands(runner, initial) {
67769
67804
  savedScopedCommands = snapshot.scopedCommands;
67770
67805
  installCompletionsContained(mergeGlobalAndSavedScoped());
67771
67806
  });
67772
- refreshPresetSelectionVisibility();
67773
67807
  };
67774
67808
  /**
67775
67809
  * The revalidating transition (target/owner change): scoped previews clear
@@ -68136,6 +68170,14 @@ function registerTuiCommands(runner, initial) {
68136
68170
  return { kind: "success" };
68137
68171
  }
68138
68172
  });
68173
+ registerTuiCommand({
68174
+ name: "plugins",
68175
+ description: "Inspect and manage DSH plugins and TUI extensions for this profile",
68176
+ handler: () => {
68177
+ runner.openPluginManager();
68178
+ return { kind: "success" };
68179
+ }
68180
+ });
68139
68181
  commands.register({
68140
68182
  name: "yolo",
68141
68183
  description: "Switch to danger-full-access (alias of /permission danger-full-access)",
@@ -68182,24 +68224,6 @@ function registerTuiCommands(runner, initial) {
68182
68224
  kind: "success",
68183
68225
  text: `default preset: ${await displayedDefault()}`
68184
68226
  };
68185
- const defaultOwner = {
68186
- generation: runner.sessionGeneration,
68187
- sessionId: runner.liveAgent?.session.id
68188
- };
68189
- const defaultVisibilityRequest = beginPresetSelectionRead();
68190
- let defaultRoster;
68191
- try {
68192
- defaultRoster = await presets.roster(runner.signal);
68193
- } catch (error) {
68194
- if (error instanceof SupersededReadError || runner.signal.aborted) return { kind: "success" };
68195
- throw error;
68196
- }
68197
- if (runner.sessionGeneration !== defaultOwner.generation || runner.liveAgent?.session.id !== defaultOwner.sessionId) return { kind: "success" };
68198
- commitPresetVisibility(defaultRoster.modeSelectionEnabled, defaultOwner, defaultVisibilityRequest);
68199
- if (!defaultRoster.modeSelectionEnabled) return {
68200
- kind: "error",
68201
- text: "preset selection is disabled in this deployment"
68202
- };
68203
68227
  try {
68204
68228
  await presets.resolve(rest, runner.signal);
68205
68229
  await runner.config.presetDefault.set(rest);
@@ -68232,15 +68256,6 @@ function registerTuiCommands(runner, initial) {
68232
68256
  };
68233
68257
  const ownerCurrent = () => runner.sessionGeneration === owner.generation && runner.liveAgent?.session.id === owner.sessionId;
68234
68258
  try {
68235
- const visibilityRequest = beginPresetSelectionRead();
68236
- const roster = await presets.roster(runner.signal);
68237
- if (token !== presetOperationToken) return { kind: "superseded" };
68238
- if (!ownerCurrent()) return { kind: "superseded" };
68239
- commitPresetVisibility(roster.modeSelectionEnabled, owner, visibilityRequest);
68240
- if (!roster.modeSelectionEnabled) return {
68241
- kind: "rejected",
68242
- message: "preset selection is disabled in this deployment"
68243
- };
68244
68259
  const agent = runner.liveAgent;
68245
68260
  if (agent === void 0) {
68246
68261
  const resolved = await presets.resolve(id, runner.signal);
@@ -68371,7 +68386,6 @@ function registerTuiCommands(runner, initial) {
68371
68386
  const pickerSessionId = runner.liveAgent?.session.id;
68372
68387
  const pickerOwnerCurrent = () => runner.sessionGeneration === pickerGeneration && runner.liveAgent?.session.id === pickerSessionId;
68373
68388
  let roster;
68374
- const visibilityRequest = beginPresetSelectionRead();
68375
68389
  try {
68376
68390
  roster = await presets.roster(runner.signal);
68377
68391
  } catch (error) {
@@ -68379,14 +68393,6 @@ function registerTuiCommands(runner, initial) {
68379
68393
  throw error;
68380
68394
  }
68381
68395
  if (!pickerOwnerCurrent()) return { kind: "success" };
68382
- if (!commitPresetVisibility(roster.modeSelectionEnabled, {
68383
- generation: pickerGeneration,
68384
- sessionId: pickerSessionId
68385
- }, visibilityRequest)) return { kind: "success" };
68386
- if (!roster.modeSelectionEnabled) return {
68387
- kind: "error",
68388
- text: "preset selection is disabled in this deployment"
68389
- };
68390
68396
  if (roster.presets.length === 0) return {
68391
68397
  kind: "success",
68392
68398
  text: "no agent presets configured"
@@ -69038,7 +69044,7 @@ function registerTuiCommands(runner, initial) {
69038
69044
  label: color.border("─".repeat(34)),
69039
69045
  currentValue: ""
69040
69046
  },
69041
- ...commands.list(runner.liveAgent).filter(presetCommandVisible).map((command) => ({
69047
+ ...commands.list(runner.liveAgent).map((command) => ({
69042
69048
  id: `cmd-${command.name}`,
69043
69049
  label: `/${command.name}`,
69044
69050
  description: command.description,
@@ -70330,22 +70336,39 @@ async function copyToClipboard(text, run, env) {
70330
70336
  //#region src/startup-status.ts
70331
70337
  /** The erase-current-line sequence (EL) used to overwrite in place. */
70332
70338
  const ERASE_LINE = "\r\x1B[2K";
70339
+ /** Erase attempts one `clear` makes before this instance gives up on the
70340
+ * stream: the original attempt plus ONE immediate retry. That is exactly the
70341
+ * one-off write failure the clear-then-log ordering must survive — it is NOT a
70342
+ * general I/O recovery policy, so it is deliberately a constant. */
70343
+ const CLEAR_ATTEMPTS = 2;
70333
70344
  /** Create the pre-mount status writer. `isTTY` defaults to true when the
70334
70345
  * output does not declare itself (the runner passes
70335
70346
  * `process.stdout.isTTY` explicitly). */
70336
70347
  function createStartupStatus(output) {
70337
70348
  const tty = output.isTTY ?? true;
70338
70349
  let shown = false;
70350
+ let disabled = false;
70351
+ const write = (text) => {
70352
+ try {
70353
+ output.write(text);
70354
+ return true;
70355
+ } catch {
70356
+ return false;
70357
+ }
70358
+ };
70339
70359
  return {
70340
70360
  show(message) {
70341
- if (!tty) return;
70342
- output.write(`${ERASE_LINE}${message}`);
70361
+ if (!tty || disabled) return;
70362
+ write(`${ERASE_LINE}${message}`);
70343
70363
  shown = true;
70344
70364
  },
70345
70365
  clear() {
70346
- if (!tty || !shown) return;
70347
- output.write(ERASE_LINE);
70348
- shown = false;
70366
+ if (!tty || !shown || disabled) return;
70367
+ for (let attempt = 0; attempt < CLEAR_ATTEMPTS && shown; attempt += 1) if (write(ERASE_LINE)) {
70368
+ shown = false;
70369
+ return;
70370
+ }
70371
+ disabled = true;
70349
70372
  }
70350
70373
  };
70351
70374
  }
@@ -70666,25 +70689,28 @@ function dshVersion() {
70666
70689
  //#region src/exit.ts
70667
70690
  /**
70668
70691
  * The TUI exit contract: latch the exit request, dispose/restore the Client
70669
- * surface, apply the resume-hint policy, then request the launcher's
70670
- * `appExit`. Pure and injectable so the resolve/reject/hang paths are
70671
- * testable without a Cordis context.
70692
+ * surface, run one synchronous Host-retirement preparation hook, apply the
70693
+ * resume-hint policy, then request the launcher's `appExit`. Pure and
70694
+ * injectable so the resolve/reject/hang paths are testable without a Cordis
70695
+ * context.
70672
70696
  *
70673
70697
  * Host-owned final durability is NOT this controller's job: the Direct
70674
70698
  * owned-session retirement (cancel → idle → drain descendants → final flush
70675
70699
  * → AgentHandle.dispose) runs inside the application-tree disposal that
70676
70700
  * `appExit` starts, under the DSH process-shutdown watchdog — see
70677
70701
  * `src/runtime/direct/owned-session-retirement.ts` and
70678
- * `docs/concurrency.md`. The exit controller only stops the Client surface
70679
- * and requests the exit; it never awaits a potentially long Host teardown
70680
- * in front of `appExit`. (The former flush-failure warning was removed
70681
- * with the flush: retirement failures are recorded by the retirement
70682
- * diagnostics, not by a user-facing exit warning.)
70702
+ * `docs/concurrency.md`. The exit controller may invoke ONE injected
70703
+ * synchronous Host-retirement preparation hook (the Direct owner's first
70704
+ * cancel, so it lands before the root teardown unregisters the inbox
70705
+ * projection), but it NEVER awaits full Host retirement in front of
70706
+ * `appExit`. (The former flush-failure warning was removed with the flush:
70707
+ * retirement failures are recorded by the retirement diagnostics, not by a
70708
+ * user-facing exit warning.)
70683
70709
  *
70684
70710
  * Zero-unhandled guarantee: the exit root is a terminal lifecycle boundary
70685
- * — every step (cleanup, hint, exit) and every error observation is
70686
- * individually protected, so no throw can skip a later step, leak a
70687
- * rejection, or leave the process running with a stopped TUI.
70711
+ * — every step (cleanup, preparation, hint, exit) and every error
70712
+ * observation is individually protected, so no throw can skip a later step,
70713
+ * leak a rejection, or leave the process running with a stopped TUI.
70688
70714
  * @module @xmoon76/dsh-pi-tui/exit
70689
70715
  */
70690
70716
  /** Best-effort diagnostics write: a throwing diag can never break the
@@ -70698,13 +70724,14 @@ function safeDiag(diag, level, message, fields) {
70698
70724
  /**
70699
70725
  * The ONE exit orchestration shared by every exit entry (Ctrl+C, Ctrl+D,
70700
70726
  * `/exit`, `/quit`): latch once → dispose/restore the Client surface →
70701
- * resume-hint policy → request `appExit`. Idempotent: later requests while
70702
- * one is in flight (or after it finished) are no-ops, so double Ctrl+C or a
70703
- * command plus a key cannot double-cleanup or double-exit.
70727
+ * synchronous Host-retirement preparation → resume-hint policy → request
70728
+ * `appExit`. Idempotent: later requests while one is in flight (or after it
70729
+ * finished) are no-ops, so double Ctrl+C or a command plus a key cannot
70730
+ * double-cleanup or double-exit.
70704
70731
  *
70705
- * TERMINAL ROOT: every step (cleanup, hint, exit) is individually
70706
- * protected, so no throw can skip a later step or leak a rejection, and
70707
- * cleanup/exit always run (exit exactly once, last).
70732
+ * TERMINAL ROOT: every step (cleanup, preparation, hint, exit) is
70733
+ * individually protected, so no throw can skip a later step or leak a
70734
+ * rejection, and cleanup/exit always run (exit exactly once, last).
70708
70735
  * @param deps - the injectable surface.
70709
70736
  * @returns `requestExit()` — safe to call from any entry, any number of times.
70710
70737
  */
@@ -70720,6 +70747,11 @@ function createExitController(deps) {
70720
70747
  } catch (cleanupError) {
70721
70748
  safeDiag(deps.diag, "error", "cleanup failed", { error: safeErrorMessage$1(cleanupError) });
70722
70749
  }
70750
+ try {
70751
+ deps.prepareRetirement?.();
70752
+ } catch (preparationError) {
70753
+ safeDiag(deps.diag, "error", "retirement preparation failed", { error: safeErrorMessage$1(preparationError) });
70754
+ }
70723
70755
  try {
70724
70756
  const resume = deps.resumeHint();
70725
70757
  if (resume !== void 0) deps.hint(resume);
@@ -71061,7 +71093,8 @@ const DIRECT_IMPLEMENTED_CAPABILITIES = [
71061
71093
  "config",
71062
71094
  "host-file",
71063
71095
  "session-archive",
71064
- "host-command"
71096
+ "host-command",
71097
+ "plugin-manager"
71065
71098
  ];
71066
71099
  //#endregion
71067
71100
  //#region src/runtime/backend.ts
@@ -71079,7 +71112,7 @@ const DIRECT_IMPLEMENTED_CAPABILITIES = [
71079
71112
  /** Assemble the Direct backend: in-process adapters over `ctx.*` services.
71080
71113
  * The runner depends on the returned `Backend`, never on `ctx.*` directly
71081
71114
  * for the ported domains. */
71082
- function createDirectBackend(subagent, sessionReader, pendingInputReader, sessionWriter, sessionLifecycle, interaction, catalog, config, hostFile, sessionArchive, hostCommand) {
71115
+ function createDirectBackend(subagent, sessionReader, pendingInputReader, sessionWriter, sessionLifecycle, interaction, catalog, config, hostFile, sessionArchive, hostCommand, pluginManager) {
71083
71116
  return {
71084
71117
  kind: "direct",
71085
71118
  capabilities: new Set(DIRECT_IMPLEMENTED_CAPABILITIES),
@@ -71093,7 +71126,8 @@ function createDirectBackend(subagent, sessionReader, pendingInputReader, sessio
71093
71126
  config,
71094
71127
  hostFile,
71095
71128
  sessionArchive,
71096
- hostCommand
71129
+ hostCommand,
71130
+ pluginManager
71097
71131
  };
71098
71132
  }
71099
71133
  //#endregion
@@ -72218,6 +72252,28 @@ var DirectInteractionPort = class {
72218
72252
  };
72219
72253
  //#endregion
72220
72254
  //#region src/runtime/direct/catalog-direct.ts
72255
+ /** A no-op diagnostics channel for an embedded/test adapter constructed
72256
+ * without the runner's Diag: detached ownership still observes failures
72257
+ * instead of leaving a bare promise. */
72258
+ const NOOP_DIAG = {
72259
+ debug: () => {},
72260
+ info: () => {},
72261
+ warn: () => {},
72262
+ error: () => {},
72263
+ dispose: () => {}
72264
+ };
72265
+ /** Adapt the optional narrow diagnostic surface to the full detached-work
72266
+ * channel (severity is collapsed onto the one sink the seam owns). */
72267
+ function detachedDiag(diag) {
72268
+ if (diag === void 0) return NOOP_DIAG;
72269
+ return {
72270
+ debug: () => {},
72271
+ info: () => {},
72272
+ warn: (message, fields) => diag.warn(message, fields),
72273
+ error: (message, fields) => diag.warn(message, fields),
72274
+ dispose: () => {}
72275
+ };
72276
+ }
72221
72277
  /** The Direct backend's catalog: the `ctx` services behind the semantic
72222
72278
  * `Catalog` interfaces. */
72223
72279
  var DirectCatalogPort = class {
@@ -72246,18 +72302,6 @@ var DirectModelCatalog = class {
72246
72302
  agentFor;
72247
72303
  modelSelections;
72248
72304
  diag;
72249
- /** Fence overlapping default writes so the newest choice wins persistence. */
72250
- defaultWriteGeneration = 0;
72251
- /** The generation of the newest SUCCESSFULLY committed default. Any
72252
- * successful write with a NEWER generation advances it, so a failed
72253
- * newer attempt never erases an older success (the correction target
72254
- * stays the newest committed value). */
72255
- committedGeneration = 0;
72256
- /** The newest SUCCESSFULLY committed default (the correction target). A
72257
- * failed attempt is never recorded here, so a failed choice can never be
72258
- * resurrected by a stale-write correction. */
72259
- latestCommitted;
72260
- latestWrite = Promise.resolve();
72261
72305
  constructor(ctx, agentFor = () => void 0, modelSelections, diag) {
72262
72306
  this.ctx = ctx;
72263
72307
  this.agentFor = agentFor;
@@ -72371,52 +72415,27 @@ var DirectModelCatalog = class {
72371
72415
  message: "model selection service unavailable"
72372
72416
  }
72373
72417
  });
72374
- const generation = ++this.defaultWriteGeneration;
72375
- const write = Promise.resolve().then(() => defaultModel.saveSelection({ ...next }));
72376
- this.latestWrite = write;
72377
- const fence = (outcome) => {
72378
- if (outcome.ok && generation > this.committedGeneration) {
72379
- this.committedGeneration = generation;
72380
- this.latestCommitted = { ...next };
72381
- }
72382
- return this.reassertLatest(generation).then(() => {
72383
- if (outcome.ok) return {
72384
- kind: "committed",
72385
- value: void 0
72386
- };
72387
- return {
72388
- kind: "indeterminate",
72389
- error: {
72390
- code: "session/model-default-indeterminate",
72391
- message: safeErrorMessage$1(outcome.error)
72392
- }
72393
- };
72394
- });
72395
- };
72396
- return write.then((value) => fence({
72397
- ok: true,
72398
- value
72399
- }), (error) => fence({
72400
- ok: false,
72401
- error
72418
+ return Promise.resolve().then(() => defaultModel.saveSelection({ ...next })).then(() => ({
72419
+ kind: "committed",
72420
+ value: void 0
72421
+ }), (error) => ({
72422
+ kind: "indeterminate",
72423
+ error: {
72424
+ code: "session/model-default-indeterminate",
72425
+ message: safeErrorMessage$1(error)
72426
+ }
72402
72427
  }));
72403
72428
  }
72404
- async reassertLatest(generation) {
72405
- while (generation !== this.defaultWriteGeneration) {
72406
- const observedGeneration = this.defaultWriteGeneration;
72407
- await this.latestWrite.catch(() => void 0);
72408
- if (observedGeneration !== this.defaultWriteGeneration) continue;
72409
- const latest = this.latestCommitted;
72410
- const defaultModel = this.defaultModel();
72411
- if (latest === void 0 || defaultModel === void 0) return;
72412
- try {
72413
- await defaultModel.saveSelection({ ...latest });
72414
- } catch (error) {
72415
- this.diag?.warn("model default correction failed", { error: error instanceof Error ? error.message : String(error) });
72416
- return;
72417
- }
72418
- generation = observedGeneration;
72419
- }
72429
+ /** Start the best-effort global-default save for a committed Session
72430
+ * selection. rc.2 returns the Session success immediately; the rejection is
72431
+ * observed through the detached owner, never the Session outcome. */
72432
+ startDefaultSave(selection, sessionId) {
72433
+ const defaultModel = this.defaultModel();
72434
+ if (defaultModel === void 0) return;
72435
+ runDetached("model default save", () => defaultModel.saveSelection({ ...selection }), {
72436
+ diag: detachedDiag(this.diag),
72437
+ sessionId: () => sessionId
72438
+ });
72420
72439
  }
72421
72440
  sessionSelection(sessionId) {
72422
72441
  const agent = this.agentFor(sessionId);
@@ -72434,13 +72453,45 @@ var DirectModelCatalog = class {
72434
72453
  if (Boolean(signal?.aborted)) return { kind: "cancelled" };
72435
72454
  const llm = this.llm();
72436
72455
  const agent = this.agentFor(sessionId);
72437
- if (agent === void 0 || this.modelSelections === void 0 || llm === void 0) return {
72456
+ const owner = this.modelSelections;
72457
+ if (agent === void 0 || owner === void 0 || llm === void 0) return {
72438
72458
  kind: "rejected",
72439
72459
  error: {
72440
72460
  code: "session/model-unavailable",
72441
72461
  message: "session model selection unavailable"
72442
72462
  }
72443
72463
  };
72464
+ return owner.serializeImageAdmission(agent, () => this.commitSessionModelSelection(sessionId, agent, owner, llm, selection, signal));
72465
+ }
72466
+ async commitSessionModelSelection(sessionId, agent, owner, llm, selection, signal) {
72467
+ if (Boolean(signal?.aborted)) return { kind: "cancelled" };
72468
+ try {
72469
+ if (!llm.listProviders().some((provider) => provider.id === selection.provider)) return {
72470
+ kind: "rejected",
72471
+ error: {
72472
+ code: "session/model-unavailable",
72473
+ message: `model provider "${selection.provider}" is not available`
72474
+ }
72475
+ };
72476
+ const models = await llm.listModels(selection.provider);
72477
+ if (Boolean(signal?.aborted)) return { kind: "cancelled" };
72478
+ if (!models.some((model) => model.id === selection.model)) return {
72479
+ kind: "rejected",
72480
+ error: {
72481
+ code: "session/model-unavailable",
72482
+ message: `model "${selection.model}" is not available on provider "${selection.provider}"`
72483
+ }
72484
+ };
72485
+ } catch (error) {
72486
+ if (Boolean(signal?.aborted)) return { kind: "cancelled" };
72487
+ return {
72488
+ kind: "rejected",
72489
+ error: {
72490
+ code: "session/model-unavailable",
72491
+ message: safeErrorMessage$1(error)
72492
+ }
72493
+ };
72494
+ }
72444
72495
  let resolved;
72445
72496
  try {
72446
72497
  resolved = await llm.resolveCallConfig({
@@ -72472,7 +72523,7 @@ var DirectModelCatalog = class {
72472
72523
  }
72473
72524
  };
72474
72525
  try {
72475
- this.modelSelections.appendSelection(agent, next);
72526
+ owner.appendSelection(agent, next);
72476
72527
  } catch (error) {
72477
72528
  return {
72478
72529
  kind: "rejected",
@@ -72482,13 +72533,8 @@ var DirectModelCatalog = class {
72482
72533
  }
72483
72534
  };
72484
72535
  }
72485
- this.modelSelections.setCurrent(agent, next);
72486
- const defaultOutcome = await this.saveDefaultSelection(next);
72487
- if (defaultOutcome.kind !== "committed") this.diag?.warn("model default save did not commit after the Session selection", {
72488
- session: sessionId,
72489
- kind: defaultOutcome.kind,
72490
- ...defaultOutcome.kind === "rejected" || defaultOutcome.kind === "indeterminate" ? { error: defaultOutcome.error.message } : {}
72491
- });
72536
+ owner.setCurrent(agent, next);
72537
+ this.startDefaultSave(next, sessionId);
72492
72538
  return {
72493
72539
  kind: "committed",
72494
72540
  value: { ...next }
@@ -72528,10 +72574,7 @@ var DirectPresetCatalog = class {
72528
72574
  async roster(signal) {
72529
72575
  signal?.throwIfAborted();
72530
72576
  const presets = this.presets();
72531
- if (presets === void 0) return {
72532
- presets: [],
72533
- modeSelectionEnabled: false
72534
- };
72577
+ if (presets === void 0) return { presets: [] };
72535
72578
  const roster = await presets.remoteExportList();
72536
72579
  signal?.throwIfAborted();
72537
72580
  const defaultId = roster.presets.find((preset) => preset.isDefault === true)?.id;
@@ -72542,8 +72585,7 @@ var DirectPresetCatalog = class {
72542
72585
  ...preset.description === void 0 ? {} : { description: preset.description },
72543
72586
  ...preset.broken === void 0 ? {} : { broken: preset.broken }
72544
72587
  })),
72545
- ...defaultId === void 0 ? {} : { defaultId },
72546
- modeSelectionEnabled: roster.modeSelectionEnabled
72588
+ ...defaultId === void 0 ? {} : { defaultId }
72547
72589
  };
72548
72590
  }
72549
72591
  async resolve(id, signal) {
@@ -73599,6 +73641,1846 @@ var DirectHostCommandPort = class {
73599
73641
  }
73600
73642
  };
73601
73643
  //#endregion
73644
+ //#region src/runtime/direct/plugin-manager-direct.ts
73645
+ /** Resolve one official localized text to the English fallback. */
73646
+ function localizedText(value) {
73647
+ if (value === void 0) return void 0;
73648
+ return typeof value === "string" ? value : value.en;
73649
+ }
73650
+ function detachIncompatible(entry) {
73651
+ return Object.freeze({
73652
+ name: entry.name,
73653
+ version: entry.version,
73654
+ runtimeVersion: entry.runtimeVersion,
73655
+ peers: Object.freeze({ ...entry.peers })
73656
+ });
73657
+ }
73658
+ function detachError(error) {
73659
+ if (error === void 0) return void 0;
73660
+ return Object.freeze({
73661
+ code: error.code,
73662
+ ...error.diagnostic === void 0 ? {} : { diagnostic: error.diagnostic },
73663
+ ...error.incompatible === void 0 ? {} : { incompatible: Object.freeze(error.incompatible.map(detachIncompatible)) }
73664
+ });
73665
+ }
73666
+ function detachBundle(bundle) {
73667
+ const rows = bundle.rows.map((row) => Object.freeze({
73668
+ rowId: row.rowId,
73669
+ moduleName: row.moduleName,
73670
+ ...row.meta === void 0 ? {} : pickMeta(row.meta),
73671
+ ...row.entryId === void 0 ? {} : { entryId: String(row.entryId) }
73672
+ }));
73673
+ return Object.freeze({
73674
+ name: bundle.name,
73675
+ ...bundle.version === void 0 ? {} : { version: bundle.version },
73676
+ ...bundle.meta === void 0 ? {} : pickMeta(bundle.meta),
73677
+ ...bundle.description === void 0 ? {} : { description: bundle.description },
73678
+ enabled: bundle.enabled,
73679
+ installed: bundle.installed,
73680
+ optional: bundle.optional,
73681
+ removable: bundle.removable,
73682
+ ...bundle.readOnlyReason === void 0 ? {} : { readOnlyReason: bundle.readOnlyReason },
73683
+ ...bundle.error === void 0 ? {} : { error: detachError(bundle.error) },
73684
+ rows: Object.freeze(rows),
73685
+ overrides: Object.freeze([...bundle.overrides])
73686
+ });
73687
+ }
73688
+ function pickMeta(meta) {
73689
+ const title = localizedText(meta.title);
73690
+ const description = localizedText(meta.description);
73691
+ return {
73692
+ ...title === void 0 ? {} : { title },
73693
+ ...description === void 0 ? {} : { description }
73694
+ };
73695
+ }
73696
+ function detachPlugin(entry) {
73697
+ const addressable = "patchId" in entry;
73698
+ return Object.freeze({
73699
+ entryId: String(entry.entryId),
73700
+ moduleName: entry.moduleName,
73701
+ ...entry.meta === void 0 ? {} : pickMeta(entry.meta),
73702
+ enabled: entry.enabled,
73703
+ fiberPhase: entry.fiberPhase,
73704
+ ...addressable ? { patchId: entry.patchId } : { readOnlyReason: entry.readOnlyReason }
73705
+ });
73706
+ }
73707
+ function detachRegistries(registries) {
73708
+ return Object.freeze({
73709
+ registry: registries.registry,
73710
+ fallbackRegistries: Object.freeze([...registries.fallbackRegistries]),
73711
+ resolved: registries.resolved
73712
+ });
73713
+ }
73714
+ function detachChange(result) {
73715
+ return Object.freeze({
73716
+ changed: result.changed,
73717
+ application: result.application,
73718
+ stage: result.stage,
73719
+ target: result.target,
73720
+ ...result.error === void 0 ? {} : { error: detachError(result.error) },
73721
+ ...result.warnings === void 0 ? {} : { warnings: Object.freeze([...result.warnings]) },
73722
+ ...result.bundle === void 0 ? {} : { bundle: result.bundle }
73723
+ });
73724
+ }
73725
+ function detachInspection(inspection) {
73726
+ if (inspection.status === "accepted") return Object.freeze({
73727
+ status: "accepted",
73728
+ kind: inspection.kind,
73729
+ ...inspection.name === void 0 ? {} : { name: inspection.name },
73730
+ ...inspection.version === void 0 ? {} : { version: inspection.version },
73731
+ ...inspection.description === void 0 ? {} : { description: inspection.description },
73732
+ bundle: inspection.bundle,
73733
+ registry: inspection.registry,
73734
+ ...inspection.host === void 0 ? {} : { host: inspection.host }
73735
+ });
73736
+ return Object.freeze({
73737
+ status: "refused",
73738
+ problem: inspection.problem,
73739
+ reason: inspection.reason,
73740
+ ...inspection.registries === void 0 ? {} : { registries: Object.freeze([...inspection.registries]) }
73741
+ });
73742
+ }
73743
+ function detachPhase(progress) {
73744
+ return Object.freeze({
73745
+ requestId: String(progress.requestId),
73746
+ phase: progress.phase,
73747
+ ...progress.attempt === void 0 ? {} : { attempt: Object.freeze({
73748
+ registry: progress.attempt.registry,
73749
+ index: progress.attempt.index,
73750
+ total: progress.attempt.total
73751
+ }) }
73752
+ });
73753
+ }
73754
+ function detachLog(chunk) {
73755
+ return Object.freeze({
73756
+ ...chunk.requestId === void 0 ? {} : { requestId: String(chunk.requestId) },
73757
+ jobId: chunk.jobId,
73758
+ stream: chunk.stream,
73759
+ text: chunk.text
73760
+ });
73761
+ }
73762
+ /** Generate one install request id (the branded shape is a plain string). */
73763
+ function pluginInstallRequestId(value) {
73764
+ return value;
73765
+ }
73766
+ /** The Direct backend's Plugin Manager port over the official Host service. */
73767
+ var DirectPluginManagerPort = class {
73768
+ ctx;
73769
+ constructor(ctx) {
73770
+ this.ctx = ctx;
73771
+ }
73772
+ /** The official service, read lazily so the adapter construction never
73773
+ * races the composition; a missing row is a fail-loud composition error. */
73774
+ service() {
73775
+ const service = this.ctx.get("pluginManager");
73776
+ if (service === void 0) throw new Error("pluginManager service unavailable: the supported DSH base must mount @deepseek-ai/dsh-plugin-manager");
73777
+ return service;
73778
+ }
73779
+ async snapshot() {
73780
+ const service = this.service();
73781
+ const [bundles, plugins, registries] = await Promise.all([
73782
+ service.listBundles(),
73783
+ service.listPlugins(),
73784
+ service.registries()
73785
+ ]);
73786
+ const exemptions = service.listVersionExemptions();
73787
+ const exemptionFacts = Object.entries(exemptions.exemptions).map(([packageVersion, runtimeVersions]) => Object.freeze({
73788
+ packageVersion,
73789
+ runtimeVersions: Object.freeze([...runtimeVersions])
73790
+ }));
73791
+ return Object.freeze({
73792
+ bundles: Object.freeze(bundles.map(detachBundle)),
73793
+ plugins: Object.freeze(plugins.map(detachPlugin)),
73794
+ registries: detachRegistries(registries),
73795
+ exemptions: Object.freeze(exemptionFacts),
73796
+ exemptionWarnings: Object.freeze([...exemptions.warnings])
73797
+ });
73798
+ }
73799
+ async inspect(spec, registry, signal) {
73800
+ return detachInspection(await this.service().inspect(spec, registry === void 0 ? void 0 : { registry }, signal));
73801
+ }
73802
+ async setBundleEnabled(name, enabled) {
73803
+ return detachChange(await this.service().setBundleEnabled(name, enabled));
73804
+ }
73805
+ async setPluginEnabled(id, enabled) {
73806
+ return detachChange(await this.service().setPluginEnabled(id, enabled));
73807
+ }
73808
+ async removeBundle(name) {
73809
+ return detachChange(await this.service().removeBundle(name));
73810
+ }
73811
+ async startInstall(request) {
73812
+ const options = {
73813
+ requestId: pluginInstallRequestId(request.requestId),
73814
+ registry: request.registry,
73815
+ ...request.enabled === void 0 ? {} : { enabled: request.enabled }
73816
+ };
73817
+ return detachChange(await this.service().installBundle(request.spec, options));
73818
+ }
73819
+ async waitForInstall(requestId) {
73820
+ const result = await this.service().waitForInstall(pluginInstallRequestId(requestId));
73821
+ return result === null ? null : detachChange(result);
73822
+ }
73823
+ async cancelInstall(requestId) {
73824
+ const result = await this.service().cancelInstall(pluginInstallRequestId(requestId));
73825
+ return Object.freeze({ status: result.status });
73826
+ }
73827
+ subscribeInstall(listener) {
73828
+ const offState = this.ctx.on("plugin-manager/install-state", (payload) => {
73829
+ listener({
73830
+ kind: "phase",
73831
+ phase: detachPhase(payload)
73832
+ });
73833
+ });
73834
+ const offLog = this.ctx.on("plugin-manager/install-log", (payload) => {
73835
+ listener({
73836
+ kind: "log",
73837
+ log: detachLog(payload)
73838
+ });
73839
+ });
73840
+ return () => {
73841
+ offState();
73842
+ offLog();
73843
+ };
73844
+ }
73845
+ };
73846
+ //#endregion
73847
+ //#region src/runtime/direct/job-observation-direct.ts
73848
+ /**
73849
+ * The Direct selected-Job observation adapter (P1-B0/B1): the ONLY module
73850
+ * that knows the official `@deepseek-ai/dsh-api-job-controller` Host service.
73851
+ *
73852
+ * It calls `JobController.follow()` — the official NON-CONSUMING Host
73853
+ * observer (upstream proof: `packages/api/job-controller/src/observe.ts`
73854
+ * reads ONLY `registry.readAt()` and never `registry.read()`; it neither
73855
+ * advances the model `job_output` cursor nor acknowledges a completion
73856
+ * notice) — and maps its frames onto detached snapshots. It never polls,
73857
+ * never builds a custom cursor/gap state machine and never calls
73858
+ * `jobs.read()`.
73859
+ *
73860
+ * @module @xmoon76/dsh-pi-tui/runtime/direct/job-observation-direct
73861
+ */
73862
+ /** The retained-output tail bound (a presentation preview, not an archive). */
73863
+ const TAIL_MAX_CHARS = 65536;
73864
+ function isTerminalStatus(status) {
73865
+ return status === "completed" || status === "killed" || status === "failed";
73866
+ }
73867
+ /** The Direct backend's selected-Job observation port. */
73868
+ var DirectJobObservationPort = class {
73869
+ ctx;
73870
+ diag;
73871
+ constructor(ctx, diag) {
73872
+ this.ctx = ctx;
73873
+ this.diag = diag;
73874
+ }
73875
+ open(sessionId, jobId, listener) {
73876
+ const abort = new AbortController();
73877
+ let closed = false;
73878
+ let text = "";
73879
+ let current;
73880
+ let last;
73881
+ const emit = (patch = {}) => {
73882
+ const job = current;
73883
+ last = Object.freeze({
73884
+ jobId: job === void 0 ? jobId : String(job.id),
73885
+ kind: job?.kind ?? "",
73886
+ label: job?.label ?? "",
73887
+ status: job?.status ?? "running",
73888
+ ...job?.progress === void 0 ? {} : { progress: job.progress },
73889
+ ...job?.detail === void 0 ? {} : { detail: job.detail },
73890
+ text,
73891
+ gapBefore: false,
73892
+ settled: job !== void 0 && isTerminalStatus(job.status),
73893
+ ...patch
73894
+ });
73895
+ listener(last);
73896
+ };
73897
+ const service = this.ctx.get("jobController");
73898
+ if (service === void 0) throw new Error("jobController service unavailable: the supported DSH base plus the pi-tui bundle must mount @deepseek-ai/dsh-api-job-controller");
73899
+ runOwned("job observation", async () => {
73900
+ let gap = false;
73901
+ for await (const frame of service.follow({
73902
+ sessionId,
73903
+ jobId
73904
+ }, abort.signal)) {
73905
+ if (abort.signal.aborted) return;
73906
+ if (frame.type === "opened") {
73907
+ gap = frame.job.output.earliest > 0;
73908
+ current = frame.job;
73909
+ emit({ gapBefore: gap });
73910
+ continue;
73911
+ }
73912
+ if (frame.type === "output") {
73913
+ if (frame.lossy === true) gap = true;
73914
+ for (const chunk of frame.chunks) {
73915
+ if (chunk.gapBefore === true) gap = true;
73916
+ text += chunk.text;
73917
+ }
73918
+ if (text.length > TAIL_MAX_CHARS) text = text.slice(text.length - TAIL_MAX_CHARS);
73919
+ emit({ gapBefore: gap });
73920
+ continue;
73921
+ }
73922
+ current = frame.job;
73923
+ emit({ gapBefore: gap });
73924
+ if (isTerminalStatus(frame.job.status)) return;
73925
+ }
73926
+ }, {
73927
+ diag: this.diag,
73928
+ isCancellation: () => abort.signal.aborted,
73929
+ onError: (error) => {
73930
+ if (closed || abort.signal.aborted) return;
73931
+ const base = last;
73932
+ listener(Object.freeze({
73933
+ jobId,
73934
+ kind: base?.kind ?? "",
73935
+ label: base?.label ?? "",
73936
+ status: base?.status ?? "running",
73937
+ text,
73938
+ gapBefore: base?.gapBefore ?? false,
73939
+ settled: base?.settled ?? false,
73940
+ error: safeErrorMessage$1(error)
73941
+ }));
73942
+ }
73943
+ });
73944
+ return () => {
73945
+ if (closed) return;
73946
+ closed = true;
73947
+ abort.abort();
73948
+ };
73949
+ }
73950
+ };
73951
+ //#endregion
73952
+ //#region src/plugin-manager/classify.ts
73953
+ /** The bundle that provides the currently running TUI surface. */
73954
+ const SELF_BUNDLE = "@xmoon76/dsh-pi-tui";
73955
+ /** Aggregate several proven observations of ONE card into detached facts. */
73956
+ function aggregate(observations) {
73957
+ const entryIds = /* @__PURE__ */ new Set();
73958
+ const kinds = /* @__PURE__ */ new Set();
73959
+ let contributionCount = 0;
73960
+ let failed = 0;
73961
+ let active = 0;
73962
+ let advanced = false;
73963
+ let unstable = false;
73964
+ for (const observation of observations) {
73965
+ if (observation.entryId !== void 0) entryIds.add(observation.entryId);
73966
+ for (const kind of observation.contributionKinds) kinds.add(kind);
73967
+ contributionCount += observation.contributionCount;
73968
+ if (observation.health === "active") active += 1;
73969
+ else if (observation.health === "failed") failed += 1;
73970
+ else {
73971
+ active += 1;
73972
+ failed += 1;
73973
+ }
73974
+ if (observation.usesAdvancedCapability) advanced = true;
73975
+ if (observation.usesUnstableCapability) unstable = true;
73976
+ }
73977
+ return Object.freeze({
73978
+ entryIds: Object.freeze([...entryIds].sort()),
73979
+ contributionKinds: Object.freeze([...kinds].sort()),
73980
+ contributionCount,
73981
+ health: failed === 0 ? "active" : active === 0 ? "failed" : "mixed",
73982
+ usesAdvancedCapability: advanced,
73983
+ usesUnstableCapability: unstable
73984
+ });
73985
+ }
73986
+ /**
73987
+ * Classify every package card. Association requires a PROVEN Loader entry id
73988
+ * that belongs to exactly one card; otherwise the card stays `dsh-plugin`.
73989
+ */
73990
+ function classifyPluginPackages(inputs, observations) {
73991
+ const perCard = /* @__PURE__ */ new Map();
73992
+ for (const observation of observations) {
73993
+ if (observation.entryId === void 0) continue;
73994
+ const owners = inputs.filter((input) => input.entryIds.includes(observation.entryId));
73995
+ if (owners.length !== 1) continue;
73996
+ const key = owners[0].key;
73997
+ const list = perCard.get(key);
73998
+ if (list === void 0) perCard.set(key, [observation]);
73999
+ else list.push(observation);
74000
+ }
74001
+ const out = /* @__PURE__ */ new Map();
74002
+ for (const input of inputs) {
74003
+ if (input.bundleName === "@xmoon76/dsh-pi-tui") {
74004
+ out.set(input.key, { role: "current-tui" });
74005
+ continue;
74006
+ }
74007
+ const owned = perCard.get(input.key);
74008
+ if (owned === void 0 || owned.length === 0) {
74009
+ out.set(input.key, { role: "dsh-plugin" });
74010
+ continue;
74011
+ }
74012
+ out.set(input.key, {
74013
+ role: "tui-extension",
74014
+ extension: aggregate(owned)
74015
+ });
74016
+ }
74017
+ return out;
74018
+ }
74019
+ //#endregion
74020
+ //#region src/plugin-manager/model.ts
74021
+ /** Stable identity of one bundle card. */
74022
+ function bundleValue(name) {
74023
+ return `bundle:${name}`;
74024
+ }
74025
+ /** Stable identity of one standalone plugin-entry card. */
74026
+ function entryValue(entryId) {
74027
+ return `entry:${entryId}`;
74028
+ }
74029
+ /** The action values the panel maps to operations. */
74030
+ const PLUGIN_ACTION = {
74031
+ install: "action:install",
74032
+ refresh: "action:refresh",
74033
+ close: "action:close",
74034
+ toggle: "action:toggle",
74035
+ remove: "action:remove",
74036
+ back: "action:back",
74037
+ confirmRemove: "action:confirm-remove",
74038
+ cancelRemove: "action:cancel-remove"
74039
+ };
74040
+ /** The row-level toggle value for one addressable plugin entry id. */
74041
+ function rowToggleValue(entryId) {
74042
+ return `action:toggle-row:${entryId}`;
74043
+ }
74044
+ /** The entry id of a row-level toggle value, or undefined. */
74045
+ function rowToggleEntryId(value) {
74046
+ return value.startsWith("action:toggle-row:") ? value.slice(18) : void 0;
74047
+ }
74048
+ function section(label) {
74049
+ return {
74050
+ value: `section:${label}`,
74051
+ kind: "section",
74052
+ label,
74053
+ tone: "dim",
74054
+ selectable: false
74055
+ };
74056
+ }
74057
+ function info(label, secondary) {
74058
+ return {
74059
+ value: `info:${label}:${secondary ?? ""}`,
74060
+ kind: "info",
74061
+ label,
74062
+ ...secondary === void 0 ? {} : { secondary },
74063
+ tone: "dim",
74064
+ selectable: false
74065
+ };
74066
+ }
74067
+ function action(value, label, tone = "normal") {
74068
+ return {
74069
+ value,
74070
+ kind: "action",
74071
+ label,
74072
+ tone,
74073
+ selectable: true
74074
+ };
74075
+ }
74076
+ /** Human text for one role, shown as the card's classification chip. */
74077
+ function roleLabel(role) {
74078
+ switch (role) {
74079
+ case "current-tui": return "Current TUI";
74080
+ case "tui-extension": return "TUI extension";
74081
+ default: return "DSH plugin";
74082
+ }
74083
+ }
74084
+ function cardBadge(card) {
74085
+ if (card.error !== void 0) return "error";
74086
+ if (card.readOnlyReason !== void 0) return "read-only";
74087
+ return card.enabled ? "active" : "disabled";
74088
+ }
74089
+ function cardTone(card) {
74090
+ if (card.error !== void 0) return "error";
74091
+ if (card.role === "current-tui") return "normal";
74092
+ if (card.readOnlyReason !== void 0) return "warning";
74093
+ return card.enabled ? "normal" : "dim";
74094
+ }
74095
+ /** The card's one-line secondary text: description + factual flags. */
74096
+ function cardSecondary(card) {
74097
+ const text = card.title ?? card.description;
74098
+ const flags = [roleLabel(card.role)];
74099
+ if (card.role === "tui-extension" && card.extension !== void 0) {
74100
+ flags.push(`${card.extension.contributionCount} contribution${card.extension.contributionCount === 1 ? "" : "s"}`);
74101
+ flags.push(card.extension.health);
74102
+ }
74103
+ if (!card.installed) flags.push("provided");
74104
+ if (card.optional) flags.push("optional");
74105
+ const head = text ?? "";
74106
+ const tail = flags.join(" · ");
74107
+ return head === "" ? tail : `${head} · ${tail}`;
74108
+ }
74109
+ /** Render one Host error fact as an actionable line. */
74110
+ function errorText(error) {
74111
+ const parts = [error.code];
74112
+ if (error.diagnostic !== void 0 && error.diagnostic !== "") parts.push(error.diagnostic);
74113
+ for (const entry of error.incompatible ?? []) parts.push(incompatibleText(entry));
74114
+ return parts.join(" — ");
74115
+ }
74116
+ /** Render one rejected package with its required peer ranges. */
74117
+ function incompatibleText(entry) {
74118
+ const peers = Object.entries(entry.peers).map(([name, range]) => `${name} ${range}`).join(", ");
74119
+ return `${entry.name}@${entry.version} rejects DSH ${entry.runtimeVersion}${peers === "" ? "" : ` (needs ${peers})`}`;
74120
+ }
74121
+ /** Format the exemption list for the diagnostics section. */
74122
+ function exemptionText(entry) {
74123
+ return `${entry.packageVersion} → ${entry.runtimeVersions.join(", ")}`;
74124
+ }
74125
+ /** The registries summary rendered on the list footer/detail. */
74126
+ function registriesText(registries) {
74127
+ const parts = [registries.registry === null ? "pnpm default" : registries.registry];
74128
+ if (registries.fallbackRegistries.length > 0) parts.push(`fallbacks: ${registries.fallbackRegistries.join(", ")}`);
74129
+ if (registries.resolved !== null) parts.push(`resolved: ${registries.resolved}`);
74130
+ return parts.join(" · ");
74131
+ }
74132
+ function rowView(row, live, selfOwned) {
74133
+ const entryId = live?.entryId ?? row.entryId ?? "";
74134
+ const title = live?.title ?? row.title;
74135
+ const description = live?.description ?? row.description;
74136
+ const addressable = live !== void 0 && live.patchId !== void 0 && live.readOnlyReason === void 0;
74137
+ return Object.freeze({
74138
+ entryId,
74139
+ moduleName: live?.moduleName ?? row.moduleName,
74140
+ ...title === void 0 ? {} : { title },
74141
+ ...description === void 0 ? {} : { description },
74142
+ enabled: live?.enabled ?? false,
74143
+ fiberPhase: live?.fiberPhase ?? null,
74144
+ live: live !== void 0,
74145
+ ...live?.patchId === void 0 ? {} : { patchId: live.patchId },
74146
+ ...live?.readOnlyReason === void 0 ? {} : { readOnlyReason: live.readOnlyReason },
74147
+ selfProtected: selfOwned,
74148
+ canToggle: addressable && !selfOwned
74149
+ });
74150
+ }
74151
+ function entryRowView(entry, selfOwned) {
74152
+ const addressable = entry.patchId !== void 0 && entry.readOnlyReason === void 0;
74153
+ return Object.freeze({
74154
+ entryId: entry.entryId,
74155
+ moduleName: entry.moduleName,
74156
+ ...entry.title === void 0 ? {} : { title: entry.title },
74157
+ ...entry.description === void 0 ? {} : { description: entry.description },
74158
+ enabled: entry.enabled,
74159
+ fiberPhase: entry.fiberPhase,
74160
+ live: true,
74161
+ ...entry.patchId === void 0 ? {} : { patchId: entry.patchId },
74162
+ ...entry.readOnlyReason === void 0 ? {} : { readOnlyReason: entry.readOnlyReason },
74163
+ selfProtected: selfOwned,
74164
+ canToggle: addressable && !selfOwned
74165
+ });
74166
+ }
74167
+ /**
74168
+ * Build the classified model. `claims` comes from
74169
+ * {@link classifyPluginPackages}; an unclassified card falls back to
74170
+ * `dsh-plugin`.
74171
+ */
74172
+ function buildPluginManagerModel(snapshot, claims) {
74173
+ const liveByEntry = /* @__PURE__ */ new Map();
74174
+ for (const entry of snapshot.plugins) liveByEntry.set(entry.entryId, entry);
74175
+ const bundledEntryIds = /* @__PURE__ */ new Set();
74176
+ for (const bundle of snapshot.bundles) for (const row of bundle.rows) if (row.entryId !== void 0) bundledEntryIds.add(String(row.entryId));
74177
+ const selfBundle = snapshot.bundles.find((bundle) => bundle.name === SELF_BUNDLE);
74178
+ const selfProtectionIds = /* @__PURE__ */ new Set([...(selfBundle?.rows ?? []).map((row) => row.rowId), ...selfBundle?.overrides ?? []]);
74179
+ const cards = [];
74180
+ for (const bundle of snapshot.bundles) {
74181
+ const value = bundleValue(bundle.name);
74182
+ const claim = claims.get(value);
74183
+ const role = claim?.role ?? "dsh-plugin";
74184
+ const isSelf = role === "current-tui";
74185
+ const rows = bundle.rows.map((row) => {
74186
+ return rowView(row, row.entryId === void 0 ? void 0 : liveByEntry.get(String(row.entryId)), isSelf || selfProtectionIds.has(row.rowId));
74187
+ });
74188
+ const hostToggle = bundle.readOnlyReason === void 0;
74189
+ const hostRemove = bundle.removable && bundle.readOnlyReason === void 0;
74190
+ cards.push(Object.freeze({
74191
+ value,
74192
+ source: "bundle",
74193
+ role,
74194
+ name: bundle.name,
74195
+ ...bundle.version === void 0 ? {} : { version: bundle.version },
74196
+ ...bundle.title === void 0 ? {} : { title: bundle.title },
74197
+ ...bundle.description === void 0 ? {} : { description: bundle.description },
74198
+ enabled: bundle.enabled,
74199
+ installed: bundle.installed,
74200
+ optional: bundle.optional,
74201
+ removable: bundle.removable,
74202
+ ...bundle.readOnlyReason === void 0 ? {} : { readOnlyReason: bundle.readOnlyReason },
74203
+ ...bundle.error === void 0 ? {} : { error: bundle.error },
74204
+ rows: Object.freeze(rows),
74205
+ overrides: Object.freeze([...bundle.overrides]),
74206
+ ...claim?.extension === void 0 ? {} : { extension: claim.extension },
74207
+ canToggle: hostToggle && !isSelf,
74208
+ canRemove: hostRemove && !isSelf,
74209
+ isSelf
74210
+ }));
74211
+ }
74212
+ for (const entry of snapshot.plugins) {
74213
+ if (bundledEntryIds.has(entry.entryId)) continue;
74214
+ const value = entryValue(entry.entryId);
74215
+ const claim = claims.get(value);
74216
+ const role = claim?.role ?? "dsh-plugin";
74217
+ const selfProtected = entry.patchId !== void 0 && selfProtectionIds.has(entry.patchId);
74218
+ const row = entryRowView(entry, selfProtected);
74219
+ cards.push(Object.freeze({
74220
+ value,
74221
+ source: "entry",
74222
+ role,
74223
+ name: entry.moduleName,
74224
+ ...entry.title === void 0 ? {} : { title: entry.title },
74225
+ ...entry.description === void 0 ? {} : { description: entry.description },
74226
+ enabled: entry.enabled,
74227
+ installed: true,
74228
+ optional: false,
74229
+ removable: false,
74230
+ ...entry.readOnlyReason === void 0 ? {} : { readOnlyReason: entry.readOnlyReason },
74231
+ rows: Object.freeze([row]),
74232
+ overrides: Object.freeze([]),
74233
+ ...claim?.extension === void 0 ? {} : { extension: claim.extension },
74234
+ canToggle: row.canToggle,
74235
+ canRemove: false,
74236
+ isSelf: selfProtected
74237
+ }));
74238
+ }
74239
+ const empty = [];
74240
+ const currentTui = cards.filter((card) => card.role === "current-tui");
74241
+ const tuiExtensions = cards.filter((card) => card.role === "tui-extension");
74242
+ const dshPlugins = cards.filter((card) => card.role === "dsh-plugin");
74243
+ return Object.freeze({
74244
+ currentTui: Object.freeze(currentTui.length > 0 ? currentTui : empty),
74245
+ tuiExtensions: Object.freeze(tuiExtensions),
74246
+ dshPlugins: Object.freeze(dshPlugins),
74247
+ registries: snapshot.registries,
74248
+ exemptions: snapshot.exemptions,
74249
+ exemptionWarnings: snapshot.exemptionWarnings
74250
+ });
74251
+ }
74252
+ function cardRow(card, busyValue) {
74253
+ const name = card.version === void 0 ? card.name : `${card.name} ${card.version}`;
74254
+ const label = busyValue === card.value ? `${name} …` : name;
74255
+ return {
74256
+ value: card.value,
74257
+ kind: "card",
74258
+ label,
74259
+ ...cardSecondary(card) === void 0 ? {} : { secondary: cardSecondary(card) },
74260
+ badge: cardBadge(card),
74261
+ tone: cardTone(card),
74262
+ selectable: true
74263
+ };
74264
+ }
74265
+ /** The top-level list: three sections, then the global actions. */
74266
+ function pluginManagerListRows(model, busyValue) {
74267
+ const rows = [];
74268
+ if (model.currentTui.length > 0) {
74269
+ rows.push(section("Current TUI"));
74270
+ for (const card of model.currentTui) rows.push(cardRow(card, busyValue));
74271
+ }
74272
+ if (model.tuiExtensions.length > 0) {
74273
+ rows.push(section("TUI Extensions"));
74274
+ for (const card of model.tuiExtensions) rows.push(cardRow(card, busyValue));
74275
+ }
74276
+ rows.push(section("DSH Plugins"));
74277
+ for (const card of model.dshPlugins) rows.push(cardRow(card, busyValue));
74278
+ rows.push(section("Registries"));
74279
+ rows.push(info("registries", registriesText(model.registries)));
74280
+ if (model.exemptions.length > 0) {
74281
+ rows.push(section("Version exemptions"));
74282
+ for (const entry of model.exemptions) rows.push(info("exemption", exemptionText(entry)));
74283
+ }
74284
+ for (const warning of model.exemptionWarnings) rows.push(info("exemption warning", warning));
74285
+ rows.push(section("Actions"));
74286
+ rows.push(action(PLUGIN_ACTION.install, "Install…"));
74287
+ rows.push(action(PLUGIN_ACTION.refresh, "Refresh"));
74288
+ rows.push(action(PLUGIN_ACTION.close, "Close"));
74289
+ return rows;
74290
+ }
74291
+ /** Package detail: metadata, declared rows, diagnostics and item actions.
74292
+ * `exemptions` cross-references the profile's exact-version exemptions so a
74293
+ * compatibility error says whether one already applies (plan §11). */
74294
+ function cardDetailRows(card, exemptions = []) {
74295
+ const rows = [];
74296
+ const status = [card.enabled ? "enabled" : "disabled", roleLabel(card.role)];
74297
+ if (!card.installed) status.push("provided by the installation");
74298
+ if (card.optional) status.push("optional");
74299
+ status.push(card.removable ? "removable" : "not removable");
74300
+ rows.push(info("Status", status.join(" · ")));
74301
+ if (card.readOnlyReason !== void 0) rows.push(info("Read-only", card.readOnlyReason));
74302
+ if (card.error !== void 0) {
74303
+ rows.push({
74304
+ ...info("Error", errorText(card.error)),
74305
+ tone: "error"
74306
+ });
74307
+ for (const entry of card.error.incompatible ?? []) {
74308
+ const key = `${entry.name}@${entry.version}`;
74309
+ const exemption = exemptions.find((candidate) => candidate.packageVersion === key);
74310
+ rows.push(info(" exemption", exemption === void 0 ? `${key}: no exact-version exemption applies` : `${key}: exempted for DSH ${exemption.runtimeVersions.join(", ")}`));
74311
+ }
74312
+ }
74313
+ if (card.role === "current-tui") rows.push(info("This bundle provides the current TUI and is managed outside this screen."));
74314
+ else if (card.isSelf) rows.push(info("This row is controlled by the Current TUI composition; it is not changed from this screen."));
74315
+ if (card.extension !== void 0) {
74316
+ const extension = card.extension;
74317
+ rows.push(info("TUI contributions", `${extension.contributionCount} · ${extension.health}`));
74318
+ rows.push(info("Contribution kinds", extension.contributionKinds.join(", ")));
74319
+ rows.push(info("Proven entries", extension.entryIds.join(", ")));
74320
+ if (extension.usesAdvancedCapability) rows.push(info("Capability use", "Uses Advanced capability"));
74321
+ if (extension.usesUnstableCapability) rows.push(info("Capability use", "Uses Unstable capability"));
74322
+ }
74323
+ if (card.rows.length > 0) {
74324
+ rows.push(info("Plugin rows"));
74325
+ for (const row of card.rows) {
74326
+ const text = row.title ?? row.description ?? row.moduleName;
74327
+ const state = row.live ? `${row.enabled ? "enabled" : "disabled"} · ${row.fiberPhase ?? "no live fiber"}` : "not live";
74328
+ rows.push(info(` ${row.entryId === "" ? row.moduleName : row.entryId}`, `${text} · ${state}`));
74329
+ if (row.selfProtected && card.role !== "current-tui") rows.push(info(" protected", "controlled by the Current TUI composition"));
74330
+ if (row.canToggle) rows.push(action(rowToggleValue(row.entryId), row.enabled ? ` Disable row ${row.entryId}` : ` Enable row ${row.entryId}`));
74331
+ }
74332
+ }
74333
+ if (card.overrides.length > 0) rows.push(info("Overrides", card.overrides.join(", ")));
74334
+ rows.push(info("Actions"));
74335
+ if (card.canToggle) rows.push(action(PLUGIN_ACTION.toggle, card.enabled ? "Disable" : "Enable"));
74336
+ if (card.canRemove) rows.push(action(PLUGIN_ACTION.remove, "Remove…", "warning"));
74337
+ rows.push(action(PLUGIN_ACTION.back, "Back"));
74338
+ return rows;
74339
+ }
74340
+ /** Human text for one completed mutation, including restart/override facts. */
74341
+ function changeSummary(change) {
74342
+ const parts = [`${change.stage} ${change.application}`];
74343
+ if (change.bundle !== void 0) parts.push(`bundle ${change.bundle}`);
74344
+ if (change.error !== void 0) parts.push(errorText(change.error));
74345
+ if (change.application === "restart-required") parts.push("restart the profile to apply");
74346
+ if (change.application === "overridden") parts.push("a higher-priority override still wins");
74347
+ for (const warning of change.warnings ?? []) parts.push(warning);
74348
+ return parts.join(" · ");
74349
+ }
74350
+ //#endregion
74351
+ //#region src/plugin-manager/controller.ts
74352
+ const LOG_MAX_LINES = 40;
74353
+ const LOG_MAX_CHARS = 4e3;
74354
+ let requestCounter = 0;
74355
+ /** One user install attempt owns exactly one id. */
74356
+ function nextInstallRequestId() {
74357
+ requestCounter += 1;
74358
+ return `plugin-install-${Date.now().toString(36)}-${requestCounter}`;
74359
+ }
74360
+ const EMPTY_MODEL = Object.freeze({
74361
+ currentTui: Object.freeze([]),
74362
+ tuiExtensions: Object.freeze([]),
74363
+ dshPlugins: Object.freeze([]),
74364
+ registries: Object.freeze({
74365
+ registry: null,
74366
+ fallbackRegistries: Object.freeze([]),
74367
+ resolved: null
74368
+ }),
74369
+ exemptions: Object.freeze([]),
74370
+ exemptionWarnings: Object.freeze([])
74371
+ });
74372
+ /** The Plugin Manager operation/presentation owner. */
74373
+ var PluginManagerController = class {
74374
+ port;
74375
+ hooks;
74376
+ observationSource;
74377
+ snapshot;
74378
+ model = EMPTY_MODEL;
74379
+ state = "loading";
74380
+ error;
74381
+ /** The exact notice text of the last failed inventory read, so a later
74382
+ * successful read can clear it without clobbering an operation message. */
74383
+ refreshErrorMessage;
74384
+ entryHost = "direct-command";
74385
+ mode = "list";
74386
+ selected = 0;
74387
+ detailValue;
74388
+ confirming;
74389
+ busy;
74390
+ message;
74391
+ readEpoch = 0;
74392
+ committedEpoch = 0;
74393
+ mutationToken = 0;
74394
+ install;
74395
+ installEpoch = 0;
74396
+ unsubscribe;
74397
+ disposed = false;
74398
+ constructor(port, hooks, options) {
74399
+ this.port = port;
74400
+ this.hooks = hooks;
74401
+ this.observationSource = options?.observations;
74402
+ this.unsubscribe = port.subscribeInstall((event) => this.onInstallEvent(event));
74403
+ }
74404
+ /** One owned detached task (AGENTS.md: never a bare `void` promise chain). */
74405
+ run(label, task) {
74406
+ runDetached(label, task, { diag: this.hooks.diag });
74407
+ }
74408
+ /** Open the shared surface from one host: read once and show the right view. */
74409
+ open(host) {
74410
+ if (this.disposed) return;
74411
+ this.entryHost = host;
74412
+ const phase = this.install?.phase;
74413
+ const dispatched = phase === "starting" || phase === "installing" || phase === "applying" || phase === "cancelling";
74414
+ this.mode = dispatched ? "install" : "list";
74415
+ this.selected = 0;
74416
+ this.detailValue = void 0;
74417
+ this.confirming = void 0;
74418
+ this.message = void 0;
74419
+ this.run("plugin manager read", () => this.read());
74420
+ }
74421
+ /** Explicit Refresh: read the official inventory again. `read()` owns the
74422
+ * notice clearing (it removes only a previous read-failure notice, so a
74423
+ * just-shown operation outcome survives a refresh). */
74424
+ refresh() {
74425
+ if (this.disposed) return;
74426
+ this.run("plugin manager read", () => this.read());
74427
+ }
74428
+ /** Release the install-event subscription (runner teardown). */
74429
+ dispose() {
74430
+ if (this.disposed) return;
74431
+ this.disposed = true;
74432
+ this.unsubscribe();
74433
+ }
74434
+ /** Current rendered rows for the panel. */
74435
+ rows() {
74436
+ if (this.mode === "detail") {
74437
+ const card = this.selectedCard();
74438
+ if (card !== void 0) return cardDetailRows(card, this.model.exemptions);
74439
+ return pluginManagerListRows(this.model, this.busy);
74440
+ }
74441
+ if (this.mode === "confirm-remove") {
74442
+ const name = this.confirming?.name ?? "";
74443
+ const stale = this.confirmingCard() === void 0;
74444
+ return [
74445
+ {
74446
+ value: "info:confirm",
74447
+ kind: "info",
74448
+ label: `Remove ${name}?`,
74449
+ secondary: "unloads the bundle, then removes the dependency",
74450
+ tone: "warning",
74451
+ selectable: false
74452
+ },
74453
+ {
74454
+ value: PLUGIN_ACTION.confirmRemove,
74455
+ kind: "action",
74456
+ label: "Remove",
74457
+ tone: "warning",
74458
+ selectable: true
74459
+ },
74460
+ {
74461
+ value: PLUGIN_ACTION.cancelRemove,
74462
+ kind: "action",
74463
+ label: "Cancel",
74464
+ tone: "normal",
74465
+ selectable: true
74466
+ },
74467
+ ...stale ? [{
74468
+ value: "info:stale",
74469
+ kind: "info",
74470
+ label: "This bundle changed since the confirmation opened; re-open it from the list.",
74471
+ tone: "error",
74472
+ selectable: false
74473
+ }] : []
74474
+ ];
74475
+ }
74476
+ return pluginManagerListRows(this.model, this.busy);
74477
+ }
74478
+ /** The stable identity of the currently selected selectable row. */
74479
+ selectedValue() {
74480
+ const selectable = this.rows().filter((row) => row.selectable);
74481
+ if (selectable.length === 0) return void 0;
74482
+ return selectable[Math.min(this.selected, selectable.length - 1)]?.value;
74483
+ }
74484
+ /** The one-line footer hint for the current mode. */
74485
+ hint() {
74486
+ if (this.mode === "install") return this.installHint();
74487
+ if (this.mode === "confirm-remove") return "Enter confirm · Esc cancel";
74488
+ if (this.mode === "detail") return "Enter action · Esc back · R refresh";
74489
+ return this.entryHost === "settings-submenu" ? "Enter open/act · R refresh · I install · Esc back" : "Enter open/act · R refresh · I install · Esc close";
74490
+ }
74491
+ /** The panel title. */
74492
+ title() {
74493
+ if (this.mode === "install") return "Plugin Manager · install";
74494
+ if (this.mode === "detail") {
74495
+ const card = this.selectedCard();
74496
+ if (card !== void 0) return `Plugin Manager · ${card.name}`;
74497
+ }
74498
+ return "Plugin Manager";
74499
+ }
74500
+ /** The transient notice line (an outcome or a stale-confirmation warning). */
74501
+ notice() {
74502
+ return this.message;
74503
+ }
74504
+ /** The load/error state for the panel's empty frame. */
74505
+ status() {
74506
+ return {
74507
+ state: this.state,
74508
+ ...this.error === void 0 ? {} : { error: this.error }
74509
+ };
74510
+ }
74511
+ /** The install dialog view, when the panel is in install mode. */
74512
+ installView() {
74513
+ if (this.mode !== "install" || this.install === void 0) return void 0;
74514
+ const active = this.install;
74515
+ return {
74516
+ phase: active.phase,
74517
+ spec: active.spec,
74518
+ registry: active.registry,
74519
+ ...active.inspection === void 0 ? {} : { inspection: active.inspection },
74520
+ log: active.log,
74521
+ ...active.outcome === void 0 ? {} : { outcome: active.outcome },
74522
+ ...active.message === void 0 ? {} : { message: active.message },
74523
+ cancellable: active.phase === "installing" || active.phase === "starting" || active.phase === "cancelling",
74524
+ ...active.phase === "editing" || active.phase === "inspecting" || active.phase === "confirm" ? {} : { requestId: active.requestId }
74525
+ };
74526
+ }
74527
+ /** The Host-offered registries for the install dialog (Host facts + null). */
74528
+ registryOptions() {
74529
+ const registries = this.snapshot?.registries;
74530
+ const options = [];
74531
+ options.push({
74532
+ value: null,
74533
+ label: "pnpm configuration default"
74534
+ });
74535
+ if (registries !== void 0) {
74536
+ if (registries.registry !== null) options.push({
74537
+ value: registries.registry,
74538
+ label: registries.registry
74539
+ });
74540
+ for (const fallback of registries.fallbackRegistries) if (fallback !== registries.registry) options.push({
74541
+ value: fallback,
74542
+ label: fallback
74543
+ });
74544
+ }
74545
+ return options;
74546
+ }
74547
+ /** Move the selection among the current mode's selectable rows. */
74548
+ move(delta) {
74549
+ const count = this.rows().filter((row) => row.selectable).length;
74550
+ if (count === 0) return;
74551
+ const next = Math.min(count - 1, Math.max(0, this.selected + delta));
74552
+ if (next === this.selected) return;
74553
+ this.selected = next;
74554
+ this.hooks.requestRender();
74555
+ }
74556
+ /** Activate the selected row. */
74557
+ activate() {
74558
+ const selectable = this.rows().filter((row) => row.selectable);
74559
+ const row = selectable[Math.min(this.selected, Math.max(0, selectable.length - 1))];
74560
+ if (row === void 0) return;
74561
+ if (row.kind === "card") {
74562
+ this.detailValue = row.value;
74563
+ this.mode = "detail";
74564
+ this.selected = 0;
74565
+ this.message = void 0;
74566
+ this.hooks.requestRender();
74567
+ return;
74568
+ }
74569
+ const toggleEntryId = rowToggleEntryId(row.value);
74570
+ if (toggleEntryId !== void 0) {
74571
+ this.run("plugin manager row toggle", () => this.runToggleRow(toggleEntryId));
74572
+ return;
74573
+ }
74574
+ switch (row.value) {
74575
+ case PLUGIN_ACTION.back:
74576
+ this.mode = "list";
74577
+ this.detailValue = void 0;
74578
+ this.selected = 0;
74579
+ this.hooks.requestRender();
74580
+ return;
74581
+ case PLUGIN_ACTION.refresh:
74582
+ this.run("plugin manager read", () => this.read());
74583
+ return;
74584
+ case PLUGIN_ACTION.close:
74585
+ this.hooks.requestClose();
74586
+ return;
74587
+ case PLUGIN_ACTION.install:
74588
+ this.openInstall();
74589
+ return;
74590
+ case PLUGIN_ACTION.toggle:
74591
+ this.run("plugin manager toggle", () => this.toggleSelectedCard());
74592
+ return;
74593
+ case PLUGIN_ACTION.remove:
74594
+ this.requestRemoveSelected();
74595
+ return;
74596
+ case PLUGIN_ACTION.confirmRemove:
74597
+ this.run("plugin manager remove", () => this.runRemove());
74598
+ return;
74599
+ case PLUGIN_ACTION.cancelRemove:
74600
+ this.mode = "detail";
74601
+ this.confirming = void 0;
74602
+ this.selected = 0;
74603
+ this.hooks.requestRender();
74604
+ return;
74605
+ default: return;
74606
+ }
74607
+ }
74608
+ /** Esc semantics: leave the innermost view; close only from the list. */
74609
+ back() {
74610
+ if (this.mode === "install") {
74611
+ const phase = this.install?.phase;
74612
+ if (phase === "confirm" || phase === "inspecting") {
74613
+ this.editInstall();
74614
+ return;
74615
+ }
74616
+ this.closeInstall();
74617
+ return;
74618
+ }
74619
+ if (this.mode === "confirm-remove") {
74620
+ this.mode = "detail";
74621
+ this.confirming = void 0;
74622
+ this.selected = 0;
74623
+ this.hooks.requestRender();
74624
+ return;
74625
+ }
74626
+ if (this.mode === "detail") {
74627
+ this.mode = "list";
74628
+ this.detailValue = void 0;
74629
+ this.selected = 0;
74630
+ this.hooks.requestRender();
74631
+ return;
74632
+ }
74633
+ this.hooks.requestClose();
74634
+ }
74635
+ /** Open the install dialog; an active operation is reopened, never restarted. */
74636
+ openInstall() {
74637
+ const active = this.install;
74638
+ if (active !== void 0 && active.phase !== "done" && active.phase !== "failed" && active.phase !== "unknown") {
74639
+ this.mode = "install";
74640
+ this.selected = 0;
74641
+ this.hooks.requestRender();
74642
+ return;
74643
+ }
74644
+ this.installEpoch += 1;
74645
+ this.install = {
74646
+ requestId: nextInstallRequestId(),
74647
+ spec: "",
74648
+ registry: null,
74649
+ phase: "editing",
74650
+ log: []
74651
+ };
74652
+ this.mode = "install";
74653
+ this.selected = 0;
74654
+ this.message = void 0;
74655
+ this.hooks.requestRender();
74656
+ }
74657
+ /** Inspect one spec against the Host before offering installation. */
74658
+ inspect(spec, registry) {
74659
+ const active = this.install;
74660
+ if (active === void 0 || this.mode !== "install") return;
74661
+ const trimmed = spec.trim();
74662
+ if (trimmed === "") {
74663
+ active.message = "Enter a package spec first";
74664
+ this.hooks.requestRender();
74665
+ return;
74666
+ }
74667
+ if (registry !== null && !/^https?:\/\/\S+$/i.test(registry)) {
74668
+ active.message = "registry must be an http(s) URL";
74669
+ this.hooks.requestRender();
74670
+ return;
74671
+ }
74672
+ const epoch = ++this.installEpoch;
74673
+ active.inspectAbort?.abort();
74674
+ const abort = new AbortController();
74675
+ active.inspectAbort = abort;
74676
+ active.spec = trimmed;
74677
+ active.registry = registry;
74678
+ active.phase = "inspecting";
74679
+ active.message = void 0;
74680
+ active.inspection = void 0;
74681
+ this.hooks.requestRender();
74682
+ this.run("plugin manager inspect", () => this.performInspect(trimmed, registry, epoch, abort));
74683
+ }
74684
+ async performInspect(spec, registry, epoch, abort) {
74685
+ try {
74686
+ const inspection = await this.port.inspect(spec, registry, abort.signal);
74687
+ if (this.disposed || this.installEpoch !== epoch) return;
74688
+ const current = this.install;
74689
+ if (current === void 0) return;
74690
+ current.inspection = inspection;
74691
+ current.phase = "confirm";
74692
+ current.message = inspection.status === "refused" ? `refused: ${inspection.problem} — ${inspection.reason}` : void 0;
74693
+ this.hooks.requestRender();
74694
+ } catch (error) {
74695
+ if (this.disposed || this.installEpoch !== epoch) return;
74696
+ const current = this.install;
74697
+ if (current === void 0) return;
74698
+ current.phase = "editing";
74699
+ current.message = `inspect failed: ${errorMessage(error)}`;
74700
+ this.hooks.requestRender();
74701
+ }
74702
+ }
74703
+ /** Return to spec entry from the confirm/inspecting step (aborts an inspect). */
74704
+ editInstall() {
74705
+ const active = this.install;
74706
+ if (active === void 0 || this.mode !== "install") return;
74707
+ active.inspectAbort?.abort();
74708
+ active.inspectAbort = void 0;
74709
+ this.installEpoch += 1;
74710
+ active.phase = "editing";
74711
+ active.message = void 0;
74712
+ this.hooks.requestRender();
74713
+ }
74714
+ /** Dispatch the official install after an accepted inspection. */
74715
+ confirmInstall() {
74716
+ const active = this.install;
74717
+ if (active === void 0 || active.phase !== "confirm") return;
74718
+ if (active.inspection?.status !== "accepted") return;
74719
+ active.phase = "starting";
74720
+ active.message = void 0;
74721
+ this.hooks.requestRender();
74722
+ const requestId = active.requestId;
74723
+ this.run("plugin manager install", () => this.performInstall(requestId));
74724
+ }
74725
+ /** Dispatch installBundle and settle it; an indeterminate failure recovers
74726
+ * through the official waitForInstall and NEVER retries the install. */
74727
+ async performInstall(requestId) {
74728
+ const active = this.install;
74729
+ if (active === void 0 || active.requestId !== requestId) return;
74730
+ let outcome;
74731
+ try {
74732
+ outcome = await this.port.startInstall({
74733
+ requestId,
74734
+ spec: active.spec,
74735
+ registry: active.registry,
74736
+ enabled: true
74737
+ });
74738
+ } catch (error) {
74739
+ if (this.disposed) return;
74740
+ let recovered = null;
74741
+ try {
74742
+ recovered = await this.port.waitForInstall(requestId);
74743
+ } catch {
74744
+ recovered = null;
74745
+ }
74746
+ if (this.disposed) return;
74747
+ const current = this.install;
74748
+ if (current === void 0 || current.requestId !== requestId) return;
74749
+ if (recovered !== null) {
74750
+ this.settleInstall(requestId, recovered);
74751
+ return;
74752
+ }
74753
+ current.phase = "unknown";
74754
+ current.message = `install outcome unknown: ${errorMessage(error)} — check the inventory below; not retried automatically`;
74755
+ this.run("plugin manager read", () => this.read());
74756
+ this.hooks.requestRender();
74757
+ if (!this.hooks.isOpen()) this.hooks.notify("plugin install outcome unknown; inventory refreshed", "error");
74758
+ return;
74759
+ }
74760
+ if (this.disposed) return;
74761
+ this.settleInstall(requestId, outcome);
74762
+ }
74763
+ /** Stop an active installation; only valid before the Host starts applying. */
74764
+ cancelInstall() {
74765
+ const active = this.install;
74766
+ if (active === void 0) return;
74767
+ if (active.phase !== "installing" && active.phase !== "starting") return;
74768
+ active.phase = "cancelling";
74769
+ active.message = void 0;
74770
+ this.hooks.requestRender();
74771
+ this.run("plugin manager cancel install", () => this.performCancel(active.requestId));
74772
+ }
74773
+ async performCancel(requestId) {
74774
+ try {
74775
+ const cancellation = await this.port.cancelInstall(requestId);
74776
+ if (this.disposed) return;
74777
+ const current = this.install;
74778
+ if (current === void 0 || current.requestId !== requestId) return;
74779
+ if (cancellation.status === "cancelled") {
74780
+ current.phase = "done";
74781
+ current.message = "cancelled — profile files restored";
74782
+ } else if (cancellation.status === "too-late") {
74783
+ current.phase = "applying";
74784
+ current.message = "too late to cancel: the bundle is being applied";
74785
+ } else {
74786
+ current.phase = "unknown";
74787
+ current.message = "the installation already settled; the outcome will reconcile";
74788
+ this.run("plugin manager read", () => this.read());
74789
+ }
74790
+ this.hooks.requestRender();
74791
+ } catch (error) {
74792
+ if (this.disposed) return;
74793
+ const current = this.install;
74794
+ if (current === void 0 || current.requestId !== requestId) return;
74795
+ current.phase = "installing";
74796
+ current.message = `cancel failed: ${errorMessage(error)}`;
74797
+ this.hooks.requestRender();
74798
+ }
74799
+ }
74800
+ /** Leave the install dialog without touching the Host operation. */
74801
+ closeInstall() {
74802
+ if (this.install?.phase === "inspecting") {
74803
+ this.install.inspectAbort?.abort();
74804
+ this.install.inspectAbort = void 0;
74805
+ this.installEpoch += 1;
74806
+ this.install.phase = "editing";
74807
+ }
74808
+ this.mode = "list";
74809
+ this.selected = 0;
74810
+ this.hooks.requestRender();
74811
+ }
74812
+ /**
74813
+ * Toggle the selected card through the official authority. Public because
74814
+ * it is the operation the panel's action row dispatches; it re-checks the
74815
+ * EFFECTIVE capability, so a Current-TUI card can never be mutated here
74816
+ * even if a future UI accidentally exposes the action.
74817
+ */
74818
+ async toggleSelectedCard() {
74819
+ const card = this.activeCard();
74820
+ if (card === void 0) return;
74821
+ if (!card.canToggle) {
74822
+ this.message = card.isSelf ? selfRefusal(card.name, card.role) : `cannot change ${card.name}: ${card.readOnlyReason ?? "read-only"}`;
74823
+ this.hooks.requestRender();
74824
+ return;
74825
+ }
74826
+ const token = ++this.mutationToken;
74827
+ this.busy = card.value;
74828
+ this.message = void 0;
74829
+ this.hooks.requestRender();
74830
+ try {
74831
+ const change = card.source === "bundle" ? await this.port.setBundleEnabled(card.name, !card.enabled) : await this.port.setPluginEnabled(card.rows[0].entryId, !card.enabled);
74832
+ if (this.disposed || token !== this.mutationToken) return;
74833
+ this.message = changeSummary(change);
74834
+ } catch (error) {
74835
+ if (this.disposed || token !== this.mutationToken) return;
74836
+ this.message = `enable/disable failed: ${errorMessage(error)}`;
74837
+ } finally {
74838
+ if (!this.disposed && token === this.mutationToken) this.busy = void 0;
74839
+ }
74840
+ await this.read();
74841
+ }
74842
+ async runToggleRow(entryId) {
74843
+ const row = this.activeCard()?.rows.find((candidate) => candidate.entryId === entryId);
74844
+ if (row === void 0) return;
74845
+ if (!row.canToggle) {
74846
+ this.message = row.selfProtected ? `${entryId} is controlled by the Current TUI composition and cannot be changed from here` : `cannot change ${entryId}: ${row.readOnlyReason ?? "not addressable"}`;
74847
+ this.hooks.requestRender();
74848
+ return;
74849
+ }
74850
+ const token = ++this.mutationToken;
74851
+ this.busy = rowToggleValueBusy(entryId);
74852
+ this.message = void 0;
74853
+ this.hooks.requestRender();
74854
+ try {
74855
+ const change = await this.port.setPluginEnabled(entryId, !row.enabled);
74856
+ if (this.disposed || token !== this.mutationToken) return;
74857
+ this.message = changeSummary(change);
74858
+ } catch (error) {
74859
+ if (this.disposed || token !== this.mutationToken) return;
74860
+ this.message = `enable/disable failed: ${errorMessage(error)}`;
74861
+ } finally {
74862
+ if (!this.disposed && token === this.mutationToken) this.busy = void 0;
74863
+ }
74864
+ await this.read();
74865
+ }
74866
+ /**
74867
+ * Open the destructive confirmation for the selected bundle. Public for the
74868
+ * same reason as {@link toggleSelectedCard}: it re-checks the effective
74869
+ * capability before a confirmation is ever shown.
74870
+ */
74871
+ requestRemoveSelected() {
74872
+ const card = this.activeCard();
74873
+ if (card === void 0 || card.source !== "bundle") return;
74874
+ if (!card.canRemove) {
74875
+ this.message = card.isSelf ? selfRefusal(card.name, card.role) : `cannot remove ${card.name}: ${card.readOnlyReason ?? "not removable"}`;
74876
+ this.hooks.requestRender();
74877
+ return;
74878
+ }
74879
+ this.confirming = {
74880
+ value: card.value,
74881
+ name: card.name,
74882
+ epoch: this.readEpoch
74883
+ };
74884
+ this.mode = "confirm-remove";
74885
+ this.selected = 0;
74886
+ this.message = void 0;
74887
+ this.hooks.requestRender();
74888
+ }
74889
+ async runRemove() {
74890
+ const confirmation = this.confirming;
74891
+ if (confirmation === void 0) return;
74892
+ const card = this.confirmingCard();
74893
+ if (card === void 0 || card.isSelf || !card.canRemove) {
74894
+ this.mode = "detail";
74895
+ this.confirming = void 0;
74896
+ this.selected = 0;
74897
+ this.message = card?.isSelf === true ? selfRefusal(confirmation.name, card?.role ?? "dsh-plugin") : "the bundle changed since the confirmation opened; nothing was removed";
74898
+ this.hooks.requestRender();
74899
+ return;
74900
+ }
74901
+ const token = ++this.mutationToken;
74902
+ this.busy = confirmation.value;
74903
+ this.mode = "detail";
74904
+ this.confirming = void 0;
74905
+ this.selected = 0;
74906
+ this.hooks.requestRender();
74907
+ try {
74908
+ const change = await this.port.removeBundle(confirmation.name);
74909
+ if (this.disposed || token !== this.mutationToken) return;
74910
+ this.message = changeSummary(change);
74911
+ if (change.error !== void 0) this.detailValue = void 0;
74912
+ } catch (error) {
74913
+ if (this.disposed || token !== this.mutationToken) return;
74914
+ this.message = `remove failed: ${errorMessage(error)}`;
74915
+ } finally {
74916
+ if (!this.disposed && token === this.mutationToken) this.busy = void 0;
74917
+ }
74918
+ await this.read();
74919
+ }
74920
+ allCards() {
74921
+ return [
74922
+ ...this.model.currentTui,
74923
+ ...this.model.tuiExtensions,
74924
+ ...this.model.dshPlugins
74925
+ ];
74926
+ }
74927
+ /** The card of the open detail view (detail mode only). */
74928
+ selectedCard() {
74929
+ if (this.detailValue === void 0) return void 0;
74930
+ return this.allCards().find((card) => card.value === this.detailValue);
74931
+ }
74932
+ /**
74933
+ * The card a mutation targets: the open detail card, else the selected list
74934
+ * row's card. Resolving from the SELECTED ROW (never an index) keeps the
74935
+ * dispatch guard reachable even when the action row is not rendered.
74936
+ */
74937
+ activeCard() {
74938
+ if (this.mode === "detail") return this.selectedCard();
74939
+ const selectable = this.rows().filter((row) => row.selectable);
74940
+ if (selectable.length === 0) return void 0;
74941
+ const value = selectable[Math.min(this.selected, selectable.length - 1)]?.value;
74942
+ if (value === void 0) return void 0;
74943
+ return this.allCards().find((card) => card.value === value);
74944
+ }
74945
+ confirmingCard() {
74946
+ const confirmation = this.confirming;
74947
+ if (confirmation === void 0) return void 0;
74948
+ if (confirmation.epoch !== this.readEpoch) return void 0;
74949
+ return this.allCards().find((card) => card.value === confirmation.value);
74950
+ }
74951
+ buildModel(snapshot) {
74952
+ const observations = this.observationSource?.() ?? [];
74953
+ const inputs = [];
74954
+ const bundledEntryIds = /* @__PURE__ */ new Set();
74955
+ for (const bundle of snapshot.bundles) {
74956
+ for (const row of bundle.rows) if (row.entryId !== void 0) bundledEntryIds.add(String(row.entryId));
74957
+ inputs.push({
74958
+ key: bundleValue(bundle.name),
74959
+ bundleName: bundle.name,
74960
+ entryIds: bundle.rows.flatMap((row) => row.entryId === void 0 ? [] : [String(row.entryId)])
74961
+ });
74962
+ }
74963
+ for (const entry of snapshot.plugins) {
74964
+ if (bundledEntryIds.has(entry.entryId)) continue;
74965
+ inputs.push({
74966
+ key: entryValue(entry.entryId),
74967
+ entryIds: [entry.entryId]
74968
+ });
74969
+ }
74970
+ return buildPluginManagerModel(snapshot, classifyPluginPackages(inputs, observations));
74971
+ }
74972
+ async read() {
74973
+ if (this.disposed) return;
74974
+ const epoch = ++this.readEpoch;
74975
+ this.state = "loading";
74976
+ this.hooks.requestRender();
74977
+ try {
74978
+ const snapshot = await this.port.snapshot();
74979
+ if (this.disposed || epoch < this.committedEpoch) return;
74980
+ this.committedEpoch = epoch;
74981
+ this.snapshot = snapshot;
74982
+ this.model = this.buildModel(snapshot);
74983
+ this.state = "ready";
74984
+ this.error = void 0;
74985
+ if (this.message === this.refreshErrorMessage) this.message = void 0;
74986
+ this.refreshErrorMessage = void 0;
74987
+ this.clampSelection();
74988
+ this.hooks.requestRender();
74989
+ } catch (error) {
74990
+ if (this.disposed || epoch < this.committedEpoch) return;
74991
+ this.committedEpoch = epoch;
74992
+ this.state = "error";
74993
+ this.error = errorMessage(error);
74994
+ this.refreshErrorMessage = `refresh failed: ${this.error}`;
74995
+ this.message = this.refreshErrorMessage;
74996
+ this.hooks.requestRender();
74997
+ }
74998
+ }
74999
+ clampSelection() {
75000
+ const count = this.rows().filter((row) => row.selectable).length;
75001
+ this.selected = count === 0 ? 0 : Math.min(this.selected, count - 1);
75002
+ }
75003
+ settleInstall(requestId, outcome) {
75004
+ const current = this.install;
75005
+ if (current === void 0 || current.requestId !== requestId) return;
75006
+ current.outcome = outcome;
75007
+ if (outcome.application === "failed") {
75008
+ current.phase = "failed";
75009
+ current.message = changeSummary(outcome);
75010
+ } else if (outcome.application === "cancelled") {
75011
+ current.phase = "done";
75012
+ current.message = "cancelled — profile files restored";
75013
+ } else {
75014
+ current.phase = "done";
75015
+ current.message = changeSummary(outcome);
75016
+ }
75017
+ this.hooks.requestRender();
75018
+ this.run("plugin manager read", () => this.read());
75019
+ if (!this.hooks.isOpen()) this.hooks.notify(`plugin install ${outcome.application}: ${outcome.target}`, outcome.application === "failed" ? "error" : "info");
75020
+ }
75021
+ onInstallEvent(event) {
75022
+ if (this.disposed) return;
75023
+ const active = this.install;
75024
+ if (active === void 0) return;
75025
+ if (event.kind === "phase") {
75026
+ if (event.phase.requestId !== active.requestId) return;
75027
+ if (event.phase.phase === "installing") active.phase = "installing";
75028
+ else if (event.phase.phase === "applying") active.phase = "applying";
75029
+ else if (event.phase.phase === "cancelling") active.phase = "cancelling";
75030
+ if (event.phase.attempt !== void 0) active.message = `installing from ${event.phase.attempt.registry ?? "pnpm default"} (${event.phase.attempt.index}/${event.phase.attempt.total})`;
75031
+ this.hooks.requestRender();
75032
+ return;
75033
+ }
75034
+ if (event.log.requestId !== active.requestId) return;
75035
+ this.appendLog(active, event.log.stream, event.log.text);
75036
+ this.hooks.requestRender();
75037
+ }
75038
+ appendLog(active, stream, text) {
75039
+ if (text === "") return;
75040
+ for (const line of text.split("\n")) {
75041
+ if (line === "") continue;
75042
+ active.log.push(stream === "stderr" ? `! ${line}` : line);
75043
+ }
75044
+ if (active.log.length > LOG_MAX_LINES) active.log.splice(0, active.log.length - LOG_MAX_LINES);
75045
+ let total = active.log.reduce((sum, line) => sum + line.length + 1, 0);
75046
+ while (total > LOG_MAX_CHARS && active.log.length > 1) total -= active.log.shift().length + 1;
75047
+ }
75048
+ installHint() {
75049
+ const active = this.install;
75050
+ if (active === void 0) return "Enter inspect · Esc close";
75051
+ if (active.phase === "editing") return "Enter inspect · Esc close";
75052
+ if (active.phase === "inspecting") return "inspecting…";
75053
+ if (active.phase === "confirm") return active.inspection?.status === "accepted" ? "Enter install · Esc edit" : "Esc edit";
75054
+ if (active.phase === "done" || active.phase === "failed" || active.phase === "unknown") return "Esc close dialog";
75055
+ return active.phase === "installing" || active.phase === "starting" || active.phase === "cancelling" ? "Esc hide · C cancel" : "Esc hide";
75056
+ }
75057
+ };
75058
+ /** The busy key for one row-level toggle (never the card row's identity). */
75059
+ function rowToggleValueBusy(entryId) {
75060
+ return `action:toggle-row:${entryId}`;
75061
+ }
75062
+ /** The refusal text for a self-protected target: the whole TUI bundle vs one of
75063
+ * its modules (a protected standalone entry is not "the current TUI" itself). */
75064
+ function selfRefusal(name, role) {
75065
+ return role === "current-tui" ? `${name} provides the current TUI and cannot be changed from here` : `${name} is controlled by the Current TUI composition and cannot be changed from here`;
75066
+ }
75067
+ function errorMessage(error) {
75068
+ return error instanceof Error ? error.message : String(error);
75069
+ }
75070
+ //#endregion
75071
+ //#region src/plugin-manager/host-registry.ts
75072
+ /** Token-owned active-host registry (see the module header). */
75073
+ var PluginManagerHostRegistry = class {
75074
+ active;
75075
+ /** Whether any Plugin Manager surface currently owns the manager. */
75076
+ isOpen() {
75077
+ return this.active !== void 0;
75078
+ }
75079
+ /** Invoke the ACTIVE surface's user close path (no-op when closed). */
75080
+ closeActive() {
75081
+ this.active?.close();
75082
+ }
75083
+ /**
75084
+ * Claim the manager for one surface. `close` is the surface's user close
75085
+ * path (what {@link closeActive} calls); `onNormalClose` runs ONLY for a
75086
+ * normal close. A later claim nests over — and restores — the previous owner.
75087
+ */
75088
+ claim(close, onNormalClose = () => {}) {
75089
+ const token = {};
75090
+ const previous = this.active;
75091
+ this.active = {
75092
+ token,
75093
+ close
75094
+ };
75095
+ const restore = () => {
75096
+ if (this.active?.token !== token) return false;
75097
+ this.active = previous;
75098
+ return true;
75099
+ };
75100
+ return {
75101
+ token,
75102
+ closeNormally: () => {
75103
+ if (restore()) onNormalClose();
75104
+ },
75105
+ releaseExternally: () => {
75106
+ restore();
75107
+ }
75108
+ };
75109
+ }
75110
+ };
75111
+ //#endregion
75112
+ //#region src/plugin-manager/panel.ts
75113
+ /**
75114
+ * Plugin Manager panel (P1-A/A2/A3): rendering and input only. It draws the
75115
+ * rows the controller exposes, converts semantic keys into controller
75116
+ * actions, and owns the install dialog's text widgets (spec + custom
75117
+ * registry). It never reads a Host service, never mutates inventory and
75118
+ * never decides success.
75119
+ *
75120
+ * @module @xmoon76/dsh-pi-tui/plugin-manager/panel
75121
+ */
75122
+ function tone(text, rowTone) {
75123
+ switch (rowTone) {
75124
+ case "dim": return color.textDim(text);
75125
+ case "success": return color.success(text);
75126
+ case "warning": return color.warning(text);
75127
+ case "error": return color.error(text);
75128
+ default: return text;
75129
+ }
75130
+ }
75131
+ function badge(text, rowTone) {
75132
+ return tone(`[${text}]`, rowTone);
75133
+ }
75134
+ /** The Plugin Manager overlay component. */
75135
+ var PluginManagerPanel = class {
75136
+ controller;
75137
+ requestRender;
75138
+ maxRows = Number.POSITIVE_INFINITY;
75139
+ _focused = false;
75140
+ disposed = false;
75141
+ specInput = new Input();
75142
+ registryInput = new Input();
75143
+ installFocus = "spec";
75144
+ registryIndex = 0;
75145
+ customRegistry = false;
75146
+ /** Fired once when the owning surface disposes this panel (any hide path). */
75147
+ onDispose;
75148
+ disposeNotified = false;
75149
+ constructor(controller, requestRender, options = {}) {
75150
+ this.controller = controller;
75151
+ this.requestRender = requestRender;
75152
+ this.onDispose = options.onDispose;
75153
+ this.registryInput.setValue("");
75154
+ }
75155
+ /** The frame's row budget (frame height minus its two border rows). */
75156
+ setMaxRows(rows) {
75157
+ this.maxRows = Number.isFinite(rows) ? Math.max(1, Math.floor(rows)) : Number.POSITIVE_INFINITY;
75158
+ }
75159
+ get focused() {
75160
+ return this._focused;
75161
+ }
75162
+ set focused(value) {
75163
+ this._focused = value;
75164
+ this.syncInputFocus();
75165
+ }
75166
+ invalidate() {
75167
+ this.specInput.invalidate();
75168
+ this.registryInput.invalidate();
75169
+ }
75170
+ dispose() {
75171
+ this.disposed = true;
75172
+ this.specInput.focused = false;
75173
+ this.registryInput.focused = false;
75174
+ if (this.disposeNotified) return;
75175
+ this.disposeNotified = true;
75176
+ this.onDispose?.();
75177
+ }
75178
+ handleInput(data) {
75179
+ if (this.disposed) return;
75180
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
75181
+ this.controller.back();
75182
+ return;
75183
+ }
75184
+ const install = this.controller.installView();
75185
+ if (install !== void 0) {
75186
+ this.handleInstallInput(data, install);
75187
+ return;
75188
+ }
75189
+ if (matchesKey(data, "up") || matchesKey(data, "ctrl+p")) {
75190
+ this.controller.move(-1);
75191
+ this.requestRender();
75192
+ return;
75193
+ }
75194
+ if (matchesKey(data, "down") || matchesKey(data, "ctrl+n")) {
75195
+ this.controller.move(1);
75196
+ this.requestRender();
75197
+ return;
75198
+ }
75199
+ if (matchesKey(data, "enter")) {
75200
+ this.controller.activate();
75201
+ this.requestRender();
75202
+ return;
75203
+ }
75204
+ if (data === "r" || data === "R") {
75205
+ this.controller.refresh();
75206
+ return;
75207
+ }
75208
+ if (data === "i" || data === "I") {
75209
+ this.controller.openInstall();
75210
+ return;
75211
+ }
75212
+ }
75213
+ handleInstallInput(data, install) {
75214
+ if (install.phase === "editing") {
75215
+ if (matchesKey(data, "tab")) {
75216
+ this.installFocus = this.installFocus === "spec" ? "registry" : "spec";
75217
+ this.syncInputFocus();
75218
+ this.requestRender();
75219
+ return;
75220
+ }
75221
+ if (this.installFocus === "spec") {
75222
+ if (matchesKey(data, "enter")) {
75223
+ this.submitInspect();
75224
+ return;
75225
+ }
75226
+ this.specInput.handleInput(data);
75227
+ this.requestRender();
75228
+ return;
75229
+ }
75230
+ if (this.customRegistry) {
75231
+ if (matchesKey(data, "enter")) {
75232
+ this.submitInspect();
75233
+ return;
75234
+ }
75235
+ this.registryInput.handleInput(data);
75236
+ this.requestRender();
75237
+ return;
75238
+ }
75239
+ if (matchesKey(data, "up")) {
75240
+ this.cycleRegistry(-1);
75241
+ return;
75242
+ }
75243
+ if (matchesKey(data, "down")) {
75244
+ this.cycleRegistry(1);
75245
+ return;
75246
+ }
75247
+ if (matchesKey(data, "enter")) {
75248
+ this.submitInspect();
75249
+ return;
75250
+ }
75251
+ return;
75252
+ }
75253
+ if (install.phase === "confirm") {
75254
+ if (matchesKey(data, "enter") && install.inspection?.status === "accepted") {
75255
+ this.controller.confirmInstall();
75256
+ this.requestRender();
75257
+ }
75258
+ return;
75259
+ }
75260
+ if (data === "c" || data === "C") {
75261
+ this.controller.cancelInstall();
75262
+ this.requestRender();
75263
+ }
75264
+ }
75265
+ submitInspect() {
75266
+ this.controller.inspect(this.specInput.getValue(), this.chosenRegistry());
75267
+ this.requestRender();
75268
+ }
75269
+ cycleRegistry(delta) {
75270
+ const options = this.controller.registryOptions();
75271
+ const total = options.length + 1;
75272
+ this.registryIndex = ((this.registryIndex + delta) % total + total) % total;
75273
+ this.customRegistry = this.registryIndex === options.length;
75274
+ this.syncInputFocus();
75275
+ this.requestRender();
75276
+ }
75277
+ chosenRegistry() {
75278
+ if (this.customRegistry) return this.registryInput.getValue().trim();
75279
+ return this.controller.registryOptions()[this.registryIndex]?.value ?? null;
75280
+ }
75281
+ syncInputFocus() {
75282
+ const editing = this.controller.installView()?.phase === "editing";
75283
+ this.specInput.focused = this._focused && editing === true && this.installFocus === "spec";
75284
+ this.registryInput.focused = this._focused && editing === true && this.installFocus === "registry" && this.customRegistry;
75285
+ }
75286
+ render(width) {
75287
+ const safeWidth = Math.max(1, width);
75288
+ this.syncInputFocus();
75289
+ const install = this.controller.installView();
75290
+ const rendered = install === void 0 ? this.renderList(safeWidth) : this.renderInstall(safeWidth, install);
75291
+ const limit = Number.isFinite(this.maxRows) ? Math.max(1, Math.floor(this.maxRows)) : Number.POSITIVE_INFINITY;
75292
+ return this.windowLines(rendered.lines, rendered.selectedLine, limit);
75293
+ }
75294
+ /**
75295
+ * Selection-aware viewport: the header (title + blank) and the hint stay
75296
+ * fixed, while the body scrolls so the SELECTED row is always visible. When
75297
+ * content is clipped, `↑ more` / `↓ more` mark the hidden rows.
75298
+ */
75299
+ windowLines(lines, selectedLine, limit) {
75300
+ if (lines.length <= limit) return [...lines];
75301
+ const hintCount = limit >= 2 ? 1 : 0;
75302
+ const headerCount = Math.min(2, Math.max(0, limit - hintCount - 1));
75303
+ const available = Math.max(1, limit - headerCount - hintCount);
75304
+ const header = lines.slice(0, headerCount);
75305
+ const trailing = hintCount === 0 ? void 0 : lines[lines.length - 1];
75306
+ const body = lines.slice(headerCount, lines.length - hintCount);
75307
+ const selectedBody = selectedLine === void 0 ? 0 : Math.max(0, selectedLine - headerCount);
75308
+ const startFor = (rows) => {
75309
+ const centered = selectedBody - Math.floor(rows / 2);
75310
+ return Math.min(Math.max(0, centered), Math.max(0, body.length - rows));
75311
+ };
75312
+ let budget = available;
75313
+ let start = startFor(budget);
75314
+ let top = start > 0;
75315
+ let bottom = start + budget < body.length;
75316
+ while (budget > 1 && budget + (top ? 1 : 0) + (bottom ? 1 : 0) > available) {
75317
+ budget -= 1;
75318
+ start = startFor(budget);
75319
+ top = start > 0;
75320
+ bottom = start + budget < body.length;
75321
+ }
75322
+ if (budget + (top ? 1 : 0) + (bottom ? 1 : 0) > available) {
75323
+ top = false;
75324
+ bottom = false;
75325
+ budget = available;
75326
+ start = startFor(budget);
75327
+ }
75328
+ const out = [...header];
75329
+ if (top) out.push(color.textDim(" ↑ more"));
75330
+ out.push(...body.slice(start, start + budget));
75331
+ if (bottom) out.push(color.textDim(" ↓ more"));
75332
+ if (trailing !== void 0) out.push(trailing);
75333
+ return out;
75334
+ }
75335
+ renderList(width) {
75336
+ const rows = this.controller.rows();
75337
+ const selected = this.controller.selectedValue();
75338
+ const lines = [];
75339
+ let selectedLine;
75340
+ lines.push(color.textStrong(truncateToWidth(this.controller.title(), width, "…")));
75341
+ lines.push("");
75342
+ for (const row of rows) {
75343
+ if (row.kind === "section") {
75344
+ lines.push(color.textStrong(truncateToWidth(row.label, width, "…")));
75345
+ continue;
75346
+ }
75347
+ const isSelected = row.selectable && selected !== void 0 && row.value === selected;
75348
+ if (isSelected) selectedLine = lines.length;
75349
+ let line = `${row.selectable ? isSelected ? color.accent("▸ ") : " " : " "}${row.kind === "info" ? color.textMuted(truncateToWidth(row.label, Math.max(1, width - 2), "…")) : tone(truncateToWidth(row.label, Math.max(1, width - 2), "…"), row.tone)}`;
75350
+ if (row.badge !== void 0) line += ` ${badge(row.badge, row.tone)}`;
75351
+ if (row.secondary !== void 0) {
75352
+ const room = width - visibleWidth(line) - 3;
75353
+ if (room > 8) line += ` ${color.textDim(truncateToWidth(row.secondary, room, "…"))}`;
75354
+ }
75355
+ lines.push(line);
75356
+ }
75357
+ const notice = this.controller.notice();
75358
+ if (notice !== void 0) {
75359
+ lines.push("");
75360
+ lines.push(color.textMuted(truncateToWidth(notice, width, "…")));
75361
+ }
75362
+ if (this.controller.status().state === "loading" && rows.every((row) => row.kind === "section" || row.kind === "info")) lines.push(color.textDim(truncateToWidth("Loading plugins…", width, "…")));
75363
+ lines.push("");
75364
+ lines.push(color.textDim(truncateToWidth(this.controller.hint(), width, "…")));
75365
+ return {
75366
+ lines,
75367
+ selectedLine
75368
+ };
75369
+ }
75370
+ renderInstall(width, install) {
75371
+ const lines = [];
75372
+ lines.push(color.textStrong(truncateToWidth(this.controller.title(), width, "…")));
75373
+ lines.push("");
75374
+ if (install.phase === "editing") {
75375
+ lines.push(`Spec ${this.specInput.render(Math.max(1, width - 8))[0] ?? ""}`);
75376
+ const options = this.controller.registryOptions();
75377
+ const registry = this.customRegistry ? `custom ${this.registryInput.render(Math.max(1, width - 16))[0] ?? ""}` : options[this.registryIndex]?.label ?? "pnpm configuration default";
75378
+ const focusHint = this.installFocus === "registry" ? color.accent("‹") : " ";
75379
+ lines.push(`Reg ${focusHint} ${truncateToWidth(registry, Math.max(1, width - 10), "…")}`);
75380
+ if (!this.customRegistry) {
75381
+ const hint = options.map((option) => option.label).join(" · ");
75382
+ if (hint !== "") lines.push(color.textMuted(truncateToWidth(` ${hint}`, width, "…")));
75383
+ }
75384
+ lines.push("");
75385
+ if (install.message !== void 0) lines.push(color.warning(truncateToWidth(install.message, width, "…")));
75386
+ lines.push(color.textDim(truncateToWidth("Tab switch field · ↑↓ choose registry · Enter inspect · Esc close", width, "…")));
75387
+ return {
75388
+ lines,
75389
+ selectedLine: void 0
75390
+ };
75391
+ }
75392
+ if (install.phase === "inspecting") {
75393
+ lines.push(color.textDim(truncateToWidth(`Inspecting ${install.spec}…`, width, "…")));
75394
+ lines.push("");
75395
+ lines.push(color.textDim(truncateToWidth("Esc back", width, "…")));
75396
+ return {
75397
+ lines,
75398
+ selectedLine: void 0
75399
+ };
75400
+ }
75401
+ if (install.phase === "confirm") {
75402
+ const inspection = install.inspection;
75403
+ if (inspection?.status === "accepted") {
75404
+ lines.push(color.textStrong(truncateToWidth(inspection.name ?? install.spec, width, "…")));
75405
+ if (inspection.version !== void 0) lines.push(color.textDim(truncateToWidth(`version ${inspection.version}`, width, "…")));
75406
+ if (inspection.description !== void 0) lines.push(color.textDim(truncateToWidth(inspection.description, width, "…")));
75407
+ lines.push(color.textDim(truncateToWidth(`source ${inspection.kind} · registry ${inspection.registry ?? "pnpm default"}${inspection.host === void 0 ? "" : ` · host ${inspection.host}`}`, width, "…")));
75408
+ lines.push(color.textDim(truncateToWidth(`bundle: ${inspection.bundle === null ? "unknown" : inspection.bundle ? "yes" : "no"}`, width, "…")));
75409
+ lines.push("");
75410
+ lines.push(color.textDim(truncateToWidth("Enter install · Esc edit", width, "…")));
75411
+ } else if (inspection?.status === "refused") {
75412
+ lines.push(color.error(truncateToWidth(`refused: ${inspection.problem}`, width, "…")));
75413
+ lines.push(color.textMuted(truncateToWidth(inspection.reason, width, "…")));
75414
+ if (inspection.registries !== void 0 && inspection.registries.length > 0) lines.push(color.textMuted(truncateToWidth(`registries: ${inspection.registries.map((r) => r ?? "pnpm default").join(", ")}`, width, "…")));
75415
+ lines.push("");
75416
+ lines.push(color.textDim(truncateToWidth("Esc edit", width, "…")));
75417
+ }
75418
+ return {
75419
+ lines,
75420
+ selectedLine: void 0
75421
+ };
75422
+ }
75423
+ lines.push(color.textStrong(truncateToWidth(`${install.phase}${install.spec === "" ? "" : ` · ${install.spec}`}`, width, "…")));
75424
+ if (install.message !== void 0) lines.push(color.textMuted(truncateToWidth(install.message, width, "…")));
75425
+ lines.push("");
75426
+ const hint = this.controller.hint();
75427
+ const budget = Number.isFinite(this.maxRows) ? Math.max(0, Math.floor(this.maxRows) - lines.length - 1) : install.log.length;
75428
+ const log = install.log.slice(Math.max(0, install.log.length - budget));
75429
+ for (const line of log) lines.push(color.textDim(truncateToWidth(line, width, "…")));
75430
+ lines.push(color.textDim(truncateToWidth(hint, width, "…")));
75431
+ return {
75432
+ lines,
75433
+ selectedLine: void 0
75434
+ };
75435
+ }
75436
+ };
75437
+ //#endregion
75438
+ //#region src/plugin-manager/extension-inventory.ts
75439
+ const ADVANCED_PREFIX = "advanced.";
75440
+ const UNSTABLE_PREFIX = "unstable.";
75441
+ /**
75442
+ * Aggregate live contributions by owner. Owners are reported exactly as the
75443
+ * runtime records them; no classification or package association happens
75444
+ * here.
75445
+ */
75446
+ function observeTuiExtensions(source) {
75447
+ const entryIds = source.ownerEntryIds?.();
75448
+ const byOwner = /* @__PURE__ */ new Map();
75449
+ for (const record of source.healthSnapshot()) {
75450
+ let entry = byOwner.get(record.owner);
75451
+ if (entry === void 0) {
75452
+ entry = {
75453
+ owner: record.owner,
75454
+ kinds: /* @__PURE__ */ new Set(),
75455
+ count: 0,
75456
+ failed: 0,
75457
+ active: 0,
75458
+ advanced: false,
75459
+ unstable: false
75460
+ };
75461
+ byOwner.set(record.owner, entry);
75462
+ }
75463
+ entry.kinds.add(record.extensionPoint);
75464
+ entry.count += 1;
75465
+ if (record.state === "failed") entry.failed += 1;
75466
+ else entry.active += 1;
75467
+ if (record.extensionPoint.startsWith(ADVANCED_PREFIX)) entry.advanced = true;
75468
+ if (record.extensionPoint.startsWith(UNSTABLE_PREFIX)) entry.unstable = true;
75469
+ }
75470
+ return [...byOwner.values()].sort((left, right) => left.owner < right.owner ? -1 : left.owner > right.owner ? 1 : 0).map((entry) => {
75471
+ const entryId = entryIds?.get(entry.owner);
75472
+ return Object.freeze({
75473
+ owner: entry.owner,
75474
+ ...entryId === void 0 ? {} : { entryId },
75475
+ contributionKinds: Object.freeze([...entry.kinds].sort()),
75476
+ contributionCount: entry.count,
75477
+ health: entry.failed === 0 ? "active" : entry.active === 0 ? "failed" : "mixed",
75478
+ usesAdvancedCapability: entry.advanced,
75479
+ usesUnstableCapability: entry.unstable
75480
+ });
75481
+ });
75482
+ }
75483
+ //#endregion
73602
75484
  //#region src/runtime/direct/host-file-direct.ts
73603
75485
  /**
73604
75486
  * The Direct Host-file adapter (M1.10) — the in-process implementation of
@@ -74863,6 +76745,7 @@ const SESSIONLESS_COMMANDS = /* @__PURE__ */ new Set([
74863
76745
  "rewind",
74864
76746
  "preset",
74865
76747
  "keybindings",
76748
+ "plugins",
74866
76749
  "statusline"
74867
76750
  ]);
74868
76751
  /**
@@ -74895,6 +76778,7 @@ const LOCAL_COMMANDS = /* @__PURE__ */ new Set([
74895
76778
  "model",
74896
76779
  "new",
74897
76780
  "preset",
76781
+ "plugins",
74898
76782
  "quit",
74899
76783
  "reload",
74900
76784
  "rename",
@@ -75308,8 +77192,8 @@ function packageVersion() {
75308
77192
  }
75309
77193
  /**
75310
77194
  * The welcome card's version line: the installed dsh version plus the
75311
- * bundle's own version (header-badge parity — `dsh-0.1.7-rc.1 ·
75312
- * tui-v0.4.8`). Without a resolvable dsh launcher it degrades to
77195
+ * bundle's own version (header-badge parity — `dsh-0.1.7-rc.2 ·
77196
+ * tui-v0.4.9`). Without a resolvable dsh launcher it degrades to
75313
77197
  * the bundle version alone.
75314
77198
  * @returns the combined version string.
75315
77199
  */
@@ -75470,10 +77354,12 @@ function dangerCommand(command) {
75470
77354
  return DANGER_PATTERNS.some((pattern) => pattern.test(command));
75471
77355
  }
75472
77356
  /**
75473
- * The dsh profile this process was launched with (the `--profile` flag),
75474
- * so the exit-time resume hint names the profile the TUI actually runs
75475
- * under. Falls back to `pi-tui` when the flag is absent (the TUI bundle
75476
- * cannot load without a profile, so this is defensive only).
77357
+ * The dsh profile named by the `--profile` flag in `argv`, in both spellings.
77358
+ * This is the ARGV-ONLY fallback: it cannot see the positional `dsh <name>`
77359
+ * launch form, because the launcher consumes that bare name by synthesizing
77360
+ * `['--profile', ...argv]` for its OWN commander parse and leaves
77361
+ * `process.argv` untouched. Prefer {@link hostRunningProfile}, which reads the
77362
+ * Host's profile identity instead of scraping the command line.
75477
77363
  * @param argv - the process argument vector.
75478
77364
  * @param fallback - the default profile.
75479
77365
  */
@@ -75486,11 +77372,30 @@ function runningProfile(argv = process.argv, fallback = "pi-tui") {
75486
77372
  return fallback;
75487
77373
  }
75488
77374
  /**
77375
+ * The profile this process actually runs — the official Host
77376
+ * `profileContext.name` when it is composed, else {@link runningProfile}'s
77377
+ * argv scrape.
77378
+ *
77379
+ * `profileContext.name` is the launcher's own profile identity, so it is
77380
+ * correct for EVERY launch form: `--profile <name>`, `--profile=<name>`, the
77381
+ * positional `dsh <name>`, and `--from-default-profile <name>`. The argv
77382
+ * scrape only knows the flag form, which is why it is the fallback for a
77383
+ * profile-less mount (tests, an embedded surface) rather than the primary
77384
+ * source.
77385
+ * @param ctx - the plugin context (or any structural `get`-only stand-in).
77386
+ * @param argv - the process argument vector for the fallback path.
77387
+ * @param fallback - the default profile for the fallback path.
77388
+ * @returns the running profile name.
77389
+ */
77390
+ function hostRunningProfile(ctx, argv = process.argv, fallback = "pi-tui") {
77391
+ return ctx.get("profileContext")?.name ?? runningProfile(argv, fallback);
77392
+ }
77393
+ /**
75489
77394
  * The interactive-quit resume hint (pi parity): `dsh --profile <p>
75490
77395
  * --session <id>`, printed after the terminal restores so the user can
75491
77396
  * re-enter the session later. Returns undefined when there is no session
75492
77397
  * to resume (deferred start never created one).
75493
- * @param profile - the running profile ({@link runningProfile}).
77398
+ * @param profile - the running profile ({@link hostRunningProfile}).
75494
77399
  * @param sessionId - the live session id.
75495
77400
  * @returns the resume command line, or undefined without a session.
75496
77401
  */
@@ -75778,12 +77683,22 @@ function apply(ctx, config) {
75778
77683
  stderrWarningWriter.write(message);
75779
77684
  } catch {}
75780
77685
  };
77686
+ const startupStatus = createStartupStatus(config.startupStatusOutput ?? {
77687
+ isTTY: process.stdout.isTTY === true,
77688
+ write: (text) => process.stdout.write(text)
77689
+ });
77690
+ lifecycleController.signal.addEventListener("abort", () => startupStatus.clear(), { once: true });
75781
77691
  let liveAgent;
75782
77692
  let liveHandle;
75783
77693
  let retireOwnedSessionRef;
75784
77694
  (async () => {
75785
77695
  startup.markSurfaceMounted?.();
75786
- await ctx.get("loader")?.await();
77696
+ startupStatus.show("Starting DSH…");
77697
+ try {
77698
+ await ctx.get("loader")?.await();
77699
+ } finally {
77700
+ startupStatus.clear();
77701
+ }
75787
77702
  if (lifecycleController.signal.aborted) {
75788
77703
  diag.dispose();
75789
77704
  return;
@@ -75867,10 +77782,34 @@ function apply(ctx, config) {
75867
77782
  else recall.abort();
75868
77783
  };
75869
77784
  let retirementPromise;
77785
+ const shutdownCancelledAgents = /* @__PURE__ */ new WeakSet();
77786
+ const cancelShutdownAgent = (agent) => {
77787
+ if (shutdownCancelledAgents.has(agent)) return;
77788
+ agent.cancel({ kind: "user" });
77789
+ shutdownCancelledAgents.add(agent);
77790
+ };
77791
+ const cancelRetiredOwner = (agent) => {
77792
+ if (lifecycleController.signal.aborted) cancelShutdownAgent(agent);
77793
+ else agent.cancel({ kind: "user" });
77794
+ };
77795
+ const preCancelOwnedSession = () => {
77796
+ const agent = liveAgent;
77797
+ if (agent === void 0) return;
77798
+ if (shutdownCancelledAgents.has(agent)) return;
77799
+ diag.info("retire cancel", { session: agent.session.id });
77800
+ try {
77801
+ cancelShutdownAgent(agent);
77802
+ } catch (error) {
77803
+ diag.error("retire pre-cancel failed", {
77804
+ session: agent.session.id,
77805
+ error: safeErrorMessage$1(error)
77806
+ });
77807
+ }
77808
+ };
75870
77809
  const retireOwnedSession = () => {
75871
77810
  if (retirementPromise !== void 0) return retirementPromise;
77811
+ preCancelOwnedSession();
75872
77812
  retirementPromise = (async () => {
75873
- if (transitionGate?.pending === true) liveAgent?.cancel({ kind: "user" });
75874
77813
  const retireParked = async (agent, handle) => retireDirectOwnedSession({
75875
77814
  cancel: () => agent.cancel({ kind: "user" }),
75876
77815
  whenIdle: () => agent.whenIdle(),
@@ -75889,8 +77828,9 @@ function apply(ctx, config) {
75889
77828
  diag.info("retire start", { session: agent.session.id });
75890
77829
  const report = await retireDirectOwnedSession({
75891
77830
  cancel: () => {
77831
+ if (shutdownCancelledAgents.has(agent)) return;
75892
77832
  diag.info("retire cancel", { session: agent.session.id });
75893
- agent.cancel({ kind: "user" });
77833
+ cancelShutdownAgent(agent);
75894
77834
  },
75895
77835
  whenIdle: async () => {
75896
77836
  diag.info("retire idle", { session: agent.session.id });
@@ -75956,7 +77896,7 @@ function apply(ctx, config) {
75956
77896
  retireOwnedSessionRef = retireOwnedSession;
75957
77897
  const whenIdleOrAbort = async (agent, signal) => {
75958
77898
  if (signal.aborted) {
75959
- agent.cancel({ kind: "user" });
77899
+ cancelShutdownAgent(agent);
75960
77900
  await agent.whenIdle();
75961
77901
  return true;
75962
77902
  }
@@ -75964,7 +77904,11 @@ function apply(ctx, config) {
75964
77904
  await new Promise((resolve, reject) => {
75965
77905
  const onAbort = () => {
75966
77906
  aborted = true;
75967
- agent.cancel({ kind: "user" });
77907
+ try {
77908
+ cancelShutdownAgent(agent);
77909
+ } catch (error) {
77910
+ reject(error);
77911
+ }
75968
77912
  };
75969
77913
  signal.addEventListener("abort", onAbort, { once: true });
75970
77914
  agent.whenIdle().then(() => {
@@ -76009,6 +77953,15 @@ function apply(ctx, config) {
76009
77953
  completionController.setMode(parseNotificationMode(tuiSettings?.get().notificationMode));
76010
77954
  completionController.setMethod(parseNotificationMethod(tuiSettings?.get().notificationMethod));
76011
77955
  const modelSelections = new DirectModelSelectionOwner(defaultModel);
77956
+ /**
77957
+ * rc.2 shares ONE per-Agent serialization window between `/model` selection
77958
+ * and image-bearing prompt admission (`serializeImageAdmission`): the image
77959
+ * capability check, the attachment admission and the delivery commit apply
77960
+ * in the same Agent-local order as a model switch, so an image prompt can
77961
+ * never be admitted under a model capability that a concurrent switch has
77962
+ * already replaced. A text-only prompt keeps the un-serialized path.
77963
+ */
77964
+ const withPromptAdmission = (agent, hasImage, operation) => hasImage ? modelSelections.serializeImageAdmission(agent, operation) : operation();
76012
77965
  const defaultIntent = new DefaultIntentTracker();
76013
77966
  const setDefaultIntent = (next) => {
76014
77967
  defaultIntent.set(next);
@@ -76061,7 +78014,8 @@ function apply(ctx, config) {
76061
78014
  flushSession: async (session) => {
76062
78015
  await sessions.flush(session);
76063
78016
  }
76064
- }), directPendingInputReader, directSessionWriter, new DirectSessionLifecycle(ctx, (presetId) => compose(presetId), directOwnerPool), new DirectInteractionPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectCatalogPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0, modelSelections, diag), new DirectConfigPort(ctx, tuiSettings, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectHostFilePort((sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectSessionArchive(ctx), new DirectHostCommandPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0));
78017
+ }), directPendingInputReader, directSessionWriter, new DirectSessionLifecycle(ctx, (presetId) => compose(presetId), directOwnerPool), new DirectInteractionPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectCatalogPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0, modelSelections, diag), new DirectConfigPort(ctx, tuiSettings, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectHostFilePort((sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectSessionArchive(ctx), new DirectHostCommandPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectPluginManagerPort(ctx));
78018
+ let disposePluginManagerController;
76065
78019
  const userFooterCustomItemsForSave = () => {
76066
78020
  const raw = backend.config.footerCustomItems.rawForPersistence();
76067
78021
  if (raw.kind === "unavailable") throw new Error("custom footer definitions unavailable; settings write aborted");
@@ -76113,11 +78067,6 @@ function apply(ctx, config) {
76113
78067
  }
76114
78068
  };
76115
78069
  let resumeFailure;
76116
- const startupStatus = createStartupStatus(config.startupStatusOutput ?? {
76117
- isTTY: process.stdout.isTTY === true,
76118
- write: (text) => process.stdout.write(text)
76119
- });
76120
- lifecycleController.signal.addEventListener("abort", () => startupStatus.clear(), { once: true });
76121
78070
  let handle;
76122
78071
  let resumeResolved = false;
76123
78072
  if (sessionId !== void 0) {
@@ -76304,7 +78253,7 @@ function apply(ctx, config) {
76304
78253
  const retired = [];
76305
78254
  if (oldHandle !== void 0 && oldAgent !== void 0) {
76306
78255
  const report = await retireDirectOwnedSession({
76307
- cancel: () => oldAgent.cancel({ kind: "user" }),
78256
+ cancel: () => cancelRetiredOwner(oldAgent),
76308
78257
  whenIdle: () => oldAgent.whenIdle(),
76309
78258
  drainDescendants: async () => {
76310
78259
  await ctx.get("subagents")?.drainContinuableDescendants?.([oldAgent]);
@@ -76734,7 +78683,7 @@ function apply(ctx, config) {
76734
78683
  if (pin !== void 0) pin.state.retirementOwnsRelease = true;
76735
78684
  const retirement = retireSourceOwnerAfterSettlement(oldAgent.session.id, async () => {
76736
78685
  const report = await retireDirectOwnedSession({
76737
- cancel: () => oldAgent.cancel({ kind: "user" }),
78686
+ cancel: () => cancelRetiredOwner(oldAgent),
76738
78687
  whenIdle: () => oldAgent.whenIdle(),
76739
78688
  drainDescendants: async () => {
76740
78689
  await ctx.get("subagents")?.drainContinuableDescendants?.([oldAgent]);
@@ -76914,6 +78863,7 @@ function apply(ctx, config) {
76914
78863
  draftImages.clear();
76915
78864
  draftFiles.clear();
76916
78865
  catalogCoordinator?.dispose();
78866
+ disposePluginManagerController?.();
76917
78867
  cancelDeferredContextMeasure?.();
76918
78868
  cancelDeferredContextMeasure = void 0;
76919
78869
  footerCommandUnsubscribe?.();
@@ -76929,6 +78879,7 @@ function apply(ctx, config) {
76929
78879
  shellTempFiles.clear();
76930
78880
  jobsEventsDispose?.();
76931
78881
  jobsEventsDispose = void 0;
78882
+ activeJobViewerClose?.();
76932
78883
  activeJobViewerClose = void 0;
76933
78884
  activeTaskBrowser = void 0;
76934
78885
  activeTaskBrowserToken = void 0;
@@ -76943,9 +78894,10 @@ function apply(ctx, config) {
76943
78894
  const { requestExit } = createExitController({
76944
78895
  diag,
76945
78896
  cleanup: disposeSurface,
78897
+ prepareRetirement: preCancelOwnedSession,
76946
78898
  hint: (message) => process.stdout.write(`\n${message}\n`),
76947
78899
  resumeHint: () => {
76948
- const resume = resumeCommand(runningProfile(), liveAgent?.session.id ?? "");
78900
+ const resume = resumeCommand(hostRunningProfile(ctx), liveAgent?.session.id ?? "");
76949
78901
  return resume === void 0 ? void 0 : `${color.textDim("To resume this session:")} ${resume}`;
76950
78902
  },
76951
78903
  exit
@@ -78357,7 +80309,7 @@ function apply(ctx, config) {
78357
80309
  reserve: () => fallbackPin,
78358
80310
  run: async () => {
78359
80311
  try {
78360
- await operationBarrier.runWriter(agent.session.id, async () => {
80312
+ await operationBarrier.runWriter(agent.session.id, () => withPromptAdmission(agent, draftHasImages(text, draftImages), async () => {
78361
80313
  if (!localEchoInstalled) {
78362
80314
  beginLocalSubmission(submitRequestId, text, submissionPlacement("queue", agent.status === "running"), agent.session.id, generation, submitAckToken);
78363
80315
  localEchoInstalled = true;
@@ -78397,7 +80349,7 @@ function apply(ctx, config) {
78397
80349
  throw new Error(failure);
78398
80350
  }
78399
80351
  consumeDraftAttachments(text, draftImages, draftFiles);
78400
- });
80352
+ }));
78401
80353
  } catch (error) {
78402
80354
  if (cleanedUp) {
78403
80355
  fallbackPin();
@@ -78511,7 +80463,7 @@ function apply(ctx, config) {
78511
80463
  return;
78512
80464
  }
78513
80465
  try {
78514
- await operationBarrier.runWriter(agent.session.id, async () => {
80466
+ await operationBarrier.runWriter(agent.session.id, () => withPromptAdmission(agent, draftHasImages(text, draftImages), async () => {
78515
80467
  if (!localEchoInstalled) {
78516
80468
  beginLocalSubmission(submitRequestId, text, submissionPlacement("queue", agent.status === "running"), agent.session.id, generation, submitAckToken);
78517
80469
  localEchoInstalled = true;
@@ -78551,7 +80503,7 @@ function apply(ctx, config) {
78551
80503
  throw new Error(failure);
78552
80504
  }
78553
80505
  consumeDraftAttachments(text, draftImages, draftFiles);
78554
- });
80506
+ }));
78555
80507
  } catch (error) {
78556
80508
  if (cleanedUp) return;
78557
80509
  if (error instanceof TransitionInProgressError) {
@@ -78758,53 +80710,60 @@ function apply(ctx, config) {
78758
80710
  steerDelivery = running ? "steer" : "queue";
78759
80711
  if (draftHasPayload) beginLocalSubmission(steerRequestId, text, submissionPlacement(steerDelivery, running), agentForSteer.session.id, generationForSteer, steerAckToken);
78760
80712
  }
78761
- const prepared = await prepareUserMessage(text, draftImages, submitDeps, { requestId: steerRequestId });
78762
- if (cleanedUp) return;
78763
- if (!sessionUnchanged({
78764
- agent: agentForSteer,
78765
- generation: generationForSteer
78766
- }, liveAgent, sessionGeneration)) {
78767
- const merged = mergeDraft(app.getDraft(), text);
78768
- app.setEditorText(merged);
78769
- settleLocalSubmission(steerRequestId);
78770
- settleLocalSubmitAck("steer stale", {
78771
- token: steerAckToken,
78772
- terminal: true
78773
- });
78774
- app.notify(merged === text ? "the session changed while sending — try again" : "the draft changed while sending — review it before submitting again (the earlier text was preserved below)", "error");
78775
- return;
78776
- }
78777
- submitLatencyTracker.mark(agentForSteer.session.id, "dispatch");
78778
- const outcome = await steerAll({
78779
- currentAgent: () => cleanedUp ? void 0 : agentForSteer,
78780
- currentGeneration: () => generationForSteer,
78781
- notify: (message, kind) => {
78782
- if (cleanedUp) return;
78783
- app.notify(message, kind);
78784
- },
78785
- restoreDraft: (draft) => {
78786
- if (cleanedUp) return false;
78787
- const merged = mergeDraft(app.getDraft(), draft);
80713
+ const admission = await withPromptAdmission(agentForSteer, draftHasImages(text, draftImages), async () => {
80714
+ const prepared = await prepareUserMessage(text, draftImages, submitDeps, { requestId: steerRequestId });
80715
+ if (cleanedUp) return { kind: "stale" };
80716
+ if (!sessionUnchanged({
80717
+ agent: agentForSteer,
80718
+ generation: generationForSteer
80719
+ }, liveAgent, sessionGeneration)) {
80720
+ const merged = mergeDraft(app.getDraft(), text);
78788
80721
  app.setEditorText(merged);
78789
- steerRestored = true;
78790
- return merged === draft;
78791
- },
78792
- fence: () => transitionGate.busy || cleanedUp,
78793
- barrier: operationBarrier,
78794
- fenceNotice: () => "a session transition is in progress — try again in a moment",
78795
- createDraft: () => prepared,
78796
- staleNotice: () => "the queue or session changed while sending — try again",
78797
- mergedNotice: () => "the draft changed while sending — review it before submitting again (the earlier text was preserved below)",
78798
- pendingInputReader: backend.pendingInputReader,
78799
- writer: backend.sessionWriter
78800
- }, text, onlyDraft ? {
78801
- onlyDraft: true,
78802
- draftHasPayload,
78803
- draftDelivery: steerDelivery
78804
- } : {
78805
- draftHasPayload,
78806
- draftDelivery: steerDelivery
80722
+ settleLocalSubmission(steerRequestId);
80723
+ settleLocalSubmitAck("steer stale", {
80724
+ token: steerAckToken,
80725
+ terminal: true
80726
+ });
80727
+ app.notify(merged === text ? "the session changed while sending — try again" : "the draft changed while sending — review it before submitting again (the earlier text was preserved below)", "error");
80728
+ return { kind: "stale" };
80729
+ }
80730
+ submitLatencyTracker.mark(agentForSteer.session.id, "dispatch");
80731
+ return {
80732
+ kind: "delivered",
80733
+ outcome: await steerAll({
80734
+ currentAgent: () => cleanedUp ? void 0 : agentForSteer,
80735
+ currentGeneration: () => generationForSteer,
80736
+ notify: (message, kind) => {
80737
+ if (cleanedUp) return;
80738
+ app.notify(message, kind);
80739
+ },
80740
+ restoreDraft: (draft) => {
80741
+ if (cleanedUp) return false;
80742
+ const merged = mergeDraft(app.getDraft(), draft);
80743
+ app.setEditorText(merged);
80744
+ steerRestored = true;
80745
+ return merged === draft;
80746
+ },
80747
+ fence: () => transitionGate.busy || cleanedUp,
80748
+ barrier: operationBarrier,
80749
+ fenceNotice: () => "a session transition is in progress — try again in a moment",
80750
+ createDraft: () => prepared,
80751
+ staleNotice: () => "the queue or session changed while sending — try again",
80752
+ mergedNotice: () => "the draft changed while sending — review it before submitting again (the earlier text was preserved below)",
80753
+ pendingInputReader: backend.pendingInputReader,
80754
+ writer: backend.sessionWriter
80755
+ }, text, onlyDraft ? {
80756
+ onlyDraft: true,
80757
+ draftHasPayload,
80758
+ draftDelivery: steerDelivery
80759
+ } : {
80760
+ draftHasPayload,
80761
+ draftDelivery: steerDelivery
80762
+ })
80763
+ };
78807
80764
  });
80765
+ if (admission.kind === "stale") return;
80766
+ const outcome = admission.outcome;
78808
80767
  if (cleanedUp) return;
78809
80768
  if (outcome === "ok") consumeDraftAttachments(text, draftImages, draftFiles);
78810
80769
  if (outcome !== "ok") {
@@ -79109,6 +81068,38 @@ function apply(ctx, config) {
79109
81068
  };
79110
81069
  if (lifecycleController.signal.aborted) return;
79111
81070
  startupStatus.clear();
81071
+ const pluginManagerHosts = new PluginManagerHostRegistry();
81072
+ const pluginManagerController = new PluginManagerController(backend.pluginManager, {
81073
+ requestRender: () => {
81074
+ if (pluginManagerHosts.isOpen()) app.requestRender();
81075
+ },
81076
+ requestClose: () => pluginManagerHosts.closeActive(),
81077
+ notify: (message, kind) => app.notify(message, kind),
81078
+ isOpen: () => pluginManagerHosts.isOpen(),
81079
+ diag
81080
+ }, { observations: () => extensionService === void 0 ? [] : observeTuiExtensions({
81081
+ healthSnapshot: () => extensionService._ledger().healthSnapshot(),
81082
+ ownerEntryIds: () => extensionService._ownerEntryIds()
81083
+ }) });
81084
+ disposePluginManagerController = () => pluginManagerController.dispose();
81085
+ const jobObservation = new DirectJobObservationPort(ctx, diag);
81086
+ const openPluginManager = () => {
81087
+ if (pluginManagerHosts.isOpen()) return;
81088
+ let close = () => {};
81089
+ const claim = pluginManagerHosts.claim(() => close());
81090
+ const panel = new PluginManagerPanel(pluginManagerController, () => app.requestRender(), { onDispose: () => claim.releaseExternally() });
81091
+ close = app.openPluginManagerPanel(panel, () => claim.releaseExternally());
81092
+ pluginManagerController.open("direct-command");
81093
+ };
81094
+ /** The `/settings → Plugins` entry: the SAME panel/controller hosted as a
81095
+ * lazy SettingsList submenu; `done` returns to the Settings list. */
81096
+ const createPluginManagerSubmenu = (done) => {
81097
+ let claim;
81098
+ const panel = new PluginManagerPanel(pluginManagerController, () => app.requestRender(), { onDispose: () => claim?.releaseExternally() });
81099
+ claim = pluginManagerHosts.claim(() => claim?.closeNormally(), done);
81100
+ pluginManagerController.open("settings-submenu");
81101
+ return panel;
81102
+ };
79112
81103
  app = startProcessTui({
79113
81104
  onSubmit: (text, request) => dispatchUserInput(text, request),
79114
81105
  isImageDraft: () => draftHasImages(app.getDraft(), draftImages),
@@ -80361,11 +82352,12 @@ function apply(ctx, config) {
80361
82352
  refreshAgents();
80362
82353
  }
80363
82354
  /**
80364
- * Open one job from the task browser: a bash job shows a STATUS viewer
80365
- * (never the output — the job's single read cursor belongs to the agent's
80366
- * job_output; consuming it from the UI would leave the model an
80367
- * incomplete result and could swallow the completion notice); a subagent
80368
- * job shows the status viewer with a /tasks hint. The job record
82355
+ * Open one job from the task browser: an ordinary Job opens the detail
82356
+ * viewer, which shows a NON-CONSUMING live output preview through the
82357
+ * official JobController.follow() stream (never `jobs.read()`), so it can
82358
+ * never leave the model an incomplete `job_output` result or swallow the
82359
+ * completion notice; a subagent job whose stable child session id is
82360
+ * unknown shows the same Job detail with a /tasks hint. The job record
80369
82361
  * carries no child session id, so label/order/time heuristics cannot
80370
82362
  * distinguish a background child from a same-label foreground one-shot;
80371
82363
  * the task browser therefore never opens a transcript by guess.
@@ -80405,26 +82397,44 @@ function apply(ctx, config) {
80405
82397
  return "keep-open";
80406
82398
  };
80407
82399
  /**
80408
- * Status-only viewer for one job (never touches the read cursor). The
80409
- * subagent variant appends the /tasks hint because a transcript
80410
- * cannot always be matched; the bash variant is pure status.
82400
+ * Selected-Job detail viewer. It opens one official non-consuming
82401
+ * observation stream for exactly this Job and repaints the latest local
82402
+ * snapshot on the viewer's timer (the tick never reads Host output). The
82403
+ * subagent variant appends the /tasks hint because a transcript cannot
82404
+ * always be matched. If the jobController service is absent the detail
82405
+ * degrades to the status-only view with an explicit note.
80411
82406
  */
80412
82407
  const openJobStatusViewer = (jobId, title, snapshot) => {
80413
82408
  activeJobViewerClose?.();
80414
82409
  if (liveAgent === void 0) return;
80415
82410
  const ownerSessionId = liveAgent.session.id;
80416
- activeJobViewerClose = app.openOutputViewer({
80417
- title,
80418
- initial: snapshot.kind === "subagent" ? subagentJobViewHint(snapshot.status, snapshot.detail) : jobStatusHint(snapshot.status, snapshot.detail),
80419
- refresh: () => {
80420
- if (jobs === void 0) return "";
82411
+ const fallbackText = snapshot.kind === "subagent" ? subagentJobViewHint(snapshot.status, snapshot.detail) : jobStatusHint(snapshot.status, snapshot.detail);
82412
+ let observed;
82413
+ let observationError;
82414
+ let closeObserver = () => {};
82415
+ try {
82416
+ closeObserver = jobObservation.open(ownerSessionId, jobId, (next) => {
82417
+ observed = next;
82418
+ });
82419
+ } catch (error) {
82420
+ observationError = safeErrorMessage$1(error);
82421
+ }
82422
+ const refreshBody = () => {
82423
+ if (observed !== void 0) return formatJobObservation(observed);
82424
+ const current = jobs === void 0 ? void 0 : (() => {
80421
82425
  try {
80422
- const current = jobs.get(jobId, ownerSessionId);
80423
- return current.kind === "subagent" ? subagentJobViewHint(current.status, current.detail) : jobStatusHint(current.status, current.detail);
82426
+ return jobs.get(jobId, ownerSessionId);
80424
82427
  } catch {
80425
- return "";
82428
+ return;
80426
82429
  }
80427
- },
82430
+ })();
82431
+ const base = current === void 0 ? fallbackText : current.kind === "subagent" ? subagentJobViewHint(current.status, current.detail) : jobStatusHint(current.status, current.detail);
82432
+ return observationError === void 0 ? base : `${base}\nlive observation unavailable: ${observationError}`;
82433
+ };
82434
+ activeJobViewerClose = app.openOutputViewer({
82435
+ title,
82436
+ initial: fallbackText,
82437
+ refresh: refreshBody,
80428
82438
  onStop: () => {
80429
82439
  if (jobs === void 0) return;
80430
82440
  try {
@@ -80442,14 +82452,29 @@ function apply(ctx, config) {
80442
82452
  },
80443
82453
  closeHint: "back",
80444
82454
  onClose: () => {
82455
+ closeObserver();
80445
82456
  activeJobViewerClose = void 0;
80446
82457
  refreshTasks();
80447
82458
  }
80448
82459
  });
80449
82460
  };
82461
+ /**
82462
+ * The Job detail body from the detached observation (never a Host read).
82463
+ * The official controller provides a best-effort retained preview, not an
82464
+ * archival terminal log — say so.
82465
+ */
82466
+ const formatJobObservation = (observed) => {
82467
+ const lines = [observed.status];
82468
+ if (observed.progress !== void 0) lines.push(`progress: ${observed.progress}`);
82469
+ if (observed.detail !== void 0) lines.push(`detail: ${observed.detail}`);
82470
+ if (observed.gapBefore) lines.push("note: earlier output was evicted before this retained preview");
82471
+ if (observed.error !== void 0) lines.push(`follow error: ${observed.error}`);
82472
+ lines.push("", "best-effort retained output preview (not a complete transcript):", "", observed.text);
82473
+ return lines.join("\n");
82474
+ };
80450
82475
  /** One-line viewer hint for a job state (never touches the read cursor). */
80451
82476
  const jobStatusHint = (status, detail) => {
80452
- return `${status}${status === "running" || status === "stopping" ? " — output is delivered to the agent via job_output; viewing never consumes the job’s read cursor" : ` — final output: ask the agent to run job_output in the conversation${detail === void 0 ? "" : ` (${detail})`}`}`;
82477
+ return `${status}${status === "running" || status === "stopping" ? " — opening the non-consuming retained-output stream…" : ` — final output is delivered to the agent via job_output${detail === void 0 ? "" : ` (${detail})`}`}`;
80453
82478
  };
80454
82479
  refreshPendingInput();
80455
82480
  /**
@@ -80891,6 +82916,8 @@ function apply(ctx, config) {
80891
82916
  openJobView,
80892
82917
  openTasksBrowser: () => openTasksBrowser("full"),
80893
82918
  openRewindPicker,
82919
+ openPluginManager,
82920
+ createPluginManagerSubmenu,
80894
82921
  sessionTransitionPending: () => transitionGate.busy,
80895
82922
  withSessionTransition: (task) => transitionGate.run(() => operationBarrier.runTransition(async () => {
80896
82923
  try {
@@ -80900,6 +82927,7 @@ function apply(ctx, config) {
80900
82927
  }
80901
82928
  })),
80902
82929
  withSessionWriter: (sessionId, task) => operationBarrier.runWriter(sessionId, async () => task()),
82930
+ withPromptAdmission: (agent, line, task) => withPromptAdmission(agent, draftHasImages(line, draftImages), async () => task()),
80903
82931
  enterView,
80904
82932
  requestExit,
80905
82933
  exit
@@ -81211,6 +83239,9 @@ function apply(ctx, config) {
81211
83239
  });
81212
83240
  })().catch(async (error) => {
81213
83241
  const message = safeErrorMessage$1(error);
83242
+ try {
83243
+ startupStatus.clear();
83244
+ } catch {}
81214
83245
  try {
81215
83246
  ctx.logger.error(`tui-runner: ${message}`);
81216
83247
  } catch {}
@@ -81248,4 +83279,4 @@ function apply(ctx, config) {
81248
83279
  });
81249
83280
  }
81250
83281
  //#endregion
81251
- export { shouldConsumeAdvertisedMiss as A, PI_TUI_EXTENSIONS_SERVICE as B, normalizeSkillInvocation as C, resumeCommand as D, resolveSubmitDelivery as E, viewerActionCapability as F, describeKey as H, dshVersion as I, apply$1 as L, subagentJobViewHint as M, taskRowSelectionDisposition as N, runningProfile as O, teardownViewerForSessionSwap as P, inject$1 as R, name as S, resolveInitialCatalog as T, API_VERSION as V, interruptAgent as _, apply as a, isPlainExitPrompt as b, commandRejectsImages as c, contextRefreshKind as d, createViewerOpenToken as f, inject as g, foldQueueRows as h, SESSIONLESS_COMMANDS as i, subagentJobTranscriptId as j, settleCompactionSurface as k, compactingFromLog as l, foldCompactionEvent as m, HOST_COMMAND_CATALOG as n, busyAfterTurnBoundary as o, dangerCommand as p, LOCAL_COMMANDS as r, commandIsLocalForAttachments as s, Config as t, composeAgent as u, isBareCommandLine as v, recordedPreset as w, matchPendingSubagentCall as x, isLocalCommandLine as y, name$1 as z };
83282
+ export { settleCompactionSurface as A, name$1 as B, name as C, resolveSubmitDelivery as D, resolveInitialCatalog as E, teardownViewerForSessionSwap as F, API_VERSION as H, viewerActionCapability as I, dshVersion as L, subagentJobTranscriptId as M, subagentJobViewHint as N, resumeCommand as O, taskRowSelectionDisposition as P, apply$1 as R, matchPendingSubagentCall as S, recordedPreset as T, describeKey as U, PI_TUI_EXTENSIONS_SERVICE as V, inject as _, apply as a, isLocalCommandLine as b, commandRejectsImages as c, contextRefreshKind as d, createViewerOpenToken as f, hostRunningProfile as g, foldQueueRows as h, SESSIONLESS_COMMANDS as i, shouldConsumeAdvertisedMiss as j, runningProfile as k, compactingFromLog as l, foldCompactionEvent as m, HOST_COMMAND_CATALOG as n, busyAfterTurnBoundary as o, dangerCommand as p, LOCAL_COMMANDS as r, commandIsLocalForAttachments as s, Config as t, composeAgent as u, interruptAgent as v, normalizeSkillInvocation as w, isPlainExitPrompt as x, isBareCommandLine as y, inject$1 as z };