dsh-code 0.7.0 → 0.9.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 (49) hide show
  1. package/README.en.md +30 -7
  2. package/README.md +30 -7
  3. package/lib/index.mjs +3791 -853
  4. package/lib/types/app.d.ts +90 -1
  5. package/lib/types/approval.d.ts +3 -1
  6. package/lib/types/history.d.ts +15 -4
  7. package/lib/types/index.d.ts +48 -0
  8. package/lib/types/kernel-panels.d.ts +65 -8
  9. package/lib/types/models.d.ts +15 -1
  10. package/lib/types/permissions.d.ts +37 -0
  11. package/lib/types/presets.d.ts +2 -0
  12. package/lib/types/provider-settings.d.ts +144 -0
  13. package/lib/types/questions.d.ts +2 -0
  14. package/lib/types/render/animations.d.ts +8 -6
  15. package/lib/types/render/lines.d.ts +6 -0
  16. package/lib/types/render/markdown.d.ts +3 -3
  17. package/lib/types/render/projection.d.ts +97 -3
  18. package/lib/types/render/status.d.ts +26 -36
  19. package/lib/types/render/text.d.ts +14 -7
  20. package/lib/types/render/tool-detail.d.ts +3 -1
  21. package/lib/types/render/tool-preview.d.ts +14 -1
  22. package/lib/types/session-directory.d.ts +61 -2
  23. package/lib/types/store.d.ts +13 -2
  24. package/lib/types/subagents.d.ts +60 -0
  25. package/lib/types/version.d.ts +5 -0
  26. package/package.json +1 -1
  27. package/src/app.ts +1200 -219
  28. package/src/approval.ts +161 -126
  29. package/src/history.ts +20 -5
  30. package/src/index.ts +577 -167
  31. package/src/kernel-panels.ts +354 -37
  32. package/src/models.ts +26 -0
  33. package/src/permissions.ts +85 -0
  34. package/src/presets.ts +12 -0
  35. package/src/provider-settings.ts +520 -0
  36. package/src/questions.ts +15 -5
  37. package/src/render/animations.ts +32 -18
  38. package/src/render/lines.ts +236 -218
  39. package/src/render/markdown.ts +302 -4
  40. package/src/render/projection.ts +670 -11
  41. package/src/render/status.ts +68 -162
  42. package/src/render/text.ts +28 -9
  43. package/src/render/tool-detail.ts +81 -40
  44. package/src/render/tool-preview.ts +77 -34
  45. package/src/session-directory.ts +171 -10
  46. package/src/skills.ts +8 -4
  47. package/src/store.ts +26 -8
  48. package/src/subagents.ts +165 -0
  49. package/src/version.ts +16 -0
@@ -17,13 +17,17 @@ import { type ReactElement } from 'react';
17
17
  import type { CommandDescriptor } from '@deepseek-ai/dsh-commands';
18
18
  import { type ThemeName } from './theme.ts';
19
19
  import type { TranscriptStore } from './store.ts';
20
+ import { type TranscriptEntry } from './render/projection.ts';
20
21
  import type { ApprovalStore } from './approval.ts';
21
22
  import type { CommandsView } from './commands.ts';
22
23
  import type { ModelDirectory, ModelRow } from './models.ts';
24
+ import type { ProviderSettingsDirectory, ProviderTargetView } from './provider-settings.ts';
23
25
  import type { QuestionStore } from './questions.ts';
24
26
  import type { SkillsView, SkillRow } from './skills.ts';
25
27
  import type { MentionCandidate } from './mentions.ts';
28
+ import type { SubagentFeedView } from './subagents.ts';
26
29
  import type { PresetRow } from './presets.ts';
30
+ import type { PermissionRow } from './permissions.ts';
27
31
  import type { PluginRow } from './plugin-inventory.ts';
28
32
  import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts';
29
33
  /** Visual priority for one bounded local notice. */
@@ -36,6 +40,8 @@ export interface AppProps {
36
40
  approval: ApprovalStore;
37
41
  /** ask_user_question store fed by the single UI provider. */
38
42
  questions: QuestionStore;
43
+ /** Live subagent activity feed (child sessions of the current root). */
44
+ subagents: SubagentFeedView;
39
45
  /** Live slash-command descriptor list (completion candidates). */
40
46
  commands: CommandsView;
41
47
  /** Live user-invocable skill catalog (completion candidates). */
@@ -54,8 +60,10 @@ export interface AppProps {
54
60
  sessionId: string;
55
61
  /** Whether this session was resumed from persistence. */
56
62
  resumed: boolean;
57
- /** Agent preset currently composing the session. */
63
+ /** Agent preset selected for the current or pending first session. */
58
64
  mode: string;
65
+ /** Permission preset selected for the current or pending first session. */
66
+ permission: string;
59
67
  /** Submit one line: slash commands to the registry, other text to the agent. */
60
68
  dispatch(text: string): void;
61
69
  /** Submit steering: consumed at the running turn's next step boundary. */
@@ -70,8 +78,28 @@ export interface AppProps {
70
78
  loadMentions(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>;
71
79
  /** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */
72
80
  selectModel(row: ModelRow, effortId?: string): string;
81
+ /** The /subagent override label, '' when delegated agents follow the current model. */
82
+ subagentModel: string;
83
+ /** Apply one /subagent model pick; returns the override label. */
84
+ setSubagentModel(row: ModelRow, effortId?: string): string;
85
+ /** Drop the /subagent override (delegated agents follow the current model). */
86
+ clearSubagentModel(): void;
87
+ /** Delete one session subtree; resolves with the outcome line. */
88
+ deleteSession(id: string): Promise<string>;
89
+ /** Load provider/settings/credential facts for the optional /model provider stage. */
90
+ loadModelProviders?(): Promise<ProviderSettingsDirectory>;
91
+ /** Subscribe to Harness credential/settings/adapter invalidations while /model is open. */
92
+ subscribeModelProviders?(listener: () => void): () => void;
93
+ /** Store or rotate one provider credential through the Harness credential service. */
94
+ saveModelProviderCredential?(target: ProviderTargetView, key: string): Promise<void>;
95
+ /** Remove one writable provider credential without removing its settings profile. */
96
+ unsetModelProviderCredential?(target: ProviderTargetView): Promise<void>;
97
+ /** Remove one user-owned provider profile and its page-managed credential. */
98
+ removeModelProvider?(target: ProviderTargetView): Promise<void>;
73
99
  /** Cycle to the next permission preset (Shift+Tab); returns the new label. */
74
100
  cyclePermission(): string;
101
+ /** Select or inspect a permission preset without requiring a pre-existing session. */
102
+ setPermission(id: string): string;
75
103
  /** Export the transcript to a markdown file (/export [path]); reports via notices. */
76
104
  exportTranscript(argument: string): Promise<void>;
77
105
  /** Rename the session (/title <text>); returns the outcome line for the notice. */
@@ -79,9 +107,13 @@ export interface AppProps {
79
107
  /** Preset/session/plugin kernel operations. */
80
108
  loadPresets(): Promise<readonly PresetRow[]>;
81
109
  switchMode(id: string): Promise<string>;
110
+ /** Load the switchable permission presets for the /permission panel. */
111
+ loadPermissions(): Promise<readonly PermissionRow[]>;
82
112
  createSession(mode?: string): void;
83
113
  loadSessions(options: SessionDirectoryOptions, signal?: AbortSignal): Promise<readonly SessionRow[]>;
84
114
  loadSessionTranscript(id: string, signal?: AbortSignal): Promise<string>;
115
+ /** Load this session's subagent conversations (children by lineage). */
116
+ loadSubagents(): Promise<readonly SessionRow[]>;
85
117
  switchSession(row: SessionRow): void;
86
118
  cancelSessionSwitch(): boolean;
87
119
  loadPlugins(): readonly PluginRow[];
@@ -123,6 +155,63 @@ interface CompletionCandidate {
123
155
  * selection window bounds the visible rows, so no slice cap is needed.
124
156
  */
125
157
  export declare function completionCandidates(value: string, descriptors: readonly CommandDescriptor[], skills: readonly SkillRow[]): readonly CompletionCandidate[];
158
+ /** One cached settled row: the row Box plus its roomy-prompt spacers. */
159
+ interface SettledRowRecord {
160
+ /** The row Box element (keyed by the entry's settled index). */
161
+ box: ReactElement;
162
+ /** The roomy-prompt spacer BEFORE the row, or undefined. */
163
+ before: ReactElement | undefined;
164
+ /** The roomy-prompt spacer AFTER the row, or undefined. */
165
+ after: ReactElement | undefined;
166
+ /** Whether the row's text depends on the reasoning toggle (Ctrl+R). */
167
+ reasonSensitive: boolean;
168
+ /** The toggle state the row was built with. */
169
+ showReasoning: boolean;
170
+ }
171
+ /** The incremental settled-history cache (see `computeSettledRows`). */
172
+ interface SettledRowsCache {
173
+ /** The exact settled entries the cache covers (`view.entries[0..entries.length)`). */
174
+ entries: TranscriptEntry[];
175
+ /** Records keyed by entry identity; mutated in place so the append path
176
+ * never copies the whole map. */
177
+ records: Map<TranscriptEntry, SettledRowRecord>;
178
+ /** The header element (depends only on `resumed`). */
179
+ header: ReactElement;
180
+ /** The `resumed` the header was built with. */
181
+ resumed: boolean;
182
+ /** The toggle state the rows were built with. */
183
+ showReasoning: boolean;
184
+ /** The refreshEpoch the rows were built for; a bump forces a full rebuild. */
185
+ epoch: number;
186
+ /** The flat row list (header + per-entry before/box/after). */
187
+ flat: ReactElement[];
188
+ }
189
+ /** One step of `computeSettledRows`. */
190
+ interface SettledRowsResult {
191
+ cache: SettledRowsCache;
192
+ /** How many rows had to be BUILT by this step (0 = pure reuse). */
193
+ built: number;
194
+ }
195
+ /**
196
+ * The settled `<Static>` row set as a PURE incremental state machine (App
197
+ * drives it from the memo; tests drive it directly and read `built`).
198
+ *
199
+ * The settled prefix is permanently final: the projection only APPENDS below
200
+ * the flush boundary, removes pending rows at or beyond it, and replaces
201
+ * running tool/retry/command rows there too. So extending the cache never
202
+ * rescans the old prefix — a grown boundary builds ONLY the newly settled
203
+ * suffix and reuses every cached element, letting React bail out of unchanged
204
+ * rows and keeping long histories out of the per-durable-event path (no O(N)
205
+ * rebuild of rows, Map, or MarkdownBody parses). `records` is mutated in place
206
+ * on the append/toggle paths to stay O(delta).
207
+ *
208
+ * Full rebuilds run only on the rare, deliberate paths: no cache yet, a
209
+ * source-backed replay (`epoch` bump: resize / Ctrl+L / Ctrl+R remounts
210
+ * `<Static>` and must re-flush the CURRENT rows), a `resumed` change, or a shrink
211
+ * (`store.reset`). A reasoning toggle rebuilds only the rows whose text
212
+ * depends on it, preserving the other rows' element identity.
213
+ */
214
+ export declare function computeSettledRows(previous: SettledRowsCache | undefined, entries: readonly TranscriptEntry[], settled: number, showReasoning: boolean, resumed: boolean, epoch: number): SettledRowsResult;
126
215
  /** The whole terminal app; state arrives via the store, output via Ink. */
127
216
  export declare function App(props: AppProps): ReactElement;
128
217
  export {};
@@ -29,10 +29,12 @@ export interface PendingApproval {
29
29
  }
30
30
  /** The pending-question snapshot the renderer subscribes to. */
31
31
  export interface ApprovalSnapshot {
32
- /** The pending question, or undefined when none is being asked. */
32
+ /** The question on screen (queue head), or undefined when none is asked. */
33
33
  pending: PendingApproval | undefined;
34
34
  /** Presentational: an answer was submitted, the ask has not settled yet. */
35
35
  answered: boolean;
36
+ /** Further asks waiting behind the on-screen one (FIFO, Codex-style). */
37
+ queued: number;
36
38
  }
37
39
  /** Store the pending question lands in; the renderer reads, the answerer writes. */
38
40
  export interface ApprovalStore {
@@ -7,8 +7,8 @@
7
7
  *
8
8
  * @module @deepseek-ai/dsh-tui/history
9
9
  */
10
- /** Maximum entries retained in the persistent history file. */
11
- export declare const HISTORY_MAX_ENTRIES = 500;
10
+ /** Maximum entries retained in the persistent history file and the local recall pool. */
11
+ export declare const HISTORY_MAX_ENTRIES = 100;
12
12
  /** Encode one entry for the history file (JSON keeps multi-line drafts intact). */
13
13
  export declare function serializeHistoryEntry(text: string): string;
14
14
  /**
@@ -31,12 +31,23 @@ export declare function parseHistoryFile(raw: string, max?: number): readonly st
31
31
  export declare function appendHistoryContent(current: string, text: string, max?: number): string;
32
32
  /**
33
33
  * Record one in-session submission: empty text is ignored and an adjacent
34
- * duplicate collapses (Codex `record_local_submission` semantics).
34
+ * duplicate collapses (Codex `record_local_submission` semantics). The local
35
+ * pool shares the persistent pool's cap so the recall space stays bounded.
35
36
  * @param local - current in-session entries, oldest first.
36
37
  * @param text - the submitted prompt.
38
+ * @param max - the local pool cap.
37
39
  * @returns the updated local list.
38
40
  */
39
- export declare function recordLocalEntry(local: readonly string[], text: string): readonly string[];
41
+ export declare function recordLocalEntry(local: readonly string[], text: string, max?: number): readonly string[];
42
+ /**
43
+ * Serialize a capped entry list to the history file format (one JSON line per
44
+ * entry, trailing newline). The runner writes the in-memory list as the whole
45
+ * file, so rapid same-process submissions cannot lose entries to a
46
+ * read-modify-write race (the file is never read back before writing).
47
+ * @param entries - the entries to persist, oldest first.
48
+ * @returns the file content, '' for an empty list.
49
+ */
50
+ export declare function serializeHistoryList(entries: readonly string[]): string;
40
51
  /**
41
52
  * Build the recall space, newest first: local entries, then persistent
42
53
  * entries whose text is not duplicated locally (the local copy wins and the
@@ -10,6 +10,8 @@
10
10
  */
11
11
  import type { Context } from '@deepseek-ai/cordis';
12
12
  import z from '@deepseek-ai/schemastery';
13
+ import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence';
14
+ import type { TuiStartup } from './startup.ts';
13
15
  /** Stable Cordis plugin name. */
14
16
  export declare const name = "tui-runner";
15
17
  /** Core services required before the interactive session can start. */
@@ -25,9 +27,55 @@ export interface Config {
25
27
  };
26
28
  }
27
29
  export declare const Config: z<Config>;
30
+ /** The session identity this invocation will run, plus whether it is resumed. */
31
+ interface Target {
32
+ sessionId: string;
33
+ resume: boolean;
34
+ mode?: string;
35
+ cwd?: string;
36
+ }
37
+ /**
38
+ * Reduce a session id to a filename-safe /export default-name suffix. Session
39
+ * ids are normally minted `session-<uuid>`, but `--session` accepts arbitrary
40
+ * user text: path separators must never leak into the default export filename
41
+ * (which would escape the session cwd).
42
+ * @param id - the session id.
43
+ * @returns at most the last 8 filename-safe characters.
44
+ */
45
+ export declare function exportSessionIdSuffix(id: string): string;
46
+ /** One ordered step of the terminal quit cleanup. */
47
+ export interface QuitCleanupStep {
48
+ /** Step label used in diagnostics and tests. */
49
+ readonly name: string;
50
+ /** The step's async work; a rejection is contained by the sequence. */
51
+ readonly run: () => Promise<void>;
52
+ }
53
+ /**
54
+ * Run the ordered quit cleanup, then request exit. Every step rejection is
55
+ * contained (reported through `onError`) so a failed flush or dispose never
56
+ * skips the remaining cleanup; the exit request is always reached exactly
57
+ * once.
58
+ * @param steps - the cleanup steps in dependency order (settle the visible
59
+ * session, await the final in-flight composition, await durable recall).
60
+ * @param exit - the terminal exit request (code 0).
61
+ * @param onError - optional failure sink; called once per failing step and
62
+ * itself contained, so a throwing sink cannot abort the sequence.
63
+ * @returns the names of the steps that started, in order (for tests).
64
+ */
65
+ export declare function runQuitSequence(steps: readonly QuitCleanupStep[], exit: (code: number) => void, onError?: (name: string, error: unknown) => void): Promise<readonly string[]>;
66
+ /**
67
+ * Resolve the invocation's target session against the persisted headers.
68
+ * @param startup - the parsed startup flags.
69
+ * @param persistence - the persistence service; required for resume/latest.
70
+ * @param cwd - the working directory `--continue` filters by.
71
+ * @returns the target identity.
72
+ * @throws with a user-facing message when the flags name nothing resolvable.
73
+ */
74
+ export declare function resolveTarget(startup: TuiStartup, persistence: SessionPersistence | undefined, cwd: string): Promise<Target>;
28
75
  /**
29
76
  * Mount the interactive terminal driver.
30
77
  * @param ctx - plugin context carrying core services and the launcher-provided exit request.
31
78
  * @param config - validated startup config resolved from the tuiStartup provider.
32
79
  */
33
80
  export declare function apply(ctx: Context, config: Config): void;
81
+ export {};
@@ -1,6 +1,8 @@
1
1
  /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
2
2
  import { type ReactElement } from 'react';
3
- import type { ModelRow } from './models.ts';
3
+ import type { ModelDirectory, ModelRow } from './models.ts';
4
+ import type { SubagentRow } from './subagents.ts';
5
+ import type { PermissionRow } from './permissions.ts';
4
6
  import type { PresetRow } from './presets.ts';
5
7
  import type { PluginRow } from './plugin-inventory.ts';
6
8
  import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts';
@@ -11,16 +13,30 @@ export declare function ModePanel({ current, load, select, close }: {
11
13
  select(id: string): void;
12
14
  close(): void;
13
15
  }): ReactElement;
16
+ export declare function PermissionPanel({ current, load, select, close }: {
17
+ current: string;
18
+ load(): Promise<readonly PermissionRow[]>;
19
+ select(id: string): void;
20
+ close(): void;
21
+ }): ReactElement;
14
22
  export declare function PluginPanel({ load, close, initialQuery }: {
15
23
  load(): readonly PluginRow[];
16
24
  close(): void;
17
25
  initialQuery?: string;
18
26
  }): ReactElement;
19
- export declare function ResumePanel({ currentCwd, load, readTranscript, select, close }: {
27
+ export declare function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken, deleteMode, close }: {
20
28
  currentCwd: string;
21
29
  load(options: SessionDirectoryOptions, signal?: AbortSignal): Promise<readonly SessionRow[]>;
22
30
  readTranscript(id: string, signal?: AbortSignal): Promise<string>;
23
31
  select(row: SessionRow): void;
32
+ /** Arm the composer-based delete confirm for one row (App owns the keys). */
33
+ requestDelete?(row: SessionRow): void;
34
+ /** The row id awaiting y/n in the composer, when any (App-owned). */
35
+ deleteConfirmId?: string;
36
+ /** Bump to reload the listing (e.g. after a deletion). */
37
+ reloadToken?: number;
38
+ /** Opened via /delete: hint-first delete mode. */
39
+ deleteMode?: boolean;
24
40
  close(): void;
25
41
  }): ReactElement;
26
42
  /**
@@ -50,12 +66,15 @@ export declare function StatuslinePanel({ enabled, change, close }: {
50
66
  /**
51
67
  * The `/model` reasoning-effort stage (the Codex model → reasoning popup
52
68
  * contract): one bounded list over the selected model's adapter-advertised
53
- * effort levels, with the effective effort and the model default marked.
54
- * A model WITHOUT an adapter-declared default leads with a "Default"
55
- * (provider-default) row the web effort pane's first entry so the user
56
- * can clear a picked level back to provider behavior instead of being forced
57
- * to choose an advertised one. Enter applies one level; Esc returns to the
58
- * model list without applying.
69
+ * effort levels in the adapter's own display order, ids verbatim (the
70
+ * kernel treats them as opaque and rejects anything else) — with the
71
+ * effective effort and the model default marked. A model WITHOUT an
72
+ * adapter-declared default leads with a "Default" (provider-default) row
73
+ * the web effort pane's first entry so the user can clear a picked level
74
+ * back to provider behavior. A model advertising no levels opens the same
75
+ * stage with an explicit empty state (the web pane's "no levels" copy)
76
+ * instead of a bare failure notice. Enter applies one level; Esc returns to
77
+ * the model list without applying.
59
78
  */
60
79
  export declare function EffortPanel({ row, current, select, back }: {
61
80
  /** The model row whose advertised levels this stage lists. */
@@ -67,3 +86,41 @@ export declare function EffortPanel({ row, current, select, back }: {
67
86
  /** Return to the model list without applying. */
68
87
  back(): void;
69
88
  }): ReactElement;
89
+ /**
90
+ * The /agents panel (the Codex agent-picker contract, read-only): this
91
+ * conversation's subagent conversations — live rows from the activity feed
92
+ * first, persisted children the feed has not seen this process after — with
93
+ * Enter/t opening the child's full transcript in the shared read-only
94
+ * document view (the same projection the exporter uses).
95
+ */
96
+ export declare function AgentsPanel({ live, load, readTranscript, close }: {
97
+ /** Live feed rows (child sessions observed this process). */
98
+ live: readonly SubagentRow[];
99
+ /** Load this session's persisted child sessions by lineage. */
100
+ load(): Promise<readonly SessionRow[]>;
101
+ /** Read one child session's full transcript as markdown. */
102
+ readTranscript(id: string, signal?: AbortSignal): Promise<string>;
103
+ close(): void;
104
+ }): ReactElement;
105
+ /**
106
+ * The /subagent model panel: which model configuration delegated subagents
107
+ * run on. The kernel seeds child agents from the parent's CREATE-TIME
108
+ * AgentOptions, so a mid-session /model switch would otherwise leave them on
109
+ * the launch-time route; the TUI mirrors the selection onto subagent-origin
110
+ * requests (or an explicit override picked here) via an agent/request
111
+ * listener. The leading "inherit" row restores follow-the-current-model
112
+ * behavior; picking a model with several advertised efforts opens the same
113
+ * effort stage /model uses. Effort overrides are not offered separately —
114
+ * the kernel's AgentOptions has no effort channel for children, so the level
115
+ * rides the selected model exactly as /model applies it.
116
+ */
117
+ export declare function SubagentPanel({ current, load, pick, inherit, close }: {
118
+ /** Display label of the override in force, '' when following the current model. */
119
+ current: string;
120
+ load(): Promise<ModelDirectory>;
121
+ /** Apply one model (with an advertised effort, when picked) as the override. */
122
+ pick(row: ModelRow, effortId?: string): void;
123
+ /** Drop the override: subagents follow the current model again. */
124
+ inherit(): void;
125
+ close(): void;
126
+ }): ReactElement;
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { Context } from '@deepseek-ai/cordis';
11
11
  import type { ModelSelection } from '@deepseek-ai/dsh-agent';
12
- import { type LlmModelReasoningInfo } from '@deepseek-ai/dsh-llm';
12
+ import { type LlmCallConfig, type LlmModelReasoningInfo } from '@deepseek-ai/dsh-llm';
13
13
  /** Display metadata for one adapter-owned reasoning effort (mirrors `LlmReasoningEffortInfo`). */
14
14
  export interface ModelReasoningEffort {
15
15
  /** Opaque value accepted by the model's `GenerateOptions.reasoningEffort`. */
@@ -87,6 +87,20 @@ export declare function resolveEffectiveSelection(picked: ModelSelection | undef
87
87
  export declare function buildModelSelection(row: ModelRow, effortId?: string): ModelSelection;
88
88
  /** Display label for one applied selection: `provider/model` or `provider/model@effort`. */
89
89
  export declare function modelSelectionLabel(selection: ModelSelection): string;
90
+ /**
91
+ * Apply one model selection onto a resolved request config — the exact
92
+ * semantics of the kernel's `installModelSelection` request listener,
93
+ * extracted so the TUI can mirror it for subagent-origin requests: children
94
+ * spawned by the subagent tool inherit the parent's CREATE-TIME AgentOptions,
95
+ * which a mid-session /model switch never touches, so delegated work would
96
+ * otherwise keep running on the launch-time route. An absent effort strips
97
+ * any inherited effort (restoring the selected model's provider default),
98
+ * matching the kernel listener field-for-field.
99
+ * @param resolved - the config the inner chain produced.
100
+ * @param selection - the selection to enforce.
101
+ * @returns the overridden config.
102
+ */
103
+ export declare function applyModelSelectionToConfig(resolved: LlmCallConfig, selection: ModelSelection): LlmCallConfig;
90
104
  /**
91
105
  * Load the selectable model directory from the live `ctx.llm` registry.
92
106
  * Providers are listed synchronously; each provider's models are discovered
@@ -0,0 +1,37 @@
1
+ /** Permission-preset policy for pending and active TUI sessions. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
4
+ /** One selectable permission preset row for the /permission panel. */
5
+ export interface PermissionRow {
6
+ readonly id: string;
7
+ readonly description?: string;
8
+ }
9
+ /** Structural boundary over Harness permission presets; values stay service-owned. */
10
+ export interface PermissionPresetsService {
11
+ readonly names: readonly string[];
12
+ readonly defaultPreset: string;
13
+ resolve(name: string): unknown;
14
+ current(events: readonly SessionEvent[]): string;
15
+ set(session: Session, preset: string): void;
16
+ /** Client presentation metadata for one preset; may reject unknown names. */
17
+ optionOf?(name: string): {
18
+ name: string;
19
+ description?: string;
20
+ } | undefined;
21
+ }
22
+ /** Read the optional Harness service without importing its runtime package. */
23
+ export declare function permissionPresetsFrom(ctx: Context): PermissionPresetsService | undefined;
24
+ /** Effective label for either an active session or the not-yet-created first one. */
25
+ export declare function effectivePermission(service: PermissionPresetsService, session: Session | undefined, pending: string | undefined): string;
26
+ /** Validate a preset and write it only when a durable session already exists. */
27
+ export declare function selectPermission(service: PermissionPresetsService, session: Session | undefined, preset: string): string;
28
+ /** Cycle table order from the active, pending, or configured-default value. */
29
+ export declare function cyclePermission(service: PermissionPresetsService, session: Session | undefined, pending: string | undefined): string;
30
+ /** Materialize a pre-session choice after Harness creates the first session. */
31
+ export declare function applyPendingPermission(service: PermissionPresetsService, session: Session, pending: string | undefined): void;
32
+ /**
33
+ * List every switchable preset for the /permission panel, table order kept.
34
+ * Description lookup failures degrade to an undocumented row, never a failed
35
+ * panel load — `optionOf` rejects names its table no longer knows.
36
+ */
37
+ export declare function listPermissionRows(service: PermissionPresetsService): readonly PermissionRow[];
@@ -26,6 +26,8 @@ export declare function agentPresetsFrom(ctx: Context): AgentPresetsService | un
26
26
  export declare function isBlankSession(events: readonly SessionEvent[]): boolean;
27
27
  /** Latest logged selection wins; legacy sessions deliberately fall back to standard. */
28
28
  export declare function resolvePreset(session: Pick<Session, 'header' | 'events'>): string;
29
+ /** Resolve a pre-session choice, or recompose an active blank Agent. */
30
+ export declare function selectPreset(service: AgentPresetsService, agent: Agent | undefined, presetId: string): Promise<PresetRow>;
29
31
  /** Recompose atomically from the caller's perspective, logging only success. */
30
32
  export declare function switchPreset(service: AgentPresetsService, agent: Agent, presetId: string): Promise<PresetRow>;
31
33
  /** Minimal handle shape used by lifecycle tests without exposing Agent internals. */
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Provider settings adapter for the TUI `/model` provider-management panel:
3
+ * the same-process equivalent of the web host's Models page join
4
+ * (`packages/client/ui-settings-models`), reading the advisory `ctx.llm`
5
+ * registry, the redacted `ctx.settings` descriptors, and the value-free
6
+ * `ctx.credentials` facts directly. Secrets never cross this module: settings
7
+ * are read with `redactSecrets: true`, credentials are only ever described
8
+ * (never resolved), and every message is single-line without embedding key
9
+ * data.
10
+ *
11
+ * @module @deepseek-ai/dsh-tui/provider-settings
12
+ */
13
+ import type { Context } from '@deepseek-ai/cordis';
14
+ /**
15
+ * The conventional credential reference for a provider route: `<ROUTE>_API_KEY`
16
+ * with the route uppercased and every non-alphanumeric run collapsed to one
17
+ * underscore — the exact derivation the official Models page uses
18
+ * (`deriveKeyRef` in `ui-settings-models`), so a key saved here is found there.
19
+ * @param provider - provider route id (e.g. `pi-ai`, `minimax-cn`).
20
+ * @returns the derived reference name (e.g. `PI_AI_API_KEY`).
21
+ */
22
+ export declare function deriveCredentialRef(provider: string): string;
23
+ /** Value-free facts about one credential reference — never the value. */
24
+ export interface ProviderCredentialFacts {
25
+ /** Whether the reference currently resolves to a stored value. */
26
+ readonly configured: boolean;
27
+ /** Source layer supplying the value; absent while unconfigured. */
28
+ readonly source?: string;
29
+ /** Whether a write through this panel would currently succeed. */
30
+ readonly writable: boolean;
31
+ }
32
+ /**
33
+ * One row's credential state: value-free facts once the reference was
34
+ * described, a bounded error when that describe failed (the row itself is
35
+ * never dropped), or `undefined` when the row names no reference to describe —
36
+ * an unmanaged active provider, a dormant route, or a profile authenticating
37
+ * through the provider's own path.
38
+ */
39
+ export type ProviderCredentialView = ({
40
+ readonly kind: 'facts';
41
+ } & ProviderCredentialFacts) | {
42
+ readonly kind: 'error';
43
+ readonly message: string;
44
+ };
45
+ /**
46
+ * One provider row in the TUI provider-management panel: the configurable
47
+ * directory entry joined with its settings profile and credential facts.
48
+ * Every mutation below addresses this row, and the caller passes the row back
49
+ * after re-loading so the revision/ref facts are current.
50
+ */
51
+ export interface ProviderTargetView {
52
+ /** Provider route id (`GenerateOptions.provider`). */
53
+ readonly provider: string;
54
+ /** Human-readable provider name. */
55
+ readonly displayName: string;
56
+ /** Whether an adapter currently serves this route. */
57
+ readonly active: boolean;
58
+ /** User-settings namespace whose section configures this provider; '' when unmanaged. */
59
+ readonly settingsNs: string;
60
+ /** Path from that section's root to this provider's profile; [] when the whole section is the profile. */
61
+ readonly settingsPath: readonly string[];
62
+ /** Revision of the owning settings section at load (0 when no namespace resolved). */
63
+ readonly settingsRevision: number;
64
+ /** Whether the resolved profile exists (the whole section, or at `settingsPath`). */
65
+ readonly configured: boolean;
66
+ /** Whether only the user settings layer carries the profile, so removal restores the base. */
67
+ readonly removable: boolean;
68
+ /** The credential reference the resolved profile names, when one does. */
69
+ readonly credentialRef?: string;
70
+ /** The conventional reference a save uses for a dormant or ref-less profile. */
71
+ readonly suggestedRef: string;
72
+ /** Credential facts, a bounded describe error, or undefined when there is no ref to describe. */
73
+ readonly credential: ProviderCredentialView | undefined;
74
+ /** The owning adapter reports this route as hand-declared (absent when it draws no distinction). */
75
+ readonly declared?: boolean;
76
+ }
77
+ /** The resolved provider/settings/credential join. */
78
+ export interface ProviderSettingsDirectory {
79
+ /** Provider rows: configurable-directory order first, active-unmanaged rows after. */
80
+ readonly rows: readonly ProviderTargetView[];
81
+ /** Whether the settings provider accepts writes (mirrors the web page's flag). */
82
+ readonly writable: boolean;
83
+ /** Non-fatal join failures (settings/directory reads), for a degradation notice. */
84
+ readonly failures: readonly string[];
85
+ }
86
+ /** Subscribe to the same provider-directory invalidations as the official Web Models page. */
87
+ export declare function subscribeProviderSettings(ctx: Context, listener: () => void): () => void;
88
+ /** A single-line, bounded error from the provider-management adapter. */
89
+ export declare class ProviderSettingsError extends Error {
90
+ constructor(message: string);
91
+ }
92
+ /**
93
+ * Join the configurable-provider directory, the redacted settings
94
+ * namespaces, and the referenced credentials into panel rows, web-parity:
95
+ * - directory entries merge with `listProviders()` to mark each live or
96
+ * dormant, and routes registered without a directory declaration appear as
97
+ * read-only/unmanaged rows (no settings address);
98
+ * - a whole-section entry is configured whenever its namespace resolves;
99
+ * a path-addressed one only when the profile resolves there;
100
+ * - a row is removable when the user layer alone carries its profile;
101
+ * - only refs named by resolved profiles are described, and a per-ref failure
102
+ * degrades to that row's bounded error instead of losing it.
103
+ * Absent `settings`/`credentials` services are tolerated the same way.
104
+ * @param ctx - context carrying the `llm` service (settings/credentials optional).
105
+ * @returns the resolved directory; empty rows when `llm` is unavailable.
106
+ */
107
+ export declare function loadProviderSettings(ctx: Context): Promise<ProviderSettingsDirectory>;
108
+ /**
109
+ * Store a provider API key, web-parity: validate with `normalizeApiKey`
110
+ * (single-line, actionable errors that never echo the key), materialize the
111
+ * profile/`apiKeyEnv` through `settings.mutate` first when the resolved
112
+ * profile names no reference (dormant route or ref-less profile), then store
113
+ * under the trusted named ref or the derived conventional ref. An existing
114
+ * whole-section DeepSeek whose resolved profile already names
115
+ * `DEEPSEEK_API_KEY` needs no settings mutation. Env-supplied read-only keys
116
+ * are refused before any service call.
117
+ * @param ctx - context carrying `settings` (when materializing) and `credentials`.
118
+ * @param target - the joined row to write through.
119
+ * @param rawKey - the key exactly as typed; surrounding whitespace is trimmed.
120
+ * @throws {@link ProviderSettingsError} with a single-line, key-free message.
121
+ */
122
+ export declare function saveProviderCredential(ctx: Context, target: ProviderTargetView, rawKey: string): Promise<void>;
123
+ /**
124
+ * Remove the currently named credential without touching the provider
125
+ * profile. Only the resolved profile's own reference is unset; a dormant or
126
+ * ref-less row (nothing to remove), an already-absent key, and an
127
+ * env-supplied read-only key are rejected safely before any service call.
128
+ * @param ctx - context carrying the `credentials` service.
129
+ * @param target - the joined row whose named credential to unset.
130
+ * @throws {@link ProviderSettingsError} with a single-line, key-free message.
131
+ */
132
+ export declare function unsetProviderCredential(ctx: Context, target: ProviderTargetView): Promise<void>;
133
+ /**
134
+ * Remove a user-added provider profile, web-parity: only `removable` rows may
135
+ * be removed; a page-managed credential — the derived ref, configured and
136
+ * writable — is unset first (so a second-step failure leaves the row visible
137
+ * and the operation retryable), then `settings.mutate` unsets
138
+ * `target.settingsPath`. Both steps are idempotent. A hand-named credential
139
+ * ref may be shared elsewhere and is left alone.
140
+ * @param ctx - context carrying `credentials` and `settings`.
141
+ * @param target - the joined row to remove.
142
+ * @throws {@link ProviderSettingsError} with a single-line, key-free message.
143
+ */
144
+ export declare function removeProviderSettings(ctx: Context, target: ProviderTargetView): Promise<void>;
@@ -22,6 +22,8 @@ export interface PendingQuestion {
22
22
  resolve(answers: AskUserQuestionAnswer): void;
23
23
  /** Reject the provider promise as aborted (also used for Esc cancel). */
24
24
  reject(error: Error): void;
25
+ /** Detach the request's abort listener once the question settles (internal). */
26
+ detachAbort?(): void;
25
27
  }
26
28
  /** The pending-question snapshot the renderer subscribes to. */
27
29
  export interface QuestionSnapshot {
@@ -73,9 +73,11 @@ export declare const SPARK_GLYPHS: readonly ["·", "✦", "✧"];
73
73
  */
74
74
  export type DeepseekWaveBand = readonly [number, number, number];
75
75
  export declare const DEEPSEEK_WAVE_BANDS: Readonly<Record<DeepseekWaveStyle, Readonly<Record<DeepseekWaveTier, readonly DeepseekWaveBand[]>>>>;
76
+ /** Extra display time applied to every Codex ignition style. */
77
+ export declare const DEEPSEEK_WAVE_DURATION_EXTENSION_MS = 200;
76
78
  /**
77
- * Total animation duration Codex `IgnitionStyle::total_duration`: three
78
- * styles × two tiers.
79
+ * Total visible duration: the Codex ignition duration plus 200ms so its motion
80
+ * remains readable in a busy terminal.
79
81
  * @param tier - the active wave tier.
80
82
  * @param style - the active ignition style.
81
83
  * @returns the duration in milliseconds.
@@ -150,11 +152,11 @@ export declare function envelope(elapsed: number, total: number, fadeIn: number,
150
152
  */
151
153
  export declare function deepseekWaveColumnBg(tick: number, column: number, width: number, tier: DeepseekWaveTier, style: DeepseekWaveStyle, hues: readonly [RgbTriple, RgbTriple, RgbTriple], base: RgbTriple): RgbTriple | null;
152
154
  /**
153
- * The sparkle glyph for a tick — Codex `spark_frame`: from 900ms on, one
154
- * glyph every 100ms through ✧`, then silent. The deepseek (Ultra) tier
155
- * only; the Ink layer still must skip occupied cells.
155
+ * The sparkle glyph for a tick — Codex `spark_frame`, sampled on the same
156
+ * proportionally slowed DeepSeek Wave timeline as the composer background.
157
+ * The Ink layer still must skip occupied cells.
156
158
  * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
157
- * @returns the sparkle glyph, or null outside the 900..1200ms window.
159
+ * @returns the sparkle glyph, or null outside the stretched tail window.
158
160
  */
159
161
  export declare function deepseekWaveSpark(tick: number): string | null;
160
162
  /**
@@ -24,6 +24,12 @@ export declare function styledLines(segments: readonly StyledSegment[], columns:
24
24
  export declare function textLines(text: string, columns: number, style?: LineStyle): readonly StyledLine[];
25
25
  /** Markdown rows re-hardened so a single long word cannot escape the budget. */
26
26
  export declare function markdownLines(text: string, columns: number): readonly StyledLine[];
27
+ /**
28
+ * Codex-style reasoning rows: the marker occupies the reply gutter and every
29
+ * wrapped or explicit continuation starts with the same two-column indent, so
30
+ * reasoning content and assistant Markdown share one left edge.
31
+ */
32
+ export declare function reasoningLines(text: string, columns: number): readonly StyledLine[];
27
33
  /**
28
34
  * Convert one durable transcript entry to its complete scrollable row model.
29
35
  * The source entry stays intact; only the caller's visible slice is rendered.