dsh-code 0.7.0 → 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 (44) hide show
  1. package/README.en.md +20 -6
  2. package/README.md +20 -6
  3. package/lib/index.mjs +2685 -622
  4. package/lib/types/app.d.ts +77 -1
  5. package/lib/types/history.d.ts +15 -4
  6. package/lib/types/index.d.ts +48 -0
  7. package/lib/types/kernel-panels.d.ts +7 -0
  8. package/lib/types/permissions.d.ts +37 -0
  9. package/lib/types/presets.d.ts +2 -0
  10. package/lib/types/provider-settings.d.ts +144 -0
  11. package/lib/types/questions.d.ts +2 -0
  12. package/lib/types/render/animations.d.ts +8 -6
  13. package/lib/types/render/lines.d.ts +6 -0
  14. package/lib/types/render/markdown.d.ts +3 -3
  15. package/lib/types/render/projection.d.ts +95 -3
  16. package/lib/types/render/status.d.ts +26 -36
  17. package/lib/types/render/text.d.ts +14 -7
  18. package/lib/types/render/tool-detail.d.ts +3 -1
  19. package/lib/types/render/tool-preview.d.ts +4 -1
  20. package/lib/types/session-directory.d.ts +15 -0
  21. package/lib/types/store.d.ts +13 -2
  22. package/lib/types/version.d.ts +5 -0
  23. package/package.json +1 -1
  24. package/src/app.ts +847 -150
  25. package/src/approval.ts +11 -2
  26. package/src/history.ts +20 -5
  27. package/src/index.ts +402 -159
  28. package/src/kernel-panels.ts +45 -8
  29. package/src/permissions.ts +85 -0
  30. package/src/presets.ts +12 -0
  31. package/src/provider-settings.ts +520 -0
  32. package/src/questions.ts +15 -5
  33. package/src/render/animations.ts +32 -18
  34. package/src/render/lines.ts +21 -6
  35. package/src/render/markdown.ts +302 -4
  36. package/src/render/projection.ts +665 -10
  37. package/src/render/status.ts +68 -162
  38. package/src/render/text.ts +28 -9
  39. package/src/render/tool-detail.ts +81 -40
  40. package/src/render/tool-preview.ts +18 -2
  41. package/src/session-directory.ts +44 -5
  42. package/src/skills.ts +8 -4
  43. package/src/store.ts +26 -8
  44. package/src/version.ts +16 -0
@@ -17,13 +17,16 @@ 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';
26
28
  import type { PresetRow } from './presets.ts';
29
+ import type { PermissionRow } from './permissions.ts';
27
30
  import type { PluginRow } from './plugin-inventory.ts';
28
31
  import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts';
29
32
  /** Visual priority for one bounded local notice. */
@@ -54,8 +57,10 @@ export interface AppProps {
54
57
  sessionId: string;
55
58
  /** Whether this session was resumed from persistence. */
56
59
  resumed: boolean;
57
- /** Agent preset currently composing the session. */
60
+ /** Agent preset selected for the current or pending first session. */
58
61
  mode: string;
62
+ /** Permission preset selected for the current or pending first session. */
63
+ permission: string;
59
64
  /** Submit one line: slash commands to the registry, other text to the agent. */
60
65
  dispatch(text: string): void;
61
66
  /** Submit steering: consumed at the running turn's next step boundary. */
@@ -70,8 +75,20 @@ export interface AppProps {
70
75
  loadMentions(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>;
71
76
  /** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */
72
77
  selectModel(row: ModelRow, effortId?: string): string;
78
+ /** Load provider/settings/credential facts for the optional /model provider stage. */
79
+ loadModelProviders?(): Promise<ProviderSettingsDirectory>;
80
+ /** Subscribe to Harness credential/settings/adapter invalidations while /model is open. */
81
+ subscribeModelProviders?(listener: () => void): () => void;
82
+ /** Store or rotate one provider credential through the Harness credential service. */
83
+ saveModelProviderCredential?(target: ProviderTargetView, key: string): Promise<void>;
84
+ /** Remove one writable provider credential without removing its settings profile. */
85
+ unsetModelProviderCredential?(target: ProviderTargetView): Promise<void>;
86
+ /** Remove one user-owned provider profile and its page-managed credential. */
87
+ removeModelProvider?(target: ProviderTargetView): Promise<void>;
73
88
  /** Cycle to the next permission preset (Shift+Tab); returns the new label. */
74
89
  cyclePermission(): string;
90
+ /** Select or inspect a permission preset without requiring a pre-existing session. */
91
+ setPermission(id: string): string;
75
92
  /** Export the transcript to a markdown file (/export [path]); reports via notices. */
76
93
  exportTranscript(argument: string): Promise<void>;
77
94
  /** Rename the session (/title <text>); returns the outcome line for the notice. */
@@ -79,6 +96,8 @@ export interface AppProps {
79
96
  /** Preset/session/plugin kernel operations. */
80
97
  loadPresets(): Promise<readonly PresetRow[]>;
81
98
  switchMode(id: string): Promise<string>;
99
+ /** Load the switchable permission presets for the /permission panel. */
100
+ loadPermissions(): Promise<readonly PermissionRow[]>;
82
101
  createSession(mode?: string): void;
83
102
  loadSessions(options: SessionDirectoryOptions, signal?: AbortSignal): Promise<readonly SessionRow[]>;
84
103
  loadSessionTranscript(id: string, signal?: AbortSignal): Promise<string>;
@@ -123,6 +142,63 @@ interface CompletionCandidate {
123
142
  * selection window bounds the visible rows, so no slice cap is needed.
124
143
  */
125
144
  export declare function completionCandidates(value: string, descriptors: readonly CommandDescriptor[], skills: readonly SkillRow[]): readonly CompletionCandidate[];
145
+ /** One cached settled row: the row Box plus its roomy-prompt spacers. */
146
+ interface SettledRowRecord {
147
+ /** The row Box element (keyed by the entry's settled index). */
148
+ box: ReactElement;
149
+ /** The roomy-prompt spacer BEFORE the row, or undefined. */
150
+ before: ReactElement | undefined;
151
+ /** The roomy-prompt spacer AFTER the row, or undefined. */
152
+ after: ReactElement | undefined;
153
+ /** Whether the row's text depends on the reasoning toggle (Ctrl+R). */
154
+ reasonSensitive: boolean;
155
+ /** The toggle state the row was built with. */
156
+ showReasoning: boolean;
157
+ }
158
+ /** The incremental settled-history cache (see `computeSettledRows`). */
159
+ interface SettledRowsCache {
160
+ /** The exact settled entries the cache covers (`view.entries[0..entries.length)`). */
161
+ entries: TranscriptEntry[];
162
+ /** Records keyed by entry identity; mutated in place so the append path
163
+ * never copies the whole map. */
164
+ records: Map<TranscriptEntry, SettledRowRecord>;
165
+ /** The header element (depends only on `resumed`). */
166
+ header: ReactElement;
167
+ /** The `resumed` the header was built with. */
168
+ resumed: boolean;
169
+ /** The toggle state the rows were built with. */
170
+ showReasoning: boolean;
171
+ /** The refreshEpoch the rows were built for; a bump forces a full rebuild. */
172
+ epoch: number;
173
+ /** The flat row list (header + per-entry before/box/after). */
174
+ flat: ReactElement[];
175
+ }
176
+ /** One step of `computeSettledRows`. */
177
+ interface SettledRowsResult {
178
+ cache: SettledRowsCache;
179
+ /** How many rows had to be BUILT by this step (0 = pure reuse). */
180
+ built: number;
181
+ }
182
+ /**
183
+ * The settled `<Static>` row set as a PURE incremental state machine (App
184
+ * drives it from the memo; tests drive it directly and read `built`).
185
+ *
186
+ * The settled prefix is permanently final: the projection only APPENDS below
187
+ * the flush boundary, removes pending rows at or beyond it, and replaces
188
+ * running tool/retry/command rows there too. So extending the cache never
189
+ * rescans the old prefix — a grown boundary builds ONLY the newly settled
190
+ * suffix and reuses every cached element, letting React bail out of unchanged
191
+ * rows and keeping long histories out of the per-durable-event path (no O(N)
192
+ * rebuild of rows, Map, or MarkdownBody parses). `records` is mutated in place
193
+ * on the append/toggle paths to stay O(delta).
194
+ *
195
+ * Full rebuilds run only on the rare, deliberate paths: no cache yet, a
196
+ * source-backed replay (`epoch` bump: resize / Ctrl+L / Ctrl+R remounts
197
+ * `<Static>` and must re-flush the CURRENT rows), a `resumed` change, or a shrink
198
+ * (`store.reset`). A reasoning toggle rebuilds only the rows whose text
199
+ * depends on it, preserving the other rows' element identity.
200
+ */
201
+ export declare function computeSettledRows(previous: SettledRowsCache | undefined, entries: readonly TranscriptEntry[], settled: number, showReasoning: boolean, resumed: boolean, epoch: number): SettledRowsResult;
126
202
  /** The whole terminal app; state arrives via the store, output via Ink. */
127
203
  export declare function App(props: AppProps): ReactElement;
128
204
  export {};
@@ -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,7 @@
1
1
  /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
2
2
  import { type ReactElement } from 'react';
3
3
  import type { ModelRow } from './models.ts';
4
+ import type { PermissionRow } from './permissions.ts';
4
5
  import type { PresetRow } from './presets.ts';
5
6
  import type { PluginRow } from './plugin-inventory.ts';
6
7
  import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts';
@@ -11,6 +12,12 @@ export declare function ModePanel({ current, load, select, close }: {
11
12
  select(id: string): void;
12
13
  close(): void;
13
14
  }): ReactElement;
15
+ export declare function PermissionPanel({ current, load, select, close }: {
16
+ current: string;
17
+ load(): Promise<readonly PermissionRow[]>;
18
+ select(id: string): void;
19
+ close(): void;
20
+ }): ReactElement;
14
21
  export declare function PluginPanel({ load, close, initialQuery }: {
15
22
  load(): readonly PluginRow[];
16
23
  close(): void;
@@ -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.
@@ -3,13 +3,13 @@
3
3
  * block/inline parser producing styled line segments the Ink renderer maps
4
4
  * to colored text. No ANSI here — the app owns color mapping, tests own the
5
5
  * structure. The subset mirrors what agent replies actually emit: headings,
6
- * emphasis, inline/fenced code, flat lists, blockquotes, links, rules, and
7
- * wrapped paragraphs. Unknown syntax degrades to plain text (never throws).
6
+ * emphasis, inline/fenced code, flat lists, blockquotes, links, rules, GFM
7
+ * tables, and wrapped paragraphs. Unknown syntax degrades to plain text.
8
8
  *
9
9
  * @module @deepseek-ai/dsh-code/render/markdown
10
10
  */
11
11
  /** Style classes the renderer emits; the app maps them to colors/props. */
12
- export type MdStyle = 'plain' | 'bold' | 'italic' | 'boldItalic' | 'code' | 'accent' | 'dim' | 'strike';
12
+ export type MdStyle = 'plain' | 'bold' | 'italic' | 'boldItalic' | 'code' | 'accent' | 'accentBold' | 'dim' | 'strike';
13
13
  /** One styled run of text. */
14
14
  export interface MdSegment {
15
15
  /** Visible text (no ANSI). */
@@ -236,7 +236,10 @@ export interface TranscriptView {
236
236
  /**
237
237
  * Fold-internal timing anchors, never rendered: open step and tool-call
238
238
  * start timestamps the next `assistant/message` / `tool/result` resolves
239
- * against. Keyed `turn:step` and by call id.
239
+ * against. Keyed `turn:step` and by call id. `turnSteps`/`turnTools`
240
+ * track which step/tool anchors still belong to the open turn so
241
+ * `turn/end` (and a superseding `step/start`) can sweep anchors an
242
+ * interruption left behind; `turnFiles` keys mutated paths by turn.
240
243
  */
241
244
  readonly anchors: {
242
245
  stepStart: Map<string, number>;
@@ -245,6 +248,8 @@ export interface TranscriptView {
245
248
  compactionTokens: Map<string, number>;
246
249
  lastPruneTokens: number;
247
250
  turnFiles: Map<number, Set<string>>;
251
+ turnSteps: Map<number, string>;
252
+ turnTools: Map<number, Set<string>>;
248
253
  };
249
254
  }
250
255
  /** A fresh, empty transcript view. */
@@ -256,8 +261,92 @@ export declare function createTranscriptView(): TranscriptView;
256
261
  * @returns the view after the event; the input view is never mutated.
257
262
  */
258
263
  export declare function projectEvent(view: TranscriptView, event: SessionEvent): TranscriptView;
264
+ /**
265
+ * Mutable replay accumulator: folds a persisted log into the identical view
266
+ * `projectEvent` would produce, but in near-linear time. Where `projectEvent`
267
+ * is copy-on-write — every append/scan rebuilds the whole `entries` array, so
268
+ * folding a full log costs O(N²) — the accumulator appends by push, resolves
269
+ * id-keyed updates (tool/result, command/done, retry-started) through index
270
+ * maps, and tombstones retired pending rows, so the whole log folds in O(N)
271
+ * plus one compaction pass when tombstones exist.
272
+ *
273
+ * Index maps never delete: every appended row registers its index, so an id
274
+ * lookup miss provably means no matching row exists and the update is an O(1)
275
+ * no-op (a malicious/orphan-heavy log cannot force per-orphan full-array
276
+ * scans). Each id maps to ALL of its indices, so a duplicate id updates every
277
+ * matching row exactly like the copy-on-write reducer.
278
+ *
279
+ * @internal Exported only so tests can (a) prove replay ≡ sequential
280
+ * `projectEvent` folds and (b) assert the linear complexity deterministically
281
+ * via {@link ReplayAccumulator.ops}, which counts entry-level container work
282
+ * instead of relying on wall-clock thresholds. No public consumer.
283
+ */
284
+ export interface ReplayAccumulator {
285
+ /** Working entry list; `undefined` marks a retired pending row (tombstone). */
286
+ entries: (TranscriptEntry | undefined)[];
287
+ /** callId → every index into `entries` holding a `tool` row with that id. */
288
+ toolIndex: Map<string, number[]>;
289
+ /** commandId → every index into `entries` holding a `command` row with that id. */
290
+ commandIndex: Map<string, number[]>;
291
+ /** retryId → every index into `entries` holding a `retry` row with that id. */
292
+ retryIndex: Map<string, number[]>;
293
+ /** messageId → every index into `entries` holding a `pending` row with that id. */
294
+ pendingIndex: Map<string, number[]>;
295
+ /** Tombstone count; zero means `entries` is already the final array. */
296
+ removedCount: number;
297
+ /** Mutable inbox id lists, mirroring `view.pending` order per target. */
298
+ pendingTurn: string[];
299
+ pendingStep: string[];
300
+ streaming: string;
301
+ streamingReasoning: string;
302
+ todos: readonly TodoItem[];
303
+ busy: boolean;
304
+ busySince: number;
305
+ model: string;
306
+ plan: boolean;
307
+ permission: string;
308
+ title: string;
309
+ sandbox: string;
310
+ goal: GoalFold | undefined;
311
+ stats: TranscriptStats;
312
+ stepStart: Map<string, number>;
313
+ toolStart: Map<string, number>;
314
+ firstChunkAt: Map<string, number>;
315
+ compactionTokens: Map<string, number>;
316
+ lastPruneTokens: number;
317
+ turnFiles: Map<number, Set<string>>;
318
+ turnSteps: Map<number, string>;
319
+ turnTools: Map<number, Set<string>>;
320
+ /** Entry-level container operations performed so far (test instrumentation). */
321
+ ops: number;
322
+ }
323
+ /** @internal A fresh replay accumulator whose state mirrors `createTranscriptView()`. */
324
+ export declare function createReplayAccumulator(): ReplayAccumulator;
325
+ /**
326
+ * Fold one session event into a replay accumulator. This mirrors
327
+ * {@link projectEvent} case for case — same stats arithmetic, same anchor
328
+ * set/delete behavior, same entry shapes — so the finished view is identical
329
+ * to a sequential fold; only the `entries` container operations are mutable.
330
+ *
331
+ * @internal Test-instrumentation path; `projectEvents` is the public entry.
332
+ */
333
+ export declare function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent): void;
334
+ /**
335
+ * Materialize the accumulated fold as a `TranscriptView`, compacting any
336
+ * retired tombstones. The anchors maps are handed through as-is (their
337
+ * content is identical to a sequential fold's).
338
+ *
339
+ * @internal Test-instrumentation path; `projectEvents` is the public entry.
340
+ */
341
+ export declare function finishReplay(acc: ReplayAccumulator): TranscriptView;
259
342
  /**
260
343
  * Fold a replayed event history into one view.
344
+ *
345
+ * Folding is near-linear in the log size: the mutable replay accumulator
346
+ * appends in place and resolves id-keyed updates through index maps, so a
347
+ * long persisted session replays without the O(N²) copy-on-write rebuilds a
348
+ * naive sequential fold would incur. The result is identical to folding
349
+ * {@link projectEvent} per event in order.
261
350
  * @param events - events in `seq` order.
262
351
  * @returns the folded view.
263
352
  */
@@ -271,8 +360,11 @@ export declare function projectEvents(events: readonly SessionEvent[]): Transcri
271
360
  * the inbox claims or cancels them durably (`agent/inbox/spliced` removals,
272
361
  * `user/message` retirement), and an append-only `<Static>` flush cannot
273
362
  * erase a row that vanishes from the view — the retired row would ghost on
274
- * screen until the next source-backed replay. Everything else (including a
275
- * completed tail) is final: later events only APPEND new rows.
363
+ * screen until the next source-backed replay. Running commands join the
364
+ * mutable boundary for the same reason in reverse: `command/done` mutates the
365
+ * row's state/summary, so a flushed row would keep its stale running mark
366
+ * until a resize-triggered replay. Everything else (including a completed
367
+ * tail) is final: later events only APPEND new rows.
276
368
  * @param entries - the view's transcript entries in order.
277
369
  * @returns the count of entries safe to flush (0 for an empty transcript).
278
370
  */