@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.
- package/README.en.md +32 -10
- package/README.md +13 -8
- package/cordis.patch.yml +49 -1
- package/dist/builtins.mjs +2 -2
- package/dist/extensions.mjs +1 -1
- package/dist/index.d.mts +29 -6
- package/dist/index.mjs +2 -2
- package/dist/{src-R8SVGB-L.mjs → src-BAwPimrH.mjs} +2394 -363
- package/dist/{startup-Cd07dCyh.mjs → startup-xTIOW90U.mjs} +10 -5
- package/dist/startup.d.mts +2 -2
- package/dist/startup.mjs +1 -1
- package/generated/dsh-presets/README.md +1 -1
- package/package.json +116 -113
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as TUI_STARTUP_SERVICE } from "./startup-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
54869
|
-
*
|
|
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
|
|
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.
|
|
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)].
|
|
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
|
-
|
|
67656
|
-
|
|
67657
|
-
|
|
67658
|
-
|
|
67659
|
-
|
|
67660
|
-
|
|
67661
|
-
|
|
67662
|
-
|
|
67663
|
-
|
|
67664
|
-
|
|
67665
|
-
|
|
67666
|
-
|
|
67667
|
-
|
|
67668
|
-
|
|
67669
|
-
|
|
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 (
|
|
67683
|
-
|
|
67684
|
-
if (
|
|
67685
|
-
|
|
67686
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
70348
|
-
|
|
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,
|
|
70670
|
-
*
|
|
70671
|
-
* testable without a Cordis
|
|
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
|
|
70679
|
-
*
|
|
70680
|
-
*
|
|
70681
|
-
*
|
|
70682
|
-
*
|
|
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
|
|
70686
|
-
* individually protected, so no throw can skip a later step,
|
|
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
|
|
70702
|
-
* one is in flight (or after it
|
|
70703
|
-
* command plus a key cannot
|
|
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
|
|
70706
|
-
* protected, so no throw can skip a later step or leak a
|
|
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
|
-
|
|
72375
|
-
|
|
72376
|
-
|
|
72377
|
-
|
|
72378
|
-
|
|
72379
|
-
|
|
72380
|
-
|
|
72381
|
-
|
|
72382
|
-
|
|
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
|
-
|
|
72405
|
-
|
|
72406
|
-
|
|
72407
|
-
|
|
72408
|
-
|
|
72409
|
-
|
|
72410
|
-
|
|
72411
|
-
|
|
72412
|
-
|
|
72413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72486
|
-
|
|
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.
|
|
75312
|
-
* tui-v0.4.
|
|
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
|
|
75474
|
-
*
|
|
75475
|
-
*
|
|
75476
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
78762
|
-
|
|
78763
|
-
|
|
78764
|
-
|
|
78765
|
-
|
|
78766
|
-
|
|
78767
|
-
|
|
78768
|
-
|
|
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
|
-
|
|
78790
|
-
|
|
78791
|
-
|
|
78792
|
-
|
|
78793
|
-
|
|
78794
|
-
|
|
78795
|
-
|
|
78796
|
-
|
|
78797
|
-
|
|
78798
|
-
|
|
78799
|
-
|
|
78800
|
-
|
|
78801
|
-
|
|
78802
|
-
|
|
78803
|
-
|
|
78804
|
-
|
|
78805
|
-
|
|
78806
|
-
|
|
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:
|
|
80365
|
-
*
|
|
80366
|
-
*
|
|
80367
|
-
*
|
|
80368
|
-
*
|
|
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
|
-
*
|
|
80409
|
-
*
|
|
80410
|
-
*
|
|
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
|
-
|
|
80417
|
-
|
|
80418
|
-
|
|
80419
|
-
|
|
80420
|
-
|
|
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
|
-
|
|
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" ? " —
|
|
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 {
|
|
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 };
|