@aiwayds/dsh-tui-pi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +324 -0
  3. package/bin/dsh-tui-pi +5 -0
  4. package/cordis.patch.yml +9 -0
  5. package/lib/append-system.d.ts +66 -0
  6. package/lib/append-system.js +161 -0
  7. package/lib/append-system.js.map +1 -0
  8. package/lib/commands.d.ts +53 -0
  9. package/lib/commands.js +167 -0
  10. package/lib/commands.js.map +1 -0
  11. package/lib/dsh-events.d.ts +106 -0
  12. package/lib/dsh-events.js +30 -0
  13. package/lib/dsh-events.js.map +1 -0
  14. package/lib/editor.d.ts +28 -0
  15. package/lib/editor.js +70 -0
  16. package/lib/editor.js.map +1 -0
  17. package/lib/footer.d.ts +36 -0
  18. package/lib/footer.js +112 -0
  19. package/lib/footer.js.map +1 -0
  20. package/lib/frame.d.ts +35 -0
  21. package/lib/frame.js +75 -0
  22. package/lib/frame.js.map +1 -0
  23. package/lib/git.d.ts +17 -0
  24. package/lib/git.js +51 -0
  25. package/lib/git.js.map +1 -0
  26. package/lib/index.d.ts +15 -0
  27. package/lib/index.js +781 -0
  28. package/lib/index.js.map +1 -0
  29. package/lib/instructions.d.ts +29 -0
  30. package/lib/instructions.js +67 -0
  31. package/lib/instructions.js.map +1 -0
  32. package/lib/live-widgets.d.ts +85 -0
  33. package/lib/live-widgets.js +218 -0
  34. package/lib/live-widgets.js.map +1 -0
  35. package/lib/messages.d.ts +277 -0
  36. package/lib/messages.js +734 -0
  37. package/lib/messages.js.map +1 -0
  38. package/lib/permission.d.ts +27 -0
  39. package/lib/permission.js +48 -0
  40. package/lib/permission.js.map +1 -0
  41. package/lib/provider-catalog.d.ts +114 -0
  42. package/lib/provider-catalog.js +124 -0
  43. package/lib/provider-catalog.js.map +1 -0
  44. package/lib/quotes.d.ts +28 -0
  45. package/lib/quotes.js +144 -0
  46. package/lib/quotes.js.map +1 -0
  47. package/lib/reload.d.ts +23 -0
  48. package/lib/reload.js +171 -0
  49. package/lib/reload.js.map +1 -0
  50. package/lib/selectors.d.ts +48 -0
  51. package/lib/selectors.js +261 -0
  52. package/lib/selectors.js.map +1 -0
  53. package/lib/session.d.ts +157 -0
  54. package/lib/session.js +555 -0
  55. package/lib/session.js.map +1 -0
  56. package/lib/sessions.d.ts +73 -0
  57. package/lib/sessions.js +253 -0
  58. package/lib/sessions.js.map +1 -0
  59. package/lib/settings.d.ts +180 -0
  60. package/lib/settings.js +1328 -0
  61. package/lib/settings.js.map +1 -0
  62. package/lib/text.d.ts +22 -0
  63. package/lib/text.js +45 -0
  64. package/lib/text.js.map +1 -0
  65. package/lib/theme/index.d.ts +79 -0
  66. package/lib/theme/index.js +121 -0
  67. package/lib/theme/index.js.map +1 -0
  68. package/lib/theme/palette.d.ts +56 -0
  69. package/lib/theme/palette.js +154 -0
  70. package/lib/theme/palette.js.map +1 -0
  71. package/lib/theme-settings.d.ts +68 -0
  72. package/lib/theme-settings.js +223 -0
  73. package/lib/theme-settings.js.map +1 -0
  74. package/lib/tui.d.ts +70 -0
  75. package/lib/tui.js +206 -0
  76. package/lib/tui.js.map +1 -0
  77. package/lib/welcome.d.ts +91 -0
  78. package/lib/welcome.js +281 -0
  79. package/lib/welcome.js.map +1 -0
  80. package/package.json +50 -0
  81. package/patches/@earendil-works__pi-tui.patch +72 -0
  82. package/pnpm-workspace.yaml +2 -0
  83. package/templates/APPEND_SYSTEM.md +34 -0
package/lib/reload.js ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Manual hot-reload for the dsh-tui-pi plugin.
3
+ *
4
+ * Mirrors @deepseek-ai/cordis-plugin-hmr's partial reload: evict the plugin
5
+ * entry and its user-code dependency closure from the module caches, re-import
6
+ * the entry fresh, then swap the plugin runtime in the registry so a new fiber
7
+ * re-runs `apply` with the new code. A `/reload` after editing `src/` and
8
+ * running `pnpm build` picks the change up without restarting dsh.
9
+ *
10
+ * The swap disposes the old fiber — the TUI, its timers, and the live agent
11
+ * bridge are torn down (the session log persists and can be rejoined with
12
+ * /resume). Failures before the swap leave the old TUI untouched; failures
13
+ * during the swap roll the caches back and restart the previous code.
14
+ */
15
+ import { createRequire } from 'node:module';
16
+ import { realpathSync } from 'node:fs';
17
+ import { fileURLToPath, pathToFileURL } from 'node:url';
18
+ const require = createRequire(import.meta.url);
19
+ function messageOf(error) {
20
+ return error instanceof Error ? error.message : String(error);
21
+ }
22
+ /** Skip node: builtins and anything under node_modules (shared, stable). */
23
+ function isUserCode(url) {
24
+ return !url.startsWith('node:') && !url.includes('/node_modules/');
25
+ }
26
+ /** Recursively collect the user-code dependency closure of a module job. */
27
+ async function collectDependencies(job, out) {
28
+ if (out.has(job.url) || !isUserCode(job.url))
29
+ return;
30
+ out.add(job.url);
31
+ for (const child of await job.linked)
32
+ await collectDependencies(child, out);
33
+ }
34
+ /**
35
+ * The entry URL the loader recorded equals `import.meta.url` of the entry
36
+ * module, modulo symlink resolution — try both against the module cache.
37
+ */
38
+ function resolveEntryUrl(fallbackUrl, internal) {
39
+ const candidates = [fallbackUrl];
40
+ try {
41
+ candidates.push(pathToFileURL(realpathSync(fileURLToPath(fallbackUrl))).href);
42
+ }
43
+ catch { /* non-file URL → fallbackUrl stands */ }
44
+ for (const url of candidates) {
45
+ if (Map.prototype.has.call(internal.loadCache, url))
46
+ return url;
47
+ }
48
+ return candidates[0];
49
+ }
50
+ // ------------------------------------------------------------------- reload --
51
+ let reloading;
52
+ /**
53
+ * Reload the dsh-tui-pi plugin from the current source.
54
+ *
55
+ * @param ctx — the plugin context (the fiber being reloaded).
56
+ * @param entryUrl — `import.meta.url` of the plugin entry module.
57
+ * @returns a human-readable outcome; the plugin fiber is swapped on success.
58
+ */
59
+ export function reloadPlugin(ctx, entryUrl) {
60
+ // Re-entrancy guard: a second /reload while one is in flight joins it.
61
+ reloading ??= performReload(ctx, entryUrl).finally(() => { reloading = undefined; });
62
+ return reloading;
63
+ }
64
+ async function performReload(ctx, fallbackUrl) {
65
+ const loader = ctx.get('loader');
66
+ const internal = loader?.internal;
67
+ if (loader === undefined)
68
+ return 'Reload unavailable: the loader service is not mounted.';
69
+ if (internal === undefined)
70
+ return 'Reload unavailable: the module loader is not reachable.';
71
+ const runtime = ctx.fiber.runtime;
72
+ if (runtime === null || runtime === undefined)
73
+ return 'Reload unavailable: no plugin runtime.';
74
+ const url = resolveEntryUrl(fallbackUrl, internal);
75
+ const job = internal.loadCache.get(url);
76
+ if (job === undefined)
77
+ return `Reload failed: the plugin entry is not loaded (${url}).`;
78
+ // Dependency closure of the entry: our lib/*.js only. node: builtins and
79
+ // node_modules are shared with the rest of dsh and stay cached.
80
+ const files = new Set();
81
+ try {
82
+ await collectDependencies(job, files);
83
+ }
84
+ catch (error) {
85
+ return `Reload failed: cannot walk the module graph (${messageOf(error)}).`;
86
+ }
87
+ // Evict ESM loadCache + CJS Module._cache so the re-import re-reads source.
88
+ // Map.prototype methods bypass Node 24+'s typed LoadCache overrides (its
89
+ // `.delete` only nulls a type slot; a raw delete removes the entry).
90
+ const esmBackup = new Map();
91
+ const cjsBackup = new Map();
92
+ for (const filename of files) {
93
+ esmBackup.set(filename, Map.prototype.get.call(internal.loadCache, filename));
94
+ Map.prototype.delete.call(internal.loadCache, filename);
95
+ try {
96
+ const filepath = fileURLToPath(filename);
97
+ const cached = require.cache[filepath];
98
+ if (cached) {
99
+ cjsBackup.set(filepath, cached);
100
+ delete require.cache[filepath];
101
+ }
102
+ }
103
+ catch { /* not a file URL */ }
104
+ }
105
+ const restoreCaches = () => {
106
+ for (const [filename, entry] of esmBackup) {
107
+ if (entry === undefined)
108
+ Map.prototype.delete.call(internal.loadCache, filename);
109
+ else
110
+ Map.prototype.set.call(internal.loadCache, filename, entry);
111
+ }
112
+ for (const [filepath, module] of cjsBackup)
113
+ require.cache[filepath] = module;
114
+ };
115
+ // Re-import the entry fresh; on failure restore the caches and keep the old
116
+ // TUI untouched.
117
+ let fresh;
118
+ try {
119
+ fresh = loader.unwrapExports(await loader.import(url, () => []));
120
+ }
121
+ catch (error) {
122
+ restoreCaches();
123
+ return `Reload failed: ${messageOf(error)}`;
124
+ }
125
+ const oldFibers = [...runtime.fibers];
126
+ const oldCallback = runtime.callback;
127
+ try {
128
+ // Remove the runtime first: the loader's internal/plugin handler checks
129
+ // registry.has(callback) on disposal and must not mark the entry disabled,
130
+ // and the fiber disposers then skip their own registry bookkeeping.
131
+ ctx.registry.delete(oldCallback);
132
+ // registry.delete already called fiber.dispose() on every old fiber, so a
133
+ // second dispose() call here would be useless — cordis effect wrappers
134
+ // return void 0 once their cleanup is in flight — and the teardown would
135
+ // race the fresh fiber's start below. Await the real teardown through
136
+ // each fiber's lifecycle inertia instead. Yield one turn first: fibers
137
+ // whose "ctx.plugin()" effect is async only start their unload on a
138
+ // microtask, and await() must see the in-flight inertia to wait for it.
139
+ // This ordering matters: the old TUI's terminal stop must land BEFORE the
140
+ // fresh TUI takes the terminal over, or the late stop would disable raw
141
+ // mode and pause stdin out from under the new TUI (input deadlock).
142
+ await new Promise(resolve => setImmediate(resolve));
143
+ await Promise.allSettled(oldFibers.map(fiber => fiber.await()));
144
+ for (const oldFiber of oldFibers) {
145
+ const next = oldFiber.parent.registry.plugin(fresh, oldFiber._config, () => []);
146
+ next.entry = oldFiber.entry;
147
+ if (next.entry)
148
+ next.entry.fiber = next;
149
+ await next;
150
+ }
151
+ }
152
+ catch (error) {
153
+ // Roll back: drop the failed fresh runtime, restore caches, and restart
154
+ // the previous code so the TUI comes back.
155
+ const message = messageOf(error);
156
+ try {
157
+ ctx.registry.delete(fresh);
158
+ }
159
+ catch { /* contained */ }
160
+ restoreCaches();
161
+ try {
162
+ for (const oldFiber of oldFibers) {
163
+ await oldFiber.parent.registry.plugin(oldCallback, oldFiber._config, () => []);
164
+ }
165
+ }
166
+ catch { /* contained */ }
167
+ return `Reload failed: ${message}`;
168
+ }
169
+ return 'Reloaded — the TUI restarted from the current source.';
170
+ }
171
+ //# sourceMappingURL=reload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reload.js","sourceRoot":"","sources":["../src/reload.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * TUI overlays (pi SelectList style). Currently: the `/model` picker and the
3
+ * reasoning effort picker — the terminal counterparts of the web UI's "model"
4
+ * client contribution — plus the `/theme` preference picker and the
5
+ * `/permission` preset picker.
6
+ */
7
+ import type { Context } from '@deepseek-ai/cordis';
8
+ import type { ModelSelection } from '@deepseek-ai/dsh-agent';
9
+ import type { ReasoningEffortId } from '@deepseek-ai/dsh-llm';
10
+ import { type TUI } from '@earendil-works/pi-tui';
11
+ import type { ThemePreference, TuiTheme } from './theme/index.ts';
12
+ /** Outcome of the reasoning effort picker overlay. */
13
+ export type PickEffortResult = {
14
+ kind: 'unsupported';
15
+ } | {
16
+ kind: 'cancelled';
17
+ } | {
18
+ kind: 'effort';
19
+ effort: ReasoningEffortId | 'default';
20
+ };
21
+ /**
22
+ * Open the reasoning effort picker for the current model. Resolves with the
23
+ * picked outcome: the model exposes no selectable efforts (`unsupported`),
24
+ * the user cancelled (`cancelled`), or a chosen effort — where `'default'`
25
+ * clears the effort override. Focus returns to `restoreFocus` on close.
26
+ */
27
+ export declare function pickEffort(ctx: Context, tui: TUI, theme: TuiTheme, current: ModelSelection, restoreFocus: () => void): Promise<PickEffortResult>;
28
+ /**
29
+ * Open the theme preference picker overlay. Resolves with the picked
30
+ * preference, or `undefined` when cancelled. The row matching `current` is
31
+ * preselected; focus returns to `restoreFocus` on close.
32
+ */
33
+ export declare function pickTheme(tui: TUI, theme: TuiTheme, current: ThemePreference, restoreFocus: () => void): Promise<ThemePreference | undefined>;
34
+ /**
35
+ * Open the permission preset picker overlay. Resolves with the picked preset
36
+ * name, or `undefined` when cancelled. The row matching `current` is
37
+ * preselected when present; focus returns to `restoreFocus` on close.
38
+ * Without a composed permission-presets service there is nothing to pick —
39
+ * resolves `undefined` immediately.
40
+ */
41
+ export declare function pickPermission(ctx: Context, tui: TUI, theme: TuiTheme, current: string | undefined, restoreFocus: () => void): Promise<string | undefined>;
42
+ /**
43
+ * Open the model picker overlay. Resolves with the picked selection, or
44
+ * `undefined` when cancelled. When the picked model exposes selectable
45
+ * reasoning efforts, a second overlay asks for one; cancelling that stage
46
+ * abandons the whole pick. Focus returns to `restoreFocus` on close.
47
+ */
48
+ export declare function pickModel(ctx: Context, tui: TUI, theme: TuiTheme, current: ModelSelection | undefined, restoreFocus: () => void): Promise<ModelSelection | undefined>;
@@ -0,0 +1,261 @@
1
+ /**
2
+ * TUI overlays (pi SelectList style). Currently: the `/model` picker and the
3
+ * reasoning effort picker — the terminal counterparts of the web UI's "model"
4
+ * client contribution — plus the `/theme` preference picker and the
5
+ * `/permission` preset picker.
6
+ */
7
+ import { SelectList } from '@earendil-works/pi-tui';
8
+ import { wrapFramedOverlay } from "./frame.js";
9
+ import { permissionItems } from "./permission.js";
10
+ /** First row of the effort picker: explicitly no effort override. */
11
+ const DEFAULT_EFFORT_ITEM = {
12
+ value: 'default',
13
+ label: '(provider default)',
14
+ description: 'adapter default behavior — clears the effort override',
15
+ };
16
+ /**
17
+ * Run the reasoning effort SelectList overlay over the given efforts. Resolves
18
+ * with the chosen effort (`'default'` meaning no override), or `undefined`
19
+ * when cancelled. The row matching `selectedEffort` is preselected when
20
+ * present. `afterShow` runs once the overlay is up (used by the two-stage
21
+ * model picker to hide the previous stage only after this one owns focus);
22
+ * focus returns to `restoreFocus` on close.
23
+ */
24
+ function openEffortPicker(tui, theme, efforts, selectedEffort, restoreFocus, afterShow) {
25
+ const items = [
26
+ DEFAULT_EFFORT_ITEM,
27
+ ...efforts.map(effort => ({
28
+ value: effort.id,
29
+ label: effort.name,
30
+ ...(effort.description !== undefined ? { description: effort.description } : {}),
31
+ })),
32
+ ];
33
+ return new Promise(resolve => {
34
+ const list = new SelectList(items, 12, theme.selectList);
35
+ if (selectedEffort !== undefined) {
36
+ const index = items.findIndex(item => item.value === selectedEffort);
37
+ if (index >= 0)
38
+ list.setSelectedIndex(index);
39
+ }
40
+ // The framed overlay adds 4 rows (borders + spacers) on top of the list;
41
+ // the cap must leave them room or the bottom border is sliced off on
42
+ // small terminals (13 list rows + 4 frame rows = 17 ≤ 18 at 24 rows).
43
+ const overlay = tui.showOverlay(wrapFramedOverlay(theme, list), { width: '80%', maxHeight: '75%' });
44
+ afterShow?.();
45
+ const finish = (effort) => {
46
+ overlay.hide();
47
+ restoreFocus();
48
+ resolve(effort === undefined ? undefined : { effort });
49
+ };
50
+ list.onSelect = item => finish(item.value);
51
+ list.onCancel = () => finish(undefined);
52
+ });
53
+ }
54
+ /**
55
+ * Open the reasoning effort picker for the current model. Resolves with the
56
+ * picked outcome: the model exposes no selectable efforts (`unsupported`),
57
+ * the user cancelled (`cancelled`), or a chosen effort — where `'default'`
58
+ * clears the effort override. Focus returns to `restoreFocus` on close.
59
+ */
60
+ export async function pickEffort(ctx, tui, theme, current, restoreFocus) {
61
+ const llm = ctx.get('llm');
62
+ if (llm === undefined)
63
+ return { kind: 'unsupported' };
64
+ let info;
65
+ try {
66
+ info = await llm.resolveModelInfo(current.provider, current.model);
67
+ }
68
+ catch {
69
+ return { kind: 'unsupported' };
70
+ }
71
+ const efforts = info.reasoning?.efforts;
72
+ if (efforts === undefined || efforts.length === 0)
73
+ return { kind: 'unsupported' };
74
+ const picked = await openEffortPicker(tui, theme, efforts, current.reasoningEffort, restoreFocus);
75
+ if (picked === undefined)
76
+ return { kind: 'cancelled' };
77
+ return { kind: 'effort', effort: picked.effort };
78
+ }
79
+ /** Theme preference rows of the picker, matching the settings schema vocabulary. */
80
+ const THEME_ITEMS = [
81
+ { value: 'auto', label: 'auto (terminal detection)', description: 'follow the terminal light/dark signal' },
82
+ { value: 'light', label: 'light', description: 'GitHub light palette' },
83
+ { value: 'dark', label: 'dark', description: 'GitHub dark palette' },
84
+ ];
85
+ /**
86
+ * Open the theme preference picker overlay. Resolves with the picked
87
+ * preference, or `undefined` when cancelled. The row matching `current` is
88
+ * preselected; focus returns to `restoreFocus` on close.
89
+ */
90
+ export function pickTheme(tui, theme, current, restoreFocus) {
91
+ return new Promise(resolve => {
92
+ const list = new SelectList(THEME_ITEMS, 12, theme.selectList);
93
+ const index = THEME_ITEMS.findIndex(item => item.value === current);
94
+ if (index >= 0)
95
+ list.setSelectedIndex(index);
96
+ // Framed overlay: 13 list rows + 4 frame rows fit inside 75% of 24 rows.
97
+ const overlay = tui.showOverlay(wrapFramedOverlay(theme, list), { width: '80%', maxHeight: '75%' });
98
+ const finish = (picked) => {
99
+ overlay.hide();
100
+ restoreFocus();
101
+ resolve(picked);
102
+ };
103
+ list.onSelect = item => finish(item.value);
104
+ list.onCancel = () => finish(undefined);
105
+ });
106
+ }
107
+ /**
108
+ * Open the permission preset picker overlay. Resolves with the picked preset
109
+ * name, or `undefined` when cancelled. The row matching `current` is
110
+ * preselected when present; focus returns to `restoreFocus` on close.
111
+ * Without a composed permission-presets service there is nothing to pick —
112
+ * resolves `undefined` immediately.
113
+ */
114
+ export function pickPermission(ctx, tui, theme, current, restoreFocus) {
115
+ const presets = ctx.get('permissionPresets');
116
+ if (presets === undefined)
117
+ return Promise.resolve(undefined);
118
+ const items = permissionItems(presets, current);
119
+ return new Promise((resolve, reject) => {
120
+ const list = new SelectList(items, 12, theme.selectList);
121
+ if (current !== undefined) {
122
+ const index = items.findIndex(item => item.value === current);
123
+ if (index >= 0)
124
+ list.setSelectedIndex(index);
125
+ }
126
+ // Framed overlay: 13 list rows + 4 frame rows fit inside 75% of 24 rows.
127
+ let overlay;
128
+ try {
129
+ overlay = tui.showOverlay(wrapFramedOverlay(theme, list), { width: '80%', maxHeight: '75%' });
130
+ }
131
+ catch (error) {
132
+ // Never strand the keyboard on a half-mounted picker: the rejection
133
+ // reaches the caller (submit's try/catch surfaces it), but focus must
134
+ // already be back on the editor.
135
+ restoreFocus();
136
+ reject(error);
137
+ return;
138
+ }
139
+ const finish = (picked) => {
140
+ overlay.hide();
141
+ restoreFocus();
142
+ resolve(picked);
143
+ };
144
+ list.onSelect = item => finish(item.value);
145
+ list.onCancel = () => finish(undefined);
146
+ });
147
+ }
148
+ /**
149
+ * Open the model picker overlay. Resolves with the picked selection, or
150
+ * `undefined` when cancelled. When the picked model exposes selectable
151
+ * reasoning efforts, a second overlay asks for one; cancelling that stage
152
+ * abandons the whole pick. Focus returns to `restoreFocus` on close.
153
+ */
154
+ export async function pickModel(ctx, tui, theme, current, restoreFocus) {
155
+ const llm = ctx.get('llm');
156
+ if (llm === undefined)
157
+ return undefined;
158
+ const providers = llm.listProviders();
159
+ const models = [];
160
+ await Promise.all(providers.map(async (provider) => {
161
+ try {
162
+ for (const model of await llm.listModels(provider.id)) {
163
+ models.push({ provider: provider.id, id: model.id, name: model.name });
164
+ }
165
+ }
166
+ catch { /* one provider's listing failure must not kill the picker */ }
167
+ }));
168
+ if (models.length === 0)
169
+ return undefined;
170
+ const key = (m) => `${m.provider}/${m.id}`;
171
+ const items = models.map(model => ({
172
+ value: key(model),
173
+ label: model.name === '' ? model.id : model.name,
174
+ description: model.provider,
175
+ }));
176
+ return new Promise(resolve => {
177
+ const list = new SelectList(items, 12, theme.selectList);
178
+ const currentIndex = items.findIndex(item => item.value === `${current?.provider}/${current?.model}`);
179
+ if (currentIndex >= 0)
180
+ list.setSelectedIndex(currentIndex);
181
+ // Framed overlay: 13 list rows + 4 frame rows fit inside 75% of 24 rows.
182
+ const overlay = tui.showOverlay(wrapFramedOverlay(theme, list), { width: '80%', maxHeight: '75%' });
183
+ const finish = (picked) => {
184
+ // Detach the stage-1 input handlers on first settle: while the model
185
+ // info resolves (stage 2 not up yet), a stray Esc would otherwise fire
186
+ // a ghost settle AFTER this promise already settled, and a second
187
+ // Enter would run two concurrent stage-2s.
188
+ list.onSelect = () => { };
189
+ list.onCancel = () => { };
190
+ if (picked === undefined) {
191
+ overlay.hide();
192
+ restoreFocus();
193
+ resolve(undefined);
194
+ return;
195
+ }
196
+ // The stage-1 overlay stays visible while the model info resolves; the
197
+ // stage-2 picker hides it only after it owns focus (no focus trip
198
+ // through the editor between the two overlays).
199
+ void pickEffortStage(picked).catch(() => { });
200
+ };
201
+ /**
202
+ * Second stage: when the picked route exposes selectable reasoning
203
+ * efforts, ask for one. Cancelling this stage abandons the whole pick;
204
+ * a resolution failure skips it and keeps the current effort on the same
205
+ * provider only.
206
+ */
207
+ const pickEffortStage = async (picked) => {
208
+ // The current effort survives a pick only within the same provider; a
209
+ // new provider starts from its own default behavior.
210
+ const keptEffort = current !== undefined && current.provider === picked.provider
211
+ ? current.reasoningEffort
212
+ : undefined;
213
+ let efforts;
214
+ try {
215
+ efforts = (await llm.resolveModelInfo(picked.provider, picked.id)).reasoning?.efforts;
216
+ }
217
+ catch {
218
+ // Resolution failure: fall back to the plain selection, keeping the
219
+ // current effort on the same provider.
220
+ overlay.hide();
221
+ restoreFocus();
222
+ resolve({
223
+ provider: picked.provider,
224
+ model: picked.id,
225
+ ...(keptEffort !== undefined ? { reasoningEffort: keptEffort } : {}),
226
+ });
227
+ return;
228
+ }
229
+ if (efforts === undefined || efforts.length === 0) {
230
+ // No selectable efforts on this route: same fallback as above.
231
+ overlay.hide();
232
+ restoreFocus();
233
+ resolve({
234
+ provider: picked.provider,
235
+ model: picked.id,
236
+ ...(keptEffort !== undefined ? { reasoningEffort: keptEffort } : {}),
237
+ });
238
+ return;
239
+ }
240
+ const chosen = await openEffortPicker(tui, theme, efforts, keptEffort, restoreFocus,
241
+ // Hide stage 1 only after stage 2 took focus.
242
+ () => overlay.hide());
243
+ if (chosen === undefined) {
244
+ // Escaping the effort stage is an escape from the whole pick.
245
+ resolve(undefined);
246
+ return;
247
+ }
248
+ if (chosen.effort === 'default') {
249
+ // Explicitly no override: clear any inherited effort.
250
+ resolve({ provider: picked.provider, model: picked.id });
251
+ return;
252
+ }
253
+ resolve({ provider: picked.provider, model: picked.id, reasoningEffort: chosen.effort });
254
+ };
255
+ list.onSelect = item => {
256
+ finish(models.find(model => key(model) === item.value));
257
+ };
258
+ list.onCancel = () => finish(undefined);
259
+ });
260
+ }
261
+ //# sourceMappingURL=selectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selectors.js","sourceRoot":"","sources":["../src/selectors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Session bridge between the dsh tree and the TUI.
3
+ *
4
+ * Responsibilities:
5
+ * - lazy agent creation on first prompt (openma runner pattern)
6
+ * - prompt delivery via agent.followup()
7
+ * - session-event subscription filtered to the bridge's session
8
+ * - O(1) incremental stats maintained as events arrive (footer reads these —
9
+ * never re-scans the session log, per the pi-turbo findings)
10
+ * - resume of persisted sessions (log replay into stats + transcript)
11
+ */
12
+ import type { Context } from '@deepseek-ai/cordis';
13
+ import type { Agent, AgentHandle, ModelSelection } from '@deepseek-ai/dsh-agent';
14
+ import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session';
15
+ import type { AgentView } from './dsh-events.ts';
16
+ export interface BridgeCallbacks {
17
+ /** One session-log event for the bridge's session, in log order. */
18
+ onEvent(event: SessionEvent): void;
19
+ /** Whole-agent lifecycle transition. */
20
+ onStatus(status: 'idle' | 'running'): void;
21
+ /** Live snapshot of tracked subagent (child session) rows, on any change. */
22
+ onLive(agents: readonly AgentView[]): void;
23
+ }
24
+ /** Incrementally maintained footer statistics (all O(1) reads). */
25
+ export interface BridgeStats {
26
+ inputTokens: number;
27
+ outputTokens: number;
28
+ cacheReadTokens: number;
29
+ cacheWriteTokens: number;
30
+ /** User + assistant surface messages observed. */
31
+ msgCount: number;
32
+ toolCallCount: number;
33
+ /** Whole-log cache-hit rate: cumulative cacheReadTokens ÷ billed input (input + cacheRead + cacheWrite), matching DSH web. */
34
+ cacheHitRate?: number;
35
+ }
36
+ export declare class DshSessionBridge {
37
+ private readonly ctx;
38
+ private readonly callbacks;
39
+ private handle;
40
+ private creating;
41
+ private resuming;
42
+ private readonly disposers;
43
+ private sessionId;
44
+ private selection;
45
+ /** Mutable live selection installed into the agent; `/model` mutates `current`. */
46
+ private readonly selectionRef;
47
+ /** Mirror of the agent's `agent/status`; `isRunning()` reads this. */
48
+ private running;
49
+ private readonly stats;
50
+ /** Live subagent rows keyed by child session id. */
51
+ private readonly agentViews;
52
+ /** Child session ids whose own events we fold into the agent views. */
53
+ private readonly childSessions;
54
+ /**
55
+ * Session ids that can own tracked children (this session first, then every
56
+ * tracked child — delegation nests). Used to match `parentSession` headers
57
+ * and to fold workflow events of the parent log.
58
+ */
59
+ private readonly trackedSessions;
60
+ /** `${runId}:${seq}` → childId, for `tool-workflow/agent-end` pairing. */
61
+ private readonly runSeqToChild;
62
+ constructor(ctx: Context, callbacks: BridgeCallbacks);
63
+ /** Snapshot of the incremental stats (footer reads this O(1)). */
64
+ getStats(): BridgeStats;
65
+ /** Current session id, when one exists. */
66
+ getSessionId(): SessionId | undefined;
67
+ /** Queue one user prompt, creating the session lazily on first use. */
68
+ prompt(text: string): Promise<void>;
69
+ /** The live agent, creating the session lazily when needed. */
70
+ ensureAgent(): Promise<Agent>;
71
+ /** The live agent when a session already exists (no creation side effect). */
72
+ getAgent(): Agent | undefined;
73
+ /** Whether the bridge's agent is mid-turn (mirror of `agent/status`). */
74
+ isRunning(): boolean;
75
+ /**
76
+ * Abort the bridge's agent mid-turn, mirroring the web client's stop
77
+ * button: the host-side `session.cancel` handler calls
78
+ * `agent.cancel({ kind: 'user' }, { keepInbox: true })`
79
+ * (dsh-host-apiproxy), and that is exactly what the agent's public
80
+ * synchronous `cancel(cause, options)` API takes. `keepInbox` preserves
81
+ * queued prompts for a later turn; the active driver's AbortSignal fires
82
+ * and status converges to `idle` via the existing subscription. The API
83
+ * is fire-and-forget — the returned promise resolves once the call is
84
+ * made, not when the turn settles.
85
+ * @returns false when there is no agent or it is not running; true once
86
+ * the cancellation was issued.
87
+ */
88
+ cancelActiveTurn(): Promise<boolean>;
89
+ /** Dispose the live agent (if any) and stop event subscriptions. */
90
+ dispose(): Promise<void>;
91
+ /**
92
+ * Detach the live agent WITHOUT tearing down the event subscriptions —
93
+ * `/new` wants a fresh session while the renderer keeps receiving events.
94
+ * The agent handle is disposed, the session id cleared, and the
95
+ * incremental stats zeroed (the footer must not carry the old session's
96
+ * numbers); the disposers stay installed, and the session-id filter
97
+ * re-binds on the next lazy creation. The subagent tracker is cleared and
98
+ * an empty `onLive` is emitted so the renderer drops the agents block.
99
+ */
100
+ detachCurrent(): Promise<void>;
101
+ /**
102
+ * Resume a persisted session: tear down the live agent (disposers are kept —
103
+ * the session-id filter re-binds to the resumed id) and load the persisted
104
+ * session in its place. The caller replays `handle.agent.session.events`
105
+ * through `replay()` to rebuild stats/transcript — clear the transcript
106
+ * BEFORE replaying (the renderer's echo dedupe must not see replayed user
107
+ * messages next to a stale local echo). Serialized: concurrent calls share
108
+ * the single in-flight resume.
109
+ */
110
+ resume(sessionId: SessionId): Promise<AgentHandle>;
111
+ /**
112
+ * Replay a persisted session log into the transcript and stats — the second
113
+ * half of a resume (call after `resume()`). Stats are rebuilt from zero so
114
+ * the footer reflects the resumed session only; replaying the same log
115
+ * twice is idempotent. `assistant/chunk` events are skipped: the finalized
116
+ * `assistant/message` carries the full text, and replaying chunks would
117
+ * re-run the quadratic streaming assembly for every historical step.
118
+ */
119
+ replay(events: readonly SessionEvent[]): void;
120
+ /** Update incremental stats from one event — one pass over the event only. */
121
+ private applyEvent;
122
+ /**
123
+ * Fold one event of a tracked session into the agent views (workflow events
124
+ * of the parent log + the child's own log). O(1) per event — Maps/Sets only,
125
+ * never a session-log scan. Emits `onLive` when a view actually changed.
126
+ * @returns whether a view changed (and `onLive` was emitted).
127
+ */
128
+ private foldTracked;
129
+ /** Push the current agent views (stable order) to the renderer. */
130
+ private emitLive;
131
+ private ensureSession;
132
+ /**
133
+ * Seed `selection`/`selectionRef` from the composed default — without
134
+ * clobbering a live user choice: `/model`/`/think` before the first prompt
135
+ * write the ref, and that choice must survive session creation.
136
+ */
137
+ private seedSelectionFromDefault;
138
+ /** Create the agent with the composed default model selection. */
139
+ private createSession;
140
+ /** The model selection shown in the footer (live value after `/model`). */
141
+ getSelection(): ModelSelection | undefined;
142
+ /** Apply a live model switch (`/model` selector outcome). */
143
+ setSelection(next: ModelSelection): void;
144
+ }
145
+ /**
146
+ * Persist the default model selection. Preferred path: the
147
+ * `agent-default-model` service's `saveSelection`, the official API (a
148
+ * settings.replace, last-write-wins). The namespace is read live by that
149
+ * service — the change takes effect on the next lazy session creation
150
+ * (`/new` or the next prompt) and survives restarts. Without the service,
151
+ * fall back to a direct settings mutate (with one optimistic-concurrency
152
+ * retry). Best-effort: a deployment without a settings provider is skipped
153
+ * silently; a failed write returns its error message for the caller to
154
+ * surface.
155
+ * @returns undefined on success, the failure message otherwise.
156
+ */
157
+ export declare function persistDefaultModel(ctx: Context, selection: ModelSelection): Promise<string | undefined>;