@deepseek-harness-tui/dsh-tui 0.7.3 → 0.8.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 (69) hide show
  1. package/README.md +2 -2
  2. package/bin/dsh-tui.js +19 -3
  3. package/cordis.patch.yml +24 -11
  4. package/cordis.yml +9 -1
  5. package/lib/types/commands.d.ts.map +1 -1
  6. package/lib/types/commands.js +2 -1
  7. package/lib/types/components/MessageList.d.ts +3 -1
  8. package/lib/types/components/MessageList.d.ts.map +1 -1
  9. package/lib/types/components/MessageList.js +4 -4
  10. package/lib/types/components/PluginSceneBoundary.d.ts +31 -0
  11. package/lib/types/components/PluginSceneBoundary.d.ts.map +1 -0
  12. package/lib/types/components/PluginSceneBoundary.js +29 -0
  13. package/lib/types/components/SkillsPicker.d.ts +18 -0
  14. package/lib/types/components/SkillsPicker.d.ts.map +1 -0
  15. package/lib/types/components/SkillsPicker.js +50 -0
  16. package/lib/types/components/SplitDiffView.d.ts +28 -0
  17. package/lib/types/components/SplitDiffView.d.ts.map +1 -0
  18. package/lib/types/components/SplitDiffView.js +417 -0
  19. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +3 -1
  20. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  21. package/lib/types/components/messages/AssistantToolUseMessage.js +17 -7
  22. package/lib/types/dsh-adapter/channel.d.ts +68 -0
  23. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  24. package/lib/types/dsh-adapter/channel.js +120 -5
  25. package/lib/types/dsh-adapter/contract.d.ts +1 -1
  26. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  27. package/lib/types/dsh-adapter/contract.js +1 -0
  28. package/lib/types/dsh-adapter/index.d.ts +4 -0
  29. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  30. package/lib/types/dsh-adapter/index.js +1 -0
  31. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  32. package/lib/types/dsh-adapter/plugin.js +59 -0
  33. package/lib/types/dsh-adapter/promptDebug.d.ts +10 -0
  34. package/lib/types/dsh-adapter/promptDebug.d.ts.map +1 -0
  35. package/lib/types/dsh-adapter/promptDebug.js +148 -0
  36. package/lib/types/dsh-adapter/scenes.d.ts +75 -0
  37. package/lib/types/dsh-adapter/scenes.d.ts.map +1 -0
  38. package/lib/types/dsh-adapter/scenes.js +78 -0
  39. package/lib/types/dsh-adapter/settings-sections.d.ts +116 -0
  40. package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -0
  41. package/lib/types/dsh-adapter/settings-sections.js +63 -0
  42. package/lib/types/dsh-adapter/settingsEditor.d.ts +128 -0
  43. package/lib/types/dsh-adapter/settingsEditor.d.ts.map +1 -0
  44. package/lib/types/dsh-adapter/settingsEditor.js +244 -0
  45. package/lib/types/i18n.d.ts +137 -10
  46. package/lib/types/i18n.d.ts.map +1 -1
  47. package/lib/types/i18n.js +36 -3
  48. package/lib/types/jsx-runtime.d.ts +2 -0
  49. package/lib/types/jsx-runtime.d.ts.map +1 -0
  50. package/lib/types/jsx-runtime.js +13 -0
  51. package/lib/types/modelRoute.d.ts +10 -2
  52. package/lib/types/modelRoute.d.ts.map +1 -1
  53. package/lib/types/modelRoute.js +33 -4
  54. package/lib/types/scenes.d.ts +3 -0
  55. package/lib/types/scenes.d.ts.map +1 -0
  56. package/lib/types/scenes.js +4 -0
  57. package/lib/types/screens/Chat.d.ts.map +1 -1
  58. package/lib/types/screens/Chat.js +107 -11
  59. package/lib/types/screens/Settings.d.ts +20 -0
  60. package/lib/types/screens/Settings.d.ts.map +1 -0
  61. package/lib/types/screens/Settings.js +333 -0
  62. package/lib/types/screens/StatusLine.js +1 -1
  63. package/lib/types/settings-sections.d.ts +3 -0
  64. package/lib/types/settings-sections.d.ts.map +1 -0
  65. package/lib/types/settings-sections.js +4 -0
  66. package/lib/types/theme.d.ts +12 -0
  67. package/lib/types/theme.d.ts.map +1 -1
  68. package/lib/types/theme.js +36 -0
  69. package/package.json +17 -1
@@ -0,0 +1,75 @@
1
+ /** Provider-neutral full-screen scene registry for terminal front doors. */
2
+ import type React from 'react';
3
+ import { Context, Service } from '@deepseek-ai/cordis';
4
+ import type { Channel } from './channel.js';
5
+ /**
6
+ * Props every plugin scene receives. Both element creation and hooks must go
7
+ * through the HOST's React, never the plugin's own copy:
8
+ *
9
+ * - Hooks: a component rendered by this TUI's reconciler but calling hooks
10
+ * imported from a second React copy (the plugin's own node_modules) dies
11
+ * with an invalid-hook-call on first render. Use the injected `React`.
12
+ * - Elements: this app's reconciler (React 19) accepts only
13
+ * `Symbol.for('react.transitional.element')` elements. JSX compiled
14
+ * against a plugin-bundled older React emits `Symbol.for('react.element')`
15
+ * and throws on first render. Create elements with the injected `React`
16
+ * (`React.createElement`/`React.Fragment`), or compile JSX against the
17
+ * host runtime via tsconfig
18
+ * `"jsxImportSource": "@deepseek-harness-tui/dsh-tui"` (its `./jsx-runtime`
19
+ * subpath re-exports this app's own react/jsx-runtime). A plugin-owned
20
+ * React copy works ONLY if it is the same React 19 line — and its hooks
21
+ * remain off-limits regardless.
22
+ */
23
+ export interface TuiSceneProps {
24
+ /** The TUI's React instance — use THIS for every hook and element. */
25
+ React: typeof React;
26
+ /** The TUI's ui kit (Box/Text/useInput/useTerminalSize/…). */
27
+ ui: typeof import('../ui.js');
28
+ /** Live session channel: rows, status, trace events, notifications. */
29
+ channel: Channel;
30
+ /** Leave the scene and return to whatever screen was up before. */
31
+ close(): void;
32
+ }
33
+ export interface TuiSceneDescriptor {
34
+ /** Unique scene id (kebab-case); the plugin's own commands open it by id. */
35
+ id: string;
36
+ /** Optional human label for logs/debugging; the scene renders its own header. */
37
+ title?: string;
38
+ component: React.ComponentType<TuiSceneProps>;
39
+ }
40
+ declare module '@deepseek-ai/cordis' {
41
+ interface Context {
42
+ tuiScenes: TuiSceneRuntime;
43
+ }
44
+ }
45
+ export declare const name = "dsh-tui-scenes";
46
+ /**
47
+ * Small host-only registry; command execution remains owned by dsh-commands.
48
+ *
49
+ * A plugin registers a scene once (`ctx.tuiScenes.register(...)`, keep the
50
+ * dispose) and opens it from anywhere host-side — typically its own
51
+ * dsh-commands handler: `ctx.tuiScenes.open('my-scene')` plus a silent
52
+ * `success` result, so the conversation stays untouched while the scene
53
+ * takes the whole terminal the way the trajectory scene does.
54
+ */
55
+ export declare class TuiSceneRuntime extends Service {
56
+ private readonly scenes;
57
+ private readonly listeners;
58
+ private current;
59
+ constructor(ctx: Context);
60
+ register(descriptor: TuiSceneDescriptor): () => void;
61
+ /**
62
+ * Swap the conversation for the named scene. Returns false (and warns)
63
+ * when no plugin registered that id — a mistyped id must fail visibly in
64
+ * the log, not silently do nothing in the UI.
65
+ */
66
+ open(id: string): boolean;
67
+ close(): void;
68
+ /** The scene currently replacing the conversation, if any. */
69
+ get active(): TuiSceneDescriptor | undefined;
70
+ /** UI-side change feed: fired after every open/close/dispose transition. */
71
+ subscribe(listener: () => void): () => void;
72
+ private notify;
73
+ }
74
+ export default TuiSceneRuntime;
75
+ //# sourceMappingURL=scenes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scenes.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/scenes.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAE5E,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,KAAK,EAAE,OAAO,KAAK,CAAA;IACnB,8DAA8D;IAC9D,EAAE,EAAE,cAAc,UAAU,CAAC,CAAA;IAC7B,uEAAuE;IACvE,OAAO,EAAE,OAAO,CAAA;IAChB,mEAAmE;IACnE,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,EAAE,EAAE,MAAM,CAAA;IACV,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC,aAAa,CAAC,CAAA;CAC9C;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,SAAS,EAAE,eAAe,CAAA;KAC3B;CACF;AAED,eAAO,MAAM,IAAI,mBAAmB,CAAA;AAEpC;;;;;;;;GAQG;AACH,qBAAa,eAAgB,SAAQ,OAAO;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwC;IAC/D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,OAAO,CAAgC;gBAEnC,GAAG,EAAE,OAAO;IAIxB,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAiBpD;;;;OAIG;IACH,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYzB,KAAK,IAAI,IAAI;IAMb,8DAA8D;IAC9D,IAAI,MAAM,IAAI,kBAAkB,GAAG,SAAS,CAE3C;IAED,4EAA4E;IAC5E,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C,OAAO,CAAC,MAAM;CAGf;AAED,eAAe,eAAe,CAAA"}
@@ -0,0 +1,78 @@
1
+ /** Provider-neutral full-screen scene registry for terminal front doors. */
2
+ import { Service } from '@deepseek-ai/cordis';
3
+ export const name = 'dsh-tui-scenes';
4
+ /**
5
+ * Small host-only registry; command execution remains owned by dsh-commands.
6
+ *
7
+ * A plugin registers a scene once (`ctx.tuiScenes.register(...)`, keep the
8
+ * dispose) and opens it from anywhere host-side — typically its own
9
+ * dsh-commands handler: `ctx.tuiScenes.open('my-scene')` plus a silent
10
+ * `success` result, so the conversation stays untouched while the scene
11
+ * takes the whole terminal the way the trajectory scene does.
12
+ */
13
+ export class TuiSceneRuntime extends Service {
14
+ scenes = new Map();
15
+ listeners = new Set();
16
+ current;
17
+ constructor(ctx) {
18
+ super(ctx, 'tuiScenes');
19
+ }
20
+ register(descriptor) {
21
+ const id = descriptor.id.trim().toLowerCase();
22
+ if (!/^[a-z][a-z0-9_-]*$/u.test(id))
23
+ throw new TypeError(`invalid TUI scene id: ${descriptor.id}`);
24
+ if (this.scenes.has(id))
25
+ throw new Error(`TUI scene "${id}" is already registered`);
26
+ const normalized = { ...descriptor, id };
27
+ this.scenes.set(id, normalized);
28
+ return () => {
29
+ if (this.scenes.get(id) !== normalized)
30
+ return;
31
+ this.scenes.delete(id);
32
+ // Disposing the open scene must not strand the user on a dead screen.
33
+ if (this.current === normalized) {
34
+ this.current = undefined;
35
+ this.notify();
36
+ }
37
+ };
38
+ }
39
+ /**
40
+ * Swap the conversation for the named scene. Returns false (and warns)
41
+ * when no plugin registered that id — a mistyped id must fail visibly in
42
+ * the log, not silently do nothing in the UI.
43
+ */
44
+ open(id) {
45
+ const scene = this.scenes.get(id.trim().toLowerCase());
46
+ if (scene === undefined) {
47
+ this.ctx.logger.warn(`dsh-tui: no TUI scene registered as "${id}"`);
48
+ return false;
49
+ }
50
+ if (scene === this.current)
51
+ return true;
52
+ this.current = scene;
53
+ this.notify();
54
+ return true;
55
+ }
56
+ close() {
57
+ if (this.current === undefined)
58
+ return;
59
+ this.current = undefined;
60
+ this.notify();
61
+ }
62
+ /** The scene currently replacing the conversation, if any. */
63
+ get active() {
64
+ return this.current;
65
+ }
66
+ /** UI-side change feed: fired after every open/close/dispose transition. */
67
+ subscribe(listener) {
68
+ this.listeners.add(listener);
69
+ return () => {
70
+ this.listeners.delete(listener);
71
+ };
72
+ }
73
+ notify() {
74
+ for (const listener of this.listeners)
75
+ listener();
76
+ }
77
+ }
78
+ export default TuiSceneRuntime;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Plugin settings-section extension seam for terminal front doors.
3
+ *
4
+ * The TUI owns the settings screen: rendering, staged editing, and the
5
+ * revision-fenced `settings.mutate` writes. Optional plugins declare WHAT is
6
+ * editable — a section over their settings namespace — without coupling the
7
+ * TUI to them, mirroring the web front door's `settings.plugin.item` slot
8
+ * (plugins ship cards; the host ships the chrome). Storage, validation and
9
+ * layering stay with the dsh settings service; this registry is display
10
+ * metadata only.
11
+ */
12
+ import { Context, Service } from '@deepseek-ai/cordis';
13
+ import type { LocalizedDescriptions } from '../commands.js';
14
+ /** Control kinds the TUI settings screen knows how to render. */
15
+ export type TuiSettingsFieldKind = 'text' | 'number' | 'boolean' | 'select';
16
+ export interface TuiSettingsFieldOption {
17
+ /** Stored value. */
18
+ value: string;
19
+ /** Display label (English; also the fallback). */
20
+ label: string;
21
+ /** Provider-owned translations for the label. */
22
+ descriptions?: LocalizedDescriptions;
23
+ }
24
+ /** The write one field's draft stages when the section is saved. */
25
+ export type TuiSettingsFieldWrite = {
26
+ kind: 'set';
27
+ value: unknown;
28
+ } | {
29
+ kind: 'clear';
30
+ };
31
+ export interface TuiSettingsField {
32
+ /**
33
+ * Key path from the section root, in the settings service's `mutate` path
34
+ * vocabulary (object keys; dict keys name their entry directly).
35
+ */
36
+ path: readonly string[];
37
+ /** Short field label (English; also the fallback). */
38
+ label: string;
39
+ /** Provider-owned translations for the label. */
40
+ descriptions?: LocalizedDescriptions;
41
+ /** Optional one-line help rendered under the field. */
42
+ hint?: string;
43
+ /** Provider-owned translations for the hint. */
44
+ hintDescriptions?: LocalizedDescriptions;
45
+ kind: TuiSettingsFieldKind;
46
+ /** Choices for `kind: 'select'` (ignored otherwise). */
47
+ options?: readonly TuiSettingsFieldOption[];
48
+ /** Input placeholder for `kind: 'text' | 'number'`. */
49
+ placeholder?: string;
50
+ /**
51
+ * Credential control (mirrors the web cards' CardSecretSpec): the literal
52
+ * never rides the settings document — the draft starts blank on every
53
+ * open, a blank draft writes nothing, and a typed draft writes through the
54
+ * credentials seam under `ref`. The screen shows only whether a value is
55
+ * configured.
56
+ */
57
+ secret?: {
58
+ ref: string;
59
+ };
60
+ /**
61
+ * Render a stored value as draft text. Defaults to the kind's conversion
62
+ * (strings verbatim, numbers via `String`, booleans/selects by value).
63
+ */
64
+ format?(value: unknown): string;
65
+ /**
66
+ * The write this draft text stages, or `undefined` when the text is not a
67
+ * value this field accepts — an invalid draft blocks the save rather than
68
+ * being discarded. Defaults to the kind's conversion (an empty text/number
69
+ * draft stages a clear, letting the field re-inherit the composition
70
+ * layer).
71
+ */
72
+ parse?(text: string): TuiSettingsFieldWrite | undefined;
73
+ }
74
+ /** One plugin's section inside the TUI settings screen. */
75
+ export interface TuiSettingsSection {
76
+ /**
77
+ * Settings namespace this section edits. Should match a namespace the
78
+ * plugin registers on the dsh settings service; the screen marks the
79
+ * section unavailable when the composition serves no such namespace.
80
+ */
81
+ ns: string;
82
+ /** Section title (English; also the fallback). */
83
+ title: string;
84
+ /** Provider-owned translations for the title. */
85
+ descriptions?: LocalizedDescriptions;
86
+ /** Editable fields, in display order. */
87
+ fields: readonly TuiSettingsField[];
88
+ }
89
+ declare module '@deepseek-ai/cordis' {
90
+ interface Context {
91
+ tuiSettingsSections: TuiSettingsSectionsRuntime;
92
+ }
93
+ }
94
+ export declare const name = "dsh-tui-settings-sections";
95
+ /**
96
+ * Small host-only registry; settings storage and validation remain owned by
97
+ * the dsh settings service (`ctx.settings`).
98
+ */
99
+ export declare class TuiSettingsSectionsRuntime extends Service {
100
+ private readonly sections;
101
+ private readonly listeners;
102
+ constructor(ctx: Context);
103
+ register(section: TuiSettingsSection): () => void;
104
+ /** Registered sections in registration order. */
105
+ list(): readonly TuiSettingsSection[];
106
+ /** The section registered for a namespace, if any. */
107
+ section(ns: string): TuiSettingsSection | undefined;
108
+ /**
109
+ * Subscribe to register/unregister events so an open settings screen can
110
+ * re-read the section list (a plugin (un)loading mid-session changes it).
111
+ */
112
+ subscribe(listener: () => void): () => void;
113
+ private emit;
114
+ }
115
+ export default TuiSettingsSectionsRuntime;
116
+ //# sourceMappingURL=settings-sections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-sections.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/settings-sections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAE3D,iEAAiE;AACjE,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAA;AAE3E,MAAM,WAAW,sBAAsB;IACrC,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAA;IACb,iDAAiD;IACjD,YAAY,CAAC,EAAE,qBAAqB,CAAA;CACrC;AAED,oEAAoE;AACpE,MAAM,MAAM,qBAAqB,GAC7B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAErB,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAA;IACb,iDAAiD;IACjD,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,gBAAgB,CAAC,EAAE,qBAAqB,CAAA;IACxC,IAAI,EAAE,oBAAoB,CAAA;IAC1B,wDAAwD;IACxD,OAAO,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAA;IAC3C,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IACxB;;;OAGG;IACH,MAAM,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAA;IAC/B;;;;;;OAMG;IACH,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAAA;CACxD;AAED,2DAA2D;AAC3D,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAA;IACV,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAA;IACb,iDAAiD;IACjD,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,yCAAyC;IACzC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAA;CACpC;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,mBAAmB,EAAE,0BAA0B,CAAA;KAChD;CACF;AAED,eAAO,MAAM,IAAI,8BAA8B,CAAA;AAE/C;;;GAGG;AACH,qBAAa,0BAA2B,SAAQ,OAAO;IACrD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IACjE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;gBAEtC,GAAG,EAAE,OAAO;IAIxB,QAAQ,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAejD,iDAAiD;IACjD,IAAI,IAAI,SAAS,kBAAkB,EAAE;IAIrC,sDAAsD;IACtD,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAInD;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C,OAAO,CAAC,IAAI;CAGb;AAED,eAAe,0BAA0B,CAAA"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Plugin settings-section extension seam for terminal front doors.
3
+ *
4
+ * The TUI owns the settings screen: rendering, staged editing, and the
5
+ * revision-fenced `settings.mutate` writes. Optional plugins declare WHAT is
6
+ * editable — a section over their settings namespace — without coupling the
7
+ * TUI to them, mirroring the web front door's `settings.plugin.item` slot
8
+ * (plugins ship cards; the host ships the chrome). Storage, validation and
9
+ * layering stay with the dsh settings service; this registry is display
10
+ * metadata only.
11
+ */
12
+ import { Service } from '@deepseek-ai/cordis';
13
+ export const name = 'dsh-tui-settings-sections';
14
+ /**
15
+ * Small host-only registry; settings storage and validation remain owned by
16
+ * the dsh settings service (`ctx.settings`).
17
+ */
18
+ export class TuiSettingsSectionsRuntime extends Service {
19
+ sections = new Map();
20
+ listeners = new Set();
21
+ constructor(ctx) {
22
+ super(ctx, 'tuiSettingsSections');
23
+ }
24
+ register(section) {
25
+ const ns = section.ns.trim();
26
+ if (!/^[a-z][a-z0-9_-]*$/u.test(ns))
27
+ throw new TypeError(`invalid TUI settings-section namespace: ${section.ns}`);
28
+ if (this.sections.has(ns))
29
+ throw new Error(`TUI settings section "${ns}" is already registered`);
30
+ const normalized = { ...section, ns };
31
+ this.sections.set(ns, normalized);
32
+ this.emit();
33
+ return () => {
34
+ if (this.sections.get(ns) === normalized) {
35
+ this.sections.delete(ns);
36
+ this.emit();
37
+ }
38
+ };
39
+ }
40
+ /** Registered sections in registration order. */
41
+ list() {
42
+ return [...this.sections.values()];
43
+ }
44
+ /** The section registered for a namespace, if any. */
45
+ section(ns) {
46
+ return this.sections.get(ns.trim());
47
+ }
48
+ /**
49
+ * Subscribe to register/unregister events so an open settings screen can
50
+ * re-read the section list (a plugin (un)loading mid-session changes it).
51
+ */
52
+ subscribe(listener) {
53
+ this.listeners.add(listener);
54
+ return () => {
55
+ this.listeners.delete(listener);
56
+ };
57
+ }
58
+ emit() {
59
+ for (const listener of this.listeners)
60
+ listener();
61
+ }
62
+ }
63
+ export default TuiSettingsSectionsRuntime;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * React-free form model behind the `/settings` screen (issue #165), mirroring
3
+ * the web front door's card-form.ts semantics: a section stages what the user
4
+ * types and writes it only on save — every settings write is a durable,
5
+ * revision-fenced `settings.mutate`, so a control that committed as it
6
+ * settled turned one edit into a write the user never asked for and could
7
+ * not preview; staged text makes what is on screen exactly what a save would
8
+ * store.
9
+ *
10
+ * A field shows its effective value — the user layer over the composition
11
+ * layer over the schema default — and whether the user layer carries it. That
12
+ * presence, not a value comparison, is what marks a field overridden: an
13
+ * override equal to the composition default is still an override.
14
+ *
15
+ * The kernel side (storage, schema validation, layering, revision fencing)
16
+ * stays with the dsh settings / credentials services; this module only
17
+ * translates between drafts and `mutate` path ops.
18
+ */
19
+ import type { TuiSettingsField } from './settings-sections.js';
20
+ /** One settings namespace as the screen reads it (secrets redacted). */
21
+ export interface SettingsNamespaceView {
22
+ readonly ns: string;
23
+ /** Monotonic revision of the raw user section; fences writes. */
24
+ readonly revision: number;
25
+ /** 'live' applies immediately; 'restart' needs a relaunch. */
26
+ readonly applies: 'live' | 'restart';
27
+ /** Current resolved value (all layers composed). */
28
+ readonly value: unknown;
29
+ /** Raw user layer; a path present here is a user override. */
30
+ readonly user: unknown;
31
+ }
32
+ /**
33
+ * Runtime capabilities the settings screen needs, implemented by the channel
34
+ * over the dsh `settings` / `credentials` seams. `undefined` from
35
+ * `channel.settingsHost()` means the composition lacks them (bare cordis.yml
36
+ * start) and the screen shows namespaces read-only.
37
+ */
38
+ export interface SettingsHost {
39
+ /** Every registered namespace, secrets redacted, in registration order. */
40
+ listNamespaces(): readonly SettingsNamespaceView[];
41
+ /** Write path ops against a namespace, fenced by its current revision. */
42
+ write(ns: string, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise<void>;
43
+ /** Whether any layer supplies a credential under `ref`. */
44
+ credentialConfigured(ref: string): Promise<boolean>;
45
+ /** Persist a credential; rejects when env-shadowed or the store is read-only. */
46
+ writeCredential(ref: string, value: string): Promise<void>;
47
+ }
48
+ export type SettingsPathOp = {
49
+ op: 'set';
50
+ path: readonly string[];
51
+ value: unknown;
52
+ } | {
53
+ op: 'unset';
54
+ path: readonly string[];
55
+ };
56
+ /** One field as the screen renders it. */
57
+ export interface SettingsFieldState {
58
+ /** Draft text the control renders. */
59
+ text: string;
60
+ /** Whether saving would leave a user-layer entry for this field. */
61
+ overridden: boolean;
62
+ /** Whether the draft is not a value this field accepts, blocking the save. */
63
+ invalid: boolean;
64
+ }
65
+ /** Form state every settings section shares. */
66
+ export interface SettingsSectionShell {
67
+ /** False while the settings service serves no such namespace. */
68
+ available: boolean;
69
+ /** Whether the form holds edits that a save would write. */
70
+ dirty: boolean;
71
+ /** Whether any staged draft is invalid, which blocks the save. */
72
+ invalid: boolean;
73
+ /** Whether a save is in flight. */
74
+ saving: boolean;
75
+ /** Whether the last save failed; cleared by the next edit or save. */
76
+ failed: boolean;
77
+ }
78
+ /** Read a nested value by path (array indexes as strings). */
79
+ export declare function getPath(value: unknown, path: readonly string[]): unknown;
80
+ /**
81
+ * Whether a value explicitly carries the path — its presence marks a user
82
+ * override, independent of the value stored there.
83
+ */
84
+ export declare function hasPath(value: unknown, path: readonly string[]): boolean;
85
+ /**
86
+ * The staged form over one settings namespace's declared fields. The screen
87
+ * re-seeds it from a fresh namespace view after every save and whenever the
88
+ * underlying document changes.
89
+ */
90
+ export declare class SettingsForm {
91
+ private readonly host;
92
+ private readonly view;
93
+ private readonly fields;
94
+ private readonly edits;
95
+ saving: boolean;
96
+ failed: boolean;
97
+ constructor(host: SettingsHost, view: SettingsNamespaceView | undefined, fields: readonly TuiSettingsField[]);
98
+ get available(): boolean;
99
+ /** The namespace view the form was seeded from (undefined = not served). */
100
+ get namespace(): SettingsNamespaceView | undefined;
101
+ field(field: TuiSettingsField): SettingsFieldState;
102
+ shell(): SettingsSectionShell;
103
+ /** Whether the field holds a staged (unsaved) edit. */
104
+ isStaged(field: TuiSettingsField): boolean;
105
+ /** Stage draft text for one field. */
106
+ edit(field: TuiSettingsField, text: string): void;
107
+ /** Stage a clear, so saving lets the field re-inherit the composition layer. */
108
+ resetField(field: TuiSettingsField): void;
109
+ /** Drop every staged edit. */
110
+ discard(): void;
111
+ /** Whether any staged draft is invalid, which blocks the save. */
112
+ get invalid(): boolean;
113
+ /**
114
+ * Write every staged edit. Settings fields land as one revision-fenced
115
+ * `mutate` (one retry on a stale-revision conflict); credential fields
116
+ * write through the credentials seam. Resolves to whether every write
117
+ * landed — the caller re-seeds from a fresh namespace view afterwards.
118
+ *
119
+ * Only the edits THIS save snapshotted are cleared on success, and only
120
+ * when they are still the same edit: a draft typed while the write was in
121
+ * flight is a newer object and survives — it was never written, so it must
122
+ * not be silently dropped.
123
+ */
124
+ save(): Promise<boolean>;
125
+ /** Whether any layer supplies the credential a secret field addresses. */
126
+ credentialConfigured(field: TuiSettingsField): Promise<boolean>;
127
+ }
128
+ //# sourceMappingURL=settingsEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settingsEditor.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/settingsEditor.ts"],"names":[],"mappings":"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"}