@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
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Session TUI overlays: the `/session` info panel and the `/resume`
3
+ * persisted-session picker — terminal counterparts of the web surface's
4
+ * session management. Both follow the pi SelectList overlay pattern
5
+ * (`showOverlay` + `restoreFocus` on close); the info panel is a one-shot
6
+ * read-only component in the style of settings' ReadOnlyViewer.
7
+ */
8
+ import { getKeybindings, SelectList, } from '@earendil-works/pi-tui';
9
+ import { basename } from 'node:path';
10
+ import { wrapFramedOverlay } from "./frame.js";
11
+ import { ansiFg, BOLD, RESET } from "./theme/index.js";
12
+ import { clipToWidth, visibleWidth } from "./text.js";
13
+ /** Label column width for the key-value rows (longest: "cache read/write"). */
14
+ const LABEL_WIDTH = 12;
15
+ /** One-shot read-only panel; Esc/Enter hides the overlay and resolves. */
16
+ class SessionInfoPanel {
17
+ theme;
18
+ data;
19
+ onClose;
20
+ constructor(theme, data, onClose) {
21
+ this.theme = theme;
22
+ this.data = data;
23
+ this.onClose = onClose;
24
+ }
25
+ invalidate() { }
26
+ render(width) {
27
+ const fg = (hex) => (text) => ansiFg(hex) + text + RESET;
28
+ const p = this.theme.palette;
29
+ const lines = [
30
+ fg(p.accent)(BOLD + 'ⓘ session' + RESET),
31
+ '',
32
+ ];
33
+ const id = this.data.id;
34
+ if (id === undefined) {
35
+ lines.push(fg(p.fgDefault)(' no active session — send a prompt or /resume one'));
36
+ }
37
+ else {
38
+ const max = Math.max(2, width - LABEL_WIDTH - 4);
39
+ const clip = (text) => clipToWidth(text, max);
40
+ const row = (label, value) => {
41
+ lines.push(fg(p.fgMuted)(label.padEnd(LABEL_WIDTH)) + fg(p.fgDefault)(clip(value)));
42
+ };
43
+ const shortId = clipToWidth(id, 8);
44
+ row('session', shortId);
45
+ if (visibleWidth(id) > 8) {
46
+ lines.push(' ' + ' '.repeat(LABEL_WIDTH) + fg(p.fgSubtle)(clip(id)));
47
+ }
48
+ row('cwd', this.data.cwd ?? '—');
49
+ row('created', this.data.createdAt === undefined ? '—' : new Date(this.data.createdAt).toLocaleString());
50
+ row('model', this.data.model ?? '—');
51
+ row('think', this.data.effort ?? '—');
52
+ row('status', this.data.status === 'running' ? fg(p.accent)(this.data.status) : this.data.status);
53
+ row('messages', String(this.data.msgCount));
54
+ row('tool calls', String(this.data.toolCallCount));
55
+ row('tokens in', String(this.data.inputTokens));
56
+ row('tokens out', String(this.data.outputTokens));
57
+ row('cache read', String(this.data.cacheReadTokens));
58
+ row('cache write', String(this.data.cacheWriteTokens));
59
+ row('events', this.data.eventCount === undefined ? '—' : String(this.data.eventCount));
60
+ const parent = this.data.parentSession;
61
+ row('parent', parent === undefined ? '—' : clipToWidth(parent, 8));
62
+ }
63
+ lines.push('');
64
+ lines.push(fg(p.fgSubtle)(' Esc to close'));
65
+ return lines;
66
+ }
67
+ handleInput(data) {
68
+ if (getKeybindings().matches(data, 'tui.select.cancel') || getKeybindings().matches(data, 'tui.select.confirm')) {
69
+ this.onClose();
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * Open the `/session` info panel; resolves when the user closes it. Focus
75
+ * returns to `restoreFocus` on close — it must re-focus the CURRENT editor
76
+ * instance, which may have been rebuilt under a theme hot-swap while the
77
+ * panel was open (pi-tui's hide would otherwise restore focus to the stale
78
+ * pre-overlay editor and swallow subsequent input).
79
+ */
80
+ export async function showSessionInfo(tui, theme, data, restoreFocus) {
81
+ await new Promise(resolve => {
82
+ const panel = new SessionInfoPanel(theme, data, () => {
83
+ overlay.hide();
84
+ restoreFocus();
85
+ resolve();
86
+ });
87
+ // maxHeight only ever slices (never stretches), and the framed overlay
88
+ // needs 4 extra rows for its borders: cap high so the bottom border
89
+ // survives on small terminals (24 rows: 19 panel rows + 4 frame rows).
90
+ const overlay = tui.showOverlay(wrapFramedOverlay(theme, panel), { width: '70%', maxHeight: '100%' });
91
+ });
92
+ }
93
+ /**
94
+ * How many of the most recent candidates get a first-message preview before
95
+ * the picker opens. Older sessions fall back to the header label, so the
96
+ * picker stays fast even with a long history.
97
+ */
98
+ const PREVIEW_SESSION_CAP = 30;
99
+ /** Concurrent `inspect` calls while enriching previews. */
100
+ const PREVIEW_CONCURRENCY = 6;
101
+ /** Hard character cap for a preview string; the list column clips it further. */
102
+ const PREVIEW_MAX_CHARS = 140;
103
+ /** Join the text blocks of a message's content into one trimmed string. */
104
+ function textOfContent(content) {
105
+ if (typeof content === 'string')
106
+ return content.trim();
107
+ if (!Array.isArray(content))
108
+ return '';
109
+ let text = '';
110
+ for (const block of content) {
111
+ if (block && typeof block === 'object' && block.type === 'text') {
112
+ const blockText = block.text;
113
+ if (typeof blockText === 'string')
114
+ text += blockText + ' ';
115
+ }
116
+ }
117
+ return text.trim();
118
+ }
119
+ /**
120
+ * The session's "first sentence": the first direct human prompt, falling
121
+ * back to the first non-injected user-role text (goal/cron/recall prompts),
122
+ * then the first assistant message. Synthetic context injects (workspace
123
+ * instructions, runtime reminders) are skipped entirely — they precede the
124
+ * first real prompt and read as noise.
125
+ */
126
+ export function previewOfEvents(events) {
127
+ let fallbackUser;
128
+ let fallbackAssistant;
129
+ for (const event of events) {
130
+ if (event.type === 'user/message') {
131
+ const message = event.data;
132
+ const text = textOfContent(message?.content);
133
+ if (!text)
134
+ continue;
135
+ const kind = message?.source?.kind;
136
+ if (kind === 'user')
137
+ return text;
138
+ if (kind === 'tool' || kind === 'plugin' || kind === 'agent-instructions')
139
+ continue;
140
+ if (fallbackUser === undefined)
141
+ fallbackUser = text;
142
+ }
143
+ else if (event.type === 'assistant/message') {
144
+ const message = event.data.message;
145
+ const text = textOfContent(message?.content);
146
+ if (text && fallbackAssistant === undefined)
147
+ fallbackAssistant = text;
148
+ }
149
+ }
150
+ return fallbackUser ?? fallbackAssistant;
151
+ }
152
+ /** Collapse control chars/whitespace and clip a raw message into a one-line preview. */
153
+ export function normalizePreview(text, maxChars = PREVIEW_MAX_CHARS) {
154
+ const oneLine = text.replace(/[\x00-\x1f\x7f]+/g, ' ').replace(/\s+/g, ' ').trim();
155
+ return oneLine.length <= maxChars ? oneLine : `${oneLine.slice(0, maxChars)}…`;
156
+ }
157
+ /** Best-effort first-message preview per session id; failures map to undefined. */
158
+ async function loadSessionPreviews(persistence, ids) {
159
+ const previews = new Map();
160
+ let cursor = 0;
161
+ const workers = Array.from({ length: Math.min(PREVIEW_CONCURRENCY, ids.length) }, async () => {
162
+ while (cursor < ids.length) {
163
+ const index = cursor++;
164
+ const id = ids[index];
165
+ let preview;
166
+ try {
167
+ const { events } = await persistence.inspect(id);
168
+ const text = previewOfEvents(events);
169
+ if (text)
170
+ preview = normalizePreview(text);
171
+ }
172
+ catch {
173
+ preview = undefined;
174
+ }
175
+ previews.set(String(id), preview);
176
+ }
177
+ });
178
+ await Promise.all(workers);
179
+ return previews;
180
+ }
181
+ /**
182
+ * Validate one persisted session's log without publishing it — the /resume
183
+ * pre-check: a corrupt target must be rejected BEFORE the current live agent
184
+ * is torn down. Throws when persistence is missing or the log fails to load.
185
+ */
186
+ export async function inspectPersistedSession(ctx, id) {
187
+ const persistence = ctx.get('sessionPersistence');
188
+ if (persistence === undefined) {
189
+ throw new Error('Session persistence is not configured in this profile.');
190
+ }
191
+ return await persistence.inspect(id);
192
+ }
193
+ /**
194
+ * Open the persisted-session picker. Resolves with the picked session,
195
+ * `cancelled` when dismissed, or `empty` when no other session exists.
196
+ * Throws when the profile has no persistence backend. Focus returns to
197
+ * `restoreFocus` on close.
198
+ */
199
+ export async function pickPersistedSession(ctx, tui, theme, excludeSessionId, restoreFocus) {
200
+ const persistence = ctx.get('sessionPersistence');
201
+ if (persistence === undefined) {
202
+ throw new Error('Session persistence is not configured in this profile.');
203
+ }
204
+ let headers;
205
+ try {
206
+ headers = await persistence.list();
207
+ }
208
+ catch (error) {
209
+ const message = error instanceof Error ? error.message : String(error);
210
+ throw new Error(`Failed to list persisted sessions: ${message}`);
211
+ }
212
+ const candidates = headers
213
+ // Subagent children carry a persisted delegation depth — resuming one as
214
+ // this TUI's main conversation would misplace it in the recursion budget.
215
+ .filter(header => header.origin !== 'subagent')
216
+ .filter(header => String(header.id) !== excludeSessionId)
217
+ .sort((a, b) => b.createdAt - a.createdAt);
218
+ if (candidates.length === 0)
219
+ return { kind: 'empty' };
220
+ // Enrich the most recent candidates with their first-message preview so the
221
+ // rows are distinguishable at a glance; older ones fall back to the header
222
+ // label (cwd + short id).
223
+ const previews = await loadSessionPreviews(persistence, candidates.slice(0, PREVIEW_SESSION_CAP).map(header => header.id));
224
+ const items = candidates.map(header => {
225
+ const id = String(header.id);
226
+ const preview = previews.get(id);
227
+ return {
228
+ value: id,
229
+ label: preview ?? `${basename(header.cwd ?? '?')} · ${clipToWidth(id, 8)}`,
230
+ description: `${new Date(header.createdAt).toLocaleString()} · ${clipToWidth(id, 8)} · ${header.cwd ?? 'no cwd'}`,
231
+ };
232
+ });
233
+ return new Promise(resolve => {
234
+ // Wide primary column so previews (often CJK) get room before the time/cwd
235
+ // description column starts.
236
+ const list = new SelectList(items, 12, theme.selectList, { minPrimaryColumnWidth: 24, maxPrimaryColumnWidth: 60 });
237
+ list.setSelectedIndex(0);
238
+ // Framed overlay: 13 list rows + 4 frame rows fit inside 75% of 24 rows.
239
+ const overlay = tui.showOverlay(wrapFramedOverlay(theme, list), { width: '80%', maxHeight: '75%' });
240
+ const finish = (picked) => {
241
+ overlay.hide();
242
+ restoreFocus();
243
+ resolve(picked === undefined
244
+ ? { kind: 'cancelled' }
245
+ : { kind: 'picked', id: picked.id, header: picked });
246
+ };
247
+ list.onSelect = item => {
248
+ finish(candidates.find(header => String(header.id) === item.value));
249
+ };
250
+ list.onCancel = () => finish(undefined);
251
+ });
252
+ }
253
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EACL,cAAc,EACd,UAAU,GAIX,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAiB,MAAM,kBAAkB,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAuBrD,+EAA+E;AAC/E,MAAM,WAAW,GAAG,EAAE,CAAA;AAEtB,0EAA0E;AAC1E,MAAM,gBAAgB;IACH,KAAK,CAAU;IACf,IAAI,CAAkB;IACtB,OAAO,CAAY;IAEpC,YAAY,KAAe,EAAE,IAAsB,EAAE,OAAmB;QACtE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAED,UAAU,KAAU,CAAC;IAErB,MAAM,CAAC,KAAa;QAClB,MAAM,EAAE,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,KAAK,CAAA;QACxE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAA;QAC5B,MAAM,KAAK,GAAa;YACtB,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,GAAG,WAAW,GAAG,KAAK,CAAC;YACxC,EAAE;SACH,CAAA;QACD,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;QACvB,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACrB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,oDAAoD,CAAC,CAAC,CAAA;QACnF,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,WAAW,GAAG,CAAC,CAAC,CAAA;YAChD,MAAM,IAAI,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;YAC7D,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,KAAa,EAAQ,EAAE;gBACjD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YACrF,CAAC,CAAA;YACD,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;YAClC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;YACvB,IAAI,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;gBACzB,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YACvE,CAAC;YACD,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,CAAA;YAChC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,cAAc,EAAE,CAAC,CAAA;YACxG,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,CAAA;YACpC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,CAAA;YACrC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACjG,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;YAC3C,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAA;YAClD,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAA;YAC/C,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAA;YACjD,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAA;YACpD,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAA;YACtD,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAA;YACtF,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAA;YACtC,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;QACpE,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACd,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAA;QAC5C,OAAO,KAAK,CAAA;IACd,CAAC;IAED,WAAW,CAAC,IAAY;QACtB,IAAI,cAAc,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,mBAAmB,CAAC,IAAI,cAAc,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,oBAAoB,CAAC,EAAE,CAAC;YAChH,IAAI,CAAC,OAAO,EAAE,CAAA;QAChB,CAAC;IACH,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAQ,EACR,KAAe,EACf,IAAsB,EACtB,YAAwB;IAExB,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;QAChC,MAAM,KAAK,GAAG,IAAI,gBAAgB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE;YACnD,OAAO,CAAC,IAAI,EAAE,CAAA;YACd,YAAY,EAAE,CAAA;YACd,OAAO,EAAE,CAAA;QACX,CAAC,CAAC,CAAA;QACF,uEAAuE;QACvE,oEAAoE;QACpE,uEAAuE;QACvE,MAAM,OAAO,GAAG,GAAG,CAAC,WAAW,CAAC,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAA;IACvG,CAAC,CAAC,CAAA;AACJ,CAAC;AAeD;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAE9B,2DAA2D;AAC3D,MAAM,mBAAmB,GAAG,CAAC,CAAA;AAE7B,iFAAiF;AACjF,MAAM,iBAAiB,GAAG,GAAG,CAAA;AAE7B,2EAA2E;AAC3E,SAAS,aAAa,CAAC,OAAgB;IACrC,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC,IAAI,EAAE,CAAA;IACtD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAA;IACtC,IAAI,IAAI,GAAG,EAAE,CAAA;IACb,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAK,KAA4B,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACxF,MAAM,SAAS,GAAI,KAA4B,CAAC,IAAI,CAAA;YACpD,IAAI,OAAO,SAAS,KAAK,QAAQ;gBAAE,IAAI,IAAI,SAAS,GAAG,GAAG,CAAA;QAC5D,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,EAAE,CAAA;AACpB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,MAA+B;IAC7D,IAAI,YAAgC,CAAA;IACpC,IAAI,iBAAqC,CAAA;IACzC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YAClC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAwE,CAAA;YAC9F,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YAC5C,IAAI,CAAC,IAAI;gBAAE,SAAQ;YACnB,MAAM,IAAI,GAAG,OAAO,EAAE,MAAM,EAAE,IAAI,CAAA;YAClC,IAAI,IAAI,KAAK,MAAM;gBAAE,OAAO,IAAI,CAAA;YAChC,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,oBAAoB;gBAAE,SAAQ;YACnF,IAAI,YAAY,KAAK,SAAS;gBAAE,YAAY,GAAG,IAAI,CAAA;QACrD,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,mBAAmB,EAAE,CAAC;YAC9C,MAAM,OAAO,GAAI,KAAK,CAAC,IAA4C,CAAC,OAAO,CAAA;YAC3E,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YAC5C,IAAI,IAAI,IAAI,iBAAiB,KAAK,SAAS;gBAAE,iBAAiB,GAAG,IAAI,CAAA;QACvE,CAAC;IACH,CAAC;IACD,OAAO,YAAY,IAAI,iBAAiB,CAAA;AAC1C,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,QAAQ,GAAG,iBAAiB;IACzE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;IAClF,OAAO,OAAO,CAAC,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAA;AAChF,CAAC;AAED,mFAAmF;AACnF,KAAK,UAAU,mBAAmB,CAChC,WAA+B,EAC/B,GAAyB;IAEzB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA8B,CAAA;IACtD,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,mBAAmB,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,IAAI,EAAE;QAC3F,OAAO,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,MAAM,EAAE,CAAA;YACtB,MAAM,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;YACrB,IAAI,OAA2B,CAAA;YAC/B,IAAI,CAAC;gBACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;gBAChD,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,CAAC,CAAA;gBACpC,IAAI,IAAI;oBAAE,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,GAAG,SAAS,CAAA;YACrB,CAAC;YACD,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAA;QACnC,CAAC;IACH,CAAC,CAAC,CAAA;IACF,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAC1B,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,GAAY,EACZ,EAAa;IAEb,MAAM,WAAW,GAAG,GAAG,CAAC,GAAG,CAAC,oBAAoB,CAAmC,CAAA;IACnF,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC3E,CAAC;IACD,OAAO,MAAM,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;AACtC,CAAC;AAQD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,GAAY,EACZ,GAAQ,EACR,KAAe,EACf,gBAAoC,EACpC,YAAwB;IAExB,MAAM,WAAW,GAAG,GAAG,CAAC,GAAG,CAAC,oBAAoB,CAAmC,CAAA;IACnF,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC3E,CAAC;IACD,IAAI,OAAwB,CAAA;IAC5B,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,CAAA;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACtE,MAAM,IAAI,KAAK,CAAC,sCAAsC,OAAO,EAAE,CAAC,CAAA;IAClE,CAAC;IACD,MAAM,UAAU,GAAG,OAAO;QACxB,yEAAyE;QACzE,0EAA0E;SACzE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU,CAAC;SAC9C,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,gBAAgB,CAAC;SACxD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAA;IAC5C,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;IAErD,4EAA4E;IAC5E,2EAA2E;IAC3E,0BAA0B;IAC1B,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,WAAW,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;IAE1H,MAAM,KAAK,GAAiB,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;QAClD,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QAC5B,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAChC,OAAO;YACL,KAAK,EAAE,EAAE;YACT,KAAK,EAAE,OAAO,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE;YAC1E,WAAW,EAAE,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,cAAc,EAAE,MAAM,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,GAAG,IAAI,QAAQ,EAAE;SAClH,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,OAAO,IAAI,OAAO,CAAoB,OAAO,CAAC,EAAE;QAC9C,2EAA2E;QAC3E,6BAA6B;QAC7B,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC,UAAU,EAAE,EAAE,qBAAqB,EAAE,EAAE,EAAE,qBAAqB,EAAE,EAAE,EAAE,CAAC,CAAA;QAClH,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAA;QAExB,yEAAyE;QACzE,MAAM,OAAO,GAAG,GAAG,CAAC,WAAW,CAAC,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAA;QAEnG,MAAM,MAAM,GAAG,CAAC,MAAiC,EAAQ,EAAE;YACzD,OAAO,CAAC,IAAI,EAAE,CAAA;YACd,YAAY,EAAE,CAAA;YACd,OAAO,CAAC,MAAM,KAAK,SAAS;gBAC1B,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE;gBACvB,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;QACxD,CAAC,CAAA;QAED,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,EAAE;YACrB,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;QACrE,CAAC,CAAA;QACD,IAAI,CAAC,QAAQ,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IACzC,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Text-based settings browser — the terminal counterpart of the web GUI's
3
+ * settings surface (schema-form driven there, pi-tui overlays here).
4
+ *
5
+ * Walks `ctx.settings.describe()` (registered namespaces → serialized
6
+ * schemastery schemas → resolved values) and renders it as nested
7
+ * SettingsList overlays, one level per schema depth:
8
+ *
9
+ * level 0 category list (searchable): general / models / plugins / agent,
10
+ * then `other` for unmapped namespaces. Namespace→category comes
11
+ * from a static mapping (categorizeNamespaces) mirroring the web
12
+ * settings page: the web client slots namespaces into categories
13
+ * client-side and the data plane carries no category field, so
14
+ * the mapping is maintained here by hand; `other` is hidden when
15
+ * empty.
16
+ * level 1 namespace list for the chosen category (searchable);
17
+ * description shows applies timing. The Models category is
18
+ * special-cased: it lists configured llm-pi-ai providers (label,
19
+ * model summary, API-key state) instead of the raw namespace —
20
+ * the original schema surface is hidden — plus dedicated
21
+ * llm-deepseek / agent-default-model rows and an add-provider
22
+ * flow (built-in directory → one API key → write)
23
+ * level 2+ schema walk, dispatched on node type:
24
+ * - object/dict → drill in (nested list; dicts get an add-key row)
25
+ * - boolean / all-literal union → Enter cycles the value
26
+ * - string/number/mixed union → inline Input editor
27
+ * - array / literal / const / unknown → read-only viewer
28
+ * - role('secret') fields are masked and edited without prefill
29
+ * - every group row resets its subtree to defaults (Esc confirms)
30
+ *
31
+ * Writes go through `settings.mutate(ns, pathOps, expectedRevision)` so the
32
+ * user layer is edited by path without restating sections the UI never saw;
33
+ * the descriptor (and with it the revision) is re-read after every committed
34
+ * write, and failed writes revert the on-screen row and surface the error.
35
+ */
36
+ import type { Context } from '@deepseek-ai/cordis';
37
+ import { type SchemaNode } from '@deepseek-ai/dsh-client-schema-form';
38
+ import { type Component, type TUI } from '@earendil-works/pi-tui';
39
+ import { type TuiTheme } from './theme/index.ts';
40
+ export interface SettingsCategory {
41
+ id: string;
42
+ label: string;
43
+ namespaces: readonly string[];
44
+ }
45
+ /**
46
+ * Static namespace→category mapping, mirroring the web settings page's
47
+ * client-side slot structure (see the module header): the data plane carries
48
+ * no category field, so the slots are maintained here by hand. Namespaces not
49
+ * listed anywhere fall into the trailing `other` category.
50
+ *
51
+ * Labels are English-only: pi-tui's SettingsList search matches the label
52
+ * text, so English queries (model, shell, permission, …) hit directly.
53
+ */
54
+ export declare const CATEGORY_MAP: readonly SettingsCategory[];
55
+ /** Cap for a category row's member-name description line. */
56
+ export declare const CATEGORY_DESC_MAX = 60;
57
+ /**
58
+ * Group a describe() namespace list into ordered categories — general, models,
59
+ * plugins, agent, then `other` for everything unmapped. Categories with no
60
+ * members are dropped, including `other` when nothing falls into it.
61
+ *
62
+ * Defensive: duplicate namespaces in the input count once (a namespace
63
+ * registered twice must not be listed twice), and the `mapped` guard resolves
64
+ * CATEGORY_MAP overlap — a namespace listed in several categories goes to the
65
+ * first one, by category order.
66
+ */
67
+ export declare function categorizeNamespaces(nses: string[]): SettingsCategory[];
68
+ /**
69
+ * Description line for a category row: member names joined with ", ", capped
70
+ * at `max` columns. Namespace names are ASCII, so visible width equals char
71
+ * count here; the width-aware clip is used anyway so the cap semantics stay
72
+ * uniform with every other truncation in the TUI (an exactly-fitting string
73
+ * is kept whole, an ellipsis appears only when a column is free).
74
+ */
75
+ export declare function categoryDescription(namespaces: readonly string[], max?: number): string;
76
+ /** Human display for a resolved value (row value column; never re-scans anything). */
77
+ export declare function formatValue(value: unknown): string;
78
+ /** Display string for one union literal (the SettingsList cycle vocabulary). */
79
+ export declare function displayValue(value: unknown): string;
80
+ export interface UnionLiterals {
81
+ /** Raw literal values (strings, numbers, booleans, …) in schema order. */
82
+ values: unknown[];
83
+ /** Whether every union branch is a plain literal (cycle-eligible). */
84
+ all: boolean;
85
+ }
86
+ /** Extract literal branches of a union node (`literal`/`const` members). */
87
+ export declare function unionLiterals(node: SchemaNode): UnionLiterals;
88
+ /** One parsed Input outcome: commit a value, unset (reset), or reject. */
89
+ export type ParseOutcome = {
90
+ kind: 'value';
91
+ value: unknown;
92
+ } | {
93
+ kind: 'unset';
94
+ } | {
95
+ kind: 'keep';
96
+ } | {
97
+ kind: 'error';
98
+ error: string;
99
+ };
100
+ export declare function parseNumberInput(text: string): ParseOutcome;
101
+ export declare function parseStringInput(text: string): ParseOutcome;
102
+ /** Mixed (non-literal) union editing: JSON values, else a raw string branch. */
103
+ export declare function parseUnionInput(text: string, node: SchemaNode): ParseOutcome;
104
+ /** Seed value for a newly added dict key, from the inner schema. */
105
+ export declare function defaultValueFor(node: SchemaNode): unknown;
106
+ /** Description line for one field row (static — built once per row). */
107
+ export declare function fieldDescription(node: SchemaNode, userOverride: boolean): string;
108
+ /**
109
+ * Outcome of one commit write: an `error` (shown with the ✘ marker and
110
+ * danger color), a `notice` (shown as a plain hint — the write succeeded but
111
+ * needs a follow-up, e.g. "provider added, key must come from the
112
+ * environment"), or `undefined` (clean success → the editor closes). Exactly
113
+ * one of `error`/`notice` is set.
114
+ */
115
+ export interface CommitResult {
116
+ error?: string;
117
+ notice?: string;
118
+ }
119
+ interface EditOptions {
120
+ title: string;
121
+ subtitle: string;
122
+ initial: string;
123
+ parse: (text: string) => ParseOutcome;
124
+ onCommit: (outcome: ParseOutcome) => Promise<CommitResult | undefined>;
125
+ onDone: () => void;
126
+ /** Error sink for writes that fail after the editor already closed (Esc). */
127
+ onError?: (message: string) => void;
128
+ /**
129
+ * Masked rendering: the value is shown as a dot row instead of plaintext
130
+ * (secrets never echo). Editing semantics are unaffected — the internal
131
+ * Input still owns input/delete/paste/submit.
132
+ */
133
+ secret?: boolean;
134
+ }
135
+ /** Inline value editor: title, current-value line, error/notice line, Input. */
136
+ export declare class EditField implements Component {
137
+ private readonly tui;
138
+ private readonly options;
139
+ private readonly input;
140
+ private error;
141
+ /** Non-error commit message (success with a follow-up hint, see CommitResult). */
142
+ private notice;
143
+ /** Set while a commit write is in flight; extra Enter presses are ignored. */
144
+ private pending;
145
+ /** Guards onDone: exactly one terminal transition (submit success/keep/escape). */
146
+ private done;
147
+ private readonly fg;
148
+ private readonly fgMuted;
149
+ private readonly fgDanger;
150
+ /** Popup-surface background for the secret mask line. */
151
+ private readonly maskBg;
152
+ constructor(tui: TUI, options: EditOptions, theme: TuiTheme);
153
+ /** Terminal transition — runs the caller's onDone exactly once. */
154
+ private finish;
155
+ invalidate(): void;
156
+ render(width: number): string[];
157
+ /**
158
+ * Masked input line for secret fields: a dot row (one dot per visible
159
+ * column of the value, capped to the popup width) with a `▎` marker at the
160
+ * cursor position. The real cursor is not rendered — the marker only hints
161
+ * at the editing position; input semantics live in the internal Input.
162
+ */
163
+ private maskLine;
164
+ handleInput(data: string): void;
165
+ }
166
+ export interface OpenSettingsBrowserOptions {
167
+ ctx: Context;
168
+ tui: TUI;
169
+ theme: TuiTheme;
170
+ /** Focus target to restore when the browser closes (usually the editor). */
171
+ restoreFocus: () => void;
172
+ /** Error sink for writes that fail outside an inline editor (transcript). */
173
+ onError: (message: string) => void;
174
+ }
175
+ /**
176
+ * Open the modal settings browser. Resolves when it closes with the number of
177
+ * committed writes, or -1 when no namespace is registered (nothing to show).
178
+ */
179
+ export declare function openSettingsBrowser(options: OpenSettingsBrowserOptions): Promise<number>;
180
+ export {};