dsh-code 1.0.2 → 1.0.4

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 (53) hide show
  1. package/README.en.md +21 -13
  2. package/README.md +285 -271
  3. package/bin/deepseek.mjs +26 -3
  4. package/lib/index.mjs +2962 -1560
  5. package/lib/types/app.d.ts +13 -2
  6. package/lib/types/commands.d.ts +13 -0
  7. package/lib/types/editor-keys.d.ts +105 -0
  8. package/lib/types/git-workflow.d.ts +6 -2
  9. package/lib/types/index.d.ts +28 -0
  10. package/lib/types/input-split.d.ts +54 -0
  11. package/lib/types/kernel-panels.d.ts +3 -1
  12. package/lib/types/keyboard.d.ts +8 -0
  13. package/lib/types/model-capabilities.d.ts +82 -0
  14. package/lib/types/provider-settings.d.ts +84 -0
  15. package/lib/types/render/lines.d.ts +25 -0
  16. package/lib/types/render/markdown.d.ts +1 -1
  17. package/lib/types/render/projection.d.ts +22 -2
  18. package/lib/types/render/status.d.ts +22 -15
  19. package/lib/types/render/text.d.ts +15 -9
  20. package/lib/types/render/width.d.ts +29 -0
  21. package/lib/types/session-directory.d.ts +27 -0
  22. package/lib/types/settings-file.d.ts +33 -0
  23. package/lib/types/skills.d.ts +1 -1
  24. package/lib/types/store.d.ts +10 -0
  25. package/lib/types/subagents.d.ts +13 -3
  26. package/package.json +159 -159
  27. package/src/app.ts +4514 -3892
  28. package/src/approval.ts +8 -3
  29. package/src/authorization-panel.ts +2 -4
  30. package/src/commands.ts +27 -3
  31. package/src/editor-keys.ts +371 -0
  32. package/src/git-workflow.ts +10 -6
  33. package/src/index.ts +1752 -1523
  34. package/src/input-split.ts +191 -0
  35. package/src/internals.ts +26 -8
  36. package/src/kernel-panels.ts +26 -10
  37. package/src/keyboard.ts +123 -88
  38. package/src/mentions.ts +42 -9
  39. package/src/model-capabilities.ts +318 -0
  40. package/src/provider-settings.ts +220 -0
  41. package/src/questions.ts +20 -0
  42. package/src/render/lines.ts +415 -356
  43. package/src/render/markdown.ts +18 -19
  44. package/src/render/projection.ts +162 -52
  45. package/src/render/status.ts +76 -71
  46. package/src/render/text.ts +158 -150
  47. package/src/render/width.ts +189 -0
  48. package/src/session-directory.ts +56 -0
  49. package/src/settings-file.ts +56 -0
  50. package/src/skills.ts +19 -6
  51. package/src/store.ts +26 -7
  52. package/src/subagents.ts +39 -6
  53. package/src/theme-panel.ts +79 -72
@@ -21,9 +21,9 @@ import { type ThemeName } from './theme.ts';
21
21
  import type { TranscriptStore } from './store.ts';
22
22
  import { type TranscriptEntry } from './render/projection.ts';
23
23
  import type { ApprovalStore } from './approval.ts';
24
- import type { CommandsView } from './commands.ts';
24
+ import { type CommandsView } from './commands.ts';
25
25
  import type { ModelDirectory, ModelRow } from './models.ts';
26
- import type { ProviderConfiguration, ProviderSettingsDirectory, ProviderTargetView } from './provider-settings.ts';
26
+ import { type DiscoveredModelView, type ProviderConfiguration, type ProviderSettingsDirectory, type ProviderTargetView } from './provider-settings.ts';
27
27
  import type { QuestionStore } from './questions.ts';
28
28
  import type { SkillsView, SkillRow } from './skills.ts';
29
29
  import { type MentionCandidate } from './mentions.ts';
@@ -108,6 +108,15 @@ export interface AppProps {
108
108
  removeModelProvider?(target: ProviderTargetView): Promise<void>;
109
109
  /** Save endpoint and explicit model capacities through the provider profile. */
110
110
  saveModelProviderConfiguration?(target: ProviderTargetView, configuration: ProviderConfiguration): Promise<void>;
111
+ /**
112
+ * Interrogate the provider's real endpoint (typed key wins over the stored
113
+ * credential) for the models it actually serves — the discovery stage of
114
+ * the provider setup page.
115
+ */
116
+ discoverModelProvider?(target: ProviderTargetView, request: {
117
+ readonly apiKey?: string;
118
+ readonly baseURL?: string;
119
+ }, signal?: AbortSignal): Promise<readonly DiscoveredModelView[]>;
111
120
  /** Provider authorization flows and value-free stored-record facts. */
112
121
  loadProviderAuthorizations?(): Promise<ProviderAuthorizationDirectory>;
113
122
  subscribeProviderAuthorizations?(listener: () => void): () => void;
@@ -163,6 +172,8 @@ export interface AppProps {
163
172
  recordHistory(text: string): void;
164
173
  /** Cancel one queued inbox message by identity (Delete on the empty composer). */
165
174
  cancelQueued(messageId: string): void;
175
+ /** Apply the Ctrl+R terminal passthrough to the detected editor (/vscode-keys); resolves to a one-line summary. */
176
+ applyEditorKeys(): Promise<string>;
166
177
  }
167
178
  /** One completion candidate row. */
168
179
  interface CompletionCandidate {
@@ -37,3 +37,16 @@ export declare function watchCommands(ctx: Context): CommandsView;
37
37
  * @returns true when the line parses as `/name` or `/name input`.
38
38
  */
39
39
  export declare function isSlashLine(line: string): boolean;
40
+ /**
41
+ * The submission payload for one composer line. Trim is a blank check, not a
42
+ * rewrite: an ordinary prompt keeps its exact leading indentation, inner
43
+ * layout, and trailing spaces (pasted code must reach the model verbatim).
44
+ * Only trailing line terminators are stripped — a draft's final newline is a
45
+ * paste/Enter artifact (an open bracketed paste turns Enter into an inserted
46
+ * newline), never deliberate content. A syntactic slash line still normalizes
47
+ * fully so command routing stays stable (completion inserts a trailing space
48
+ * after `/name`).
49
+ * @param line - the complete draft text.
50
+ * @returns the text to submit verbatim.
51
+ */
52
+ export declare function submissionPayload(line: string): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * VS Code-family terminal keybinding repair. VS Code hands Ctrl+R to the
3
+ * workbench (Open Recent) even while an integrated terminal owns focus, so
4
+ * the reasoning-fold key never reaches the TUI. Workspace-scoped keybindings
5
+ * do not exist, so the fix is one user-level keybindings.json rule that
6
+ * forwards the raw Ctrl byte via sendSequence under terminalFocus. This
7
+ * module detects the hosting editor variant, resolves its user
8
+ * keybindings.json, and merges the rule idempotently; pure merge/detect
9
+ * helpers are separated from the fs orchestration so both stay testable.
10
+ * @module @deepseek-ai/dsh-code/editor-keys
11
+ */
12
+ /** Integrated-terminal editor variants this module can repair. */
13
+ export type EditorTerminalFamily = 'vscode' | 'cursor' | 'vscodium' | 'windsurf';
14
+ /**
15
+ * Detect the editor hosting this integrated terminal.
16
+ * @param env - process environment (TERM_PROGRAM decides; case/whitespace tolerant).
17
+ * @returns the family, or undefined outside VS Code-family terminals.
18
+ */
19
+ export declare function detectEditorTerminalFamily(env?: NodeJS.ProcessEnv): EditorTerminalFamily | undefined;
20
+ /**
21
+ * Whether the pty is hosted away from the editor UI (ssh/container/tunnel).
22
+ * Keybindings live on the client machine, so a remote session must never
23
+ * write them server-side.
24
+ */
25
+ export declare function isRemoteTerminalEnv(env?: NodeJS.ProcessEnv): boolean;
26
+ /** Filesystem anchors used to resolve editor config paths (injectable for tests). */
27
+ export interface EditorPathContext {
28
+ /** User home directory. */
29
+ homedir: string;
30
+ /** %APPDATA% on Windows; only read for win32 resolution. */
31
+ appdata?: string;
32
+ /** Node platform qualifier. */
33
+ platform: NodeJS.Platform;
34
+ }
35
+ /**
36
+ * Resolve the user keybindings.json candidates for one family, most likely
37
+ * install first. Only paths that exist on disk are repaired.
38
+ */
39
+ export declare function editorKeybindingCandidates(family: EditorTerminalFamily, context: EditorPathContext): readonly string[];
40
+ /** The one workbench rule that hands Ctrl+R to the focused terminal. */
41
+ export declare const CTRL_R_PASSTHROUGH_RULE: {
42
+ readonly key: "ctrl+r";
43
+ readonly command: "workbench.action.terminal.sendSequence";
44
+ readonly args: {
45
+ readonly text: "\u0012";
46
+ };
47
+ readonly when: "terminalFocus";
48
+ };
49
+ /**
50
+ * Remove // and block comments from one JSONC document. Double-quoted strings
51
+ * survive untouched, so comment markers inside string values are preserved.
52
+ */
53
+ export declare function stripJsoncComments(text: string): string;
54
+ /** Parse one JSONC document; trailing commas are tolerated. */
55
+ export declare function parseJsonc(text: string): unknown;
56
+ /** Outcome of merging the passthrough rule into one keybindings document. */
57
+ export type KeybindingsMerge = {
58
+ readonly status: 'present';
59
+ } | {
60
+ readonly status: 'updated';
61
+ readonly text: string;
62
+ } | {
63
+ readonly status: 'created';
64
+ readonly text: string;
65
+ };
66
+ /**
67
+ * Merge the Ctrl+R passthrough into one keybindings.json document. The raw
68
+ * text is preserved verbatim (comments included); the rule is inserted right
69
+ * after the array opener so it cannot be shadowed by later conflicting user
70
+ * rules. Missing files resolve to a fresh template.
71
+ * @throws when the document does not carry a rule array.
72
+ */
73
+ export declare function mergeCtrlRPassthrough(raw: string | undefined): KeybindingsMerge;
74
+ /** User-level marker file content: the startup hint fires at most once per install. */
75
+ export interface EditorKeysFlag {
76
+ hintShownAt?: string;
77
+ }
78
+ /** Parse one flag file snapshot; missing or corrupt content degrades to unshown. */
79
+ export declare function parseEditorKeysFlag(raw: string | undefined): EditorKeysFlag;
80
+ /** Persist the shown marker; best-effort, the hint is cosmetic and never a gate. */
81
+ export declare function markEditorKeysHintShown(path: string): Promise<void>;
82
+ /** Inputs shared by the apply and startup-hint flows. */
83
+ export interface EditorKeysEnv {
84
+ /** Process environment (TERM_PROGRAM / VSCODE_IPC_HOOK_CLI). */
85
+ env: NodeJS.ProcessEnv;
86
+ /** Filesystem anchors for editor config resolution. */
87
+ paths: EditorPathContext;
88
+ /** Absolute path of the one-shot hint marker under the DSH home. */
89
+ flagPath: string;
90
+ }
91
+ /**
92
+ * Apply the Ctrl+R passthrough to every local keybindings.json of the hosting
93
+ * editor and mark the startup hint shown. Existing files get a .dsh-bak
94
+ * backup before the first write.
95
+ * @returns a one-line user-facing summary.
96
+ * @throws with an actionable message when the environment cannot be repaired.
97
+ */
98
+ export declare function applyCtrlRPassthrough({ env, paths, flagPath }: EditorKeysEnv): Promise<string>;
99
+ /**
100
+ * Resolve the one-shot startup hint for VS Code-family terminals. Fires at
101
+ * most once per install (flag file), never when the passthrough rule is
102
+ * already present, and never in remote ptys where the repair cannot run.
103
+ * @returns the hint line, or undefined to stay silent.
104
+ */
105
+ export declare function resolveEditorKeysStartupHint({ env, paths, flagPath }: EditorKeysEnv): Promise<string | undefined>;
@@ -17,7 +17,11 @@ export interface GitDiffView {
17
17
  export declare function parseGitDiffFiles(text: string): readonly GitDiffFile[];
18
18
  /** Parse the intentionally small, option-safe /diff argument vocabulary. */
19
19
  export declare function parseGitDiffSpec(argument: string): GitDiffSpec;
20
- /** Load one complete textual diff without invoking external diff drivers. */
21
- export declare function loadGitDiff(cwd: string, argument: string): Promise<GitDiffView>;
20
+ /**
21
+ * Load one complete textual diff without invoking external diff drivers.
22
+ * @param signal - aborted by the caller on session switches/quit, killing the
23
+ * git subprocess instead of letting a stale repository's diff land later.
24
+ */
25
+ export declare function loadGitDiff(cwd: string, argument: string, signal?: AbortSignal): Promise<GitDiffView>;
22
26
  /** Review prompt capped before it reaches a provider context window. */
23
27
  export declare function buildReviewPrompt(diff: string, label: string, maxChars?: number): string;
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { Context } from '@deepseek-ai/cordis';
12
12
  import z from '@deepseek-ai/schemastery';
13
+ import { type ImageBlock } from '@deepseek-ai/dsh-llm';
13
14
  import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session';
14
15
  import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence';
15
16
  import type { TuiStartup } from './startup.ts';
@@ -69,6 +70,33 @@ export interface QuitCleanupStep {
69
70
  * @returns the names of the steps that started, in order (for tests).
70
71
  */
71
72
  export declare function runQuitSequence(steps: readonly QuitCleanupStep[], exit: (code: number) => void, onError?: (name: string, error: unknown) => void): Promise<readonly string[]>;
73
+ /** One composer submission waiting behind the startup delivery. */
74
+ export interface QueuedSubmission {
75
+ readonly text: string;
76
+ readonly mode: 'followup' | 'steer';
77
+ readonly images: readonly ImageBlock[];
78
+ }
79
+ /**
80
+ * Order-preserving gate for composer input while the startup prompt/images
81
+ * are still preparing. Anything submitted before the startup delivery settles
82
+ * queues and flushes afterwards in submit order, so the initial request can
83
+ * never be overtaken by typing that raced a slow image preparation. The flush
84
+ * also runs when the startup delivery fails: user input is never stranded.
85
+ */
86
+ export declare class StartupInputGate {
87
+ private readonly deliver;
88
+ private readonly queued;
89
+ private pending;
90
+ constructor(deliver: (submission: QueuedSubmission) => void);
91
+ /** Submit one line: delivered now while idle, queued behind the startup delivery otherwise. */
92
+ submit(submission: QueuedSubmission): void;
93
+ /**
94
+ * Run the startup delivery — the callback receives the direct-delivery sink
95
+ * for the startup prompt itself — then flush everything that queued behind
96
+ * it, in order, even when the callback rejects.
97
+ */
98
+ run(startup: (deliver: (submission: QueuedSubmission) => void) => Promise<void>): Promise<void>;
99
+ }
72
100
  /**
73
101
  * Resolve the invocation's target session against the persisted headers.
74
102
  * @param startup - the parsed startup flags.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Terminal input arrives as byte chunks, and one chunk can carry several
3
+ * keypresses: a fast space-then-enter, a bridged stdin that batches reads, a
4
+ * middle-click paste. Ink parses each chunk as exactly one keypress —
5
+ * `parseKeypress(' \r')` matches neither member, so both keys silently
6
+ * vanish (a multi-select question answered with an empty set). The splitter
7
+ * below cuts every chunk into the individual keypress units Ink's parser
8
+ * expects, keeping escape sequences and bracketed-paste blocks intact, and
9
+ * the stdin proxy feeds the split stream to the Ink mount.
10
+ *
11
+ * @module @deepseek-ai/dsh-tui/input-split
12
+ */
13
+ import { PassThrough } from 'node:stream';
14
+ /** One keypress cut from the input stream. */
15
+ export interface KeypressSplitter {
16
+ /** Feed one chunk; returns every keypress unit this chunk completed. */
17
+ push(chunk: string): string[];
18
+ /** Whether an unterminated bracketed-paste block is currently held. */
19
+ openPaste(): boolean;
20
+ /**
21
+ * Last-resort escape hatch for a paste whose end marker never arrived:
22
+ * drop the start marker and emit the held bytes as plain keypress units so
23
+ * nothing (Esc and Ctrl+C included) stays hostage. Inert when no paste is
24
+ * open.
25
+ */
26
+ releaseStalePaste(): string[];
27
+ }
28
+ /**
29
+ * Build a stateful chunk splitter. A partial unit at the end of one chunk
30
+ * (a cut CSI sequence, an open paste block) waits in the buffer for the
31
+ * rest. A chunk-trailing lone ESC emits as the Escape key right away:
32
+ * terminals send Escape as its own chunk, and holding it hostage for a
33
+ * sequence that may never continue would break every Esc cancel.
34
+ */
35
+ export declare function createKeypressSplitter(): KeypressSplitter;
36
+ /** The stdin-shaped stream the Ink mount renders through. */
37
+ export interface TuiStdin extends PassThrough {
38
+ /** Mirrors the real stdin so Ink's raw-mode gate passes. */
39
+ isTTY: boolean;
40
+ /** Forwarded to the real stdin; Ink toggles it around focus. */
41
+ setRawMode(value: boolean): unknown;
42
+ ref(): void;
43
+ unref(): void;
44
+ }
45
+ /**
46
+ * Wrap one real stdin in the splitting proxy: keypress units flow into a
47
+ * PassThrough Ink reads, while raw-mode/ref calls forward to the source.
48
+ * @param source - the process (or harness) input stream in raw mode.
49
+ * @returns the proxy stream plus a dispose that detaches the tap.
50
+ */
51
+ export declare function createSplitStdin(source: NodeJS.ReadStream): {
52
+ stdin: TuiStdin;
53
+ dispose(): void;
54
+ };
@@ -115,7 +115,7 @@ export declare function StatuslinePanel({ enabled, change, close }: {
115
115
  * instead of a bare failure notice. Enter applies one level; Esc returns to
116
116
  * the model list without applying.
117
117
  */
118
- export declare function EffortPanel({ row, current, select, back }: {
118
+ export declare function EffortPanel({ row, current, select, back, onExit }: {
119
119
  /** The model row whose advertised levels this stage lists. */
120
120
  row: ModelRow;
121
121
  /** Effective effort currently in force ('' when none), for the ● mark. */
@@ -124,6 +124,8 @@ export declare function EffortPanel({ row, current, select, back }: {
124
124
  select(effortId: string): void;
125
125
  /** Return to the model list without applying. */
126
126
  back(): void;
127
+ /** Leave the whole /model flow (Ctrl+C). */
128
+ onExit(): void;
127
129
  }): ReactElement;
128
130
  /**
129
131
  * The /agents panel (the Codex agent-picker contract, read-only): this
@@ -42,6 +42,14 @@ export declare function stripTerminalFocusEvents(chunk: string, onFocus: (focuse
42
42
  /** Bracketed paste markers as Ink delivers them (it strips the leading ESC). */
43
43
  export declare const PASTE_START_MARKER = "[200~";
44
44
  export declare const PASTE_END_MARKER = "[201~";
45
+ /**
46
+ * How long an unterminated bracketed-paste block may hold buffered bytes
47
+ * before the input splitter strips its start marker and releases them: a
48
+ * terminal that loses the end marker must never take the whole keyboard
49
+ * hostage (Esc/Ctrl+C included). Shared by the splitter and the composer's
50
+ * lost-paste safety net so both use one window.
51
+ */
52
+ export declare const PASTE_BRACKET_TIMEOUT_MS = 1000;
45
53
  /**
46
54
  * Remove bracketed paste markers from one input chunk. Panel drafts accept raw
47
55
  * `input` text, where an unhandled paste would otherwise persist the literal
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Same-id reasoning-capability inheritance for hand-declared pi-ai routes:
3
+ * the model catalog inherits capabilities by route key, not by model id, so a
4
+ * relay route listing `gpt-5.5` reads nothing from the installed `openai`
5
+ * catalog entry and materializes as `reasoning: false` until its settings
6
+ * entry declares `reasoningEfforts`. This adapter closes that gap without
7
+ * touching upstream: whenever a pi-ai profile's model entry carries no
8
+ * declaration and its live row advertises no efforts, but the same model id
9
+ * is declared (in a sibling settings entry) or advertised (on another route)
10
+ * elsewhere, the declaration is materialized into settings — verbatim from a
11
+ * sibling declaration when one exists, otherwise as an identity level map
12
+ * (`off` maps to null, every other level to its own name), which is the
13
+ * correct wire spelling for OpenAI-compatible relays. Writes ride the same
14
+ * `settings.mutate` path as the provider panel, so the upstream
15
+ * `assertServiceable` gate still rejects anything invalid atomically.
16
+ *
17
+ * @module @deepseek-ai/dsh-tui/model-capabilities
18
+ */
19
+ import type { Context } from '@deepseek-ai/cordis';
20
+ import { type ModelRow } from './models.ts';
21
+ /** A notice sink structurally compatible with the app bridge's `notify`. */
22
+ export type CapabilityNotice = (text: string, tone?: 'info' | 'warning' | 'error') => void;
23
+ /** One pi-ai provider profile as stored in settings, addressed for mutation. */
24
+ export interface CapabilityProfileSource {
25
+ /** Settings namespace owning the profile (`llm-pi-ai`). */
26
+ readonly settingsNs: string;
27
+ /** Path from the section root to this provider's profile. */
28
+ readonly settingsPath: readonly string[];
29
+ /** Revision of the owning section at read time. */
30
+ readonly revision: number;
31
+ /** Raw model entries, exactly as stored (declaration fields included). */
32
+ readonly models: readonly Record<string, unknown>[];
33
+ }
34
+ /** One planned per-provider models rewrite. */
35
+ export interface CapabilitySyncPlan {
36
+ /** Provider route the plan targets. */
37
+ readonly provider: string;
38
+ /** Settings namespace owning the profile. */
39
+ readonly settingsNs: string;
40
+ /** Path from the section root to the provider profile. */
41
+ readonly settingsPath: readonly string[];
42
+ /** Raw model entries, exactly as stored (declaration fields included). */
43
+ readonly models: readonly Record<string, unknown>[];
44
+ /** Fingerprint of the source models array this plan was derived from. */
45
+ readonly sourceFingerprint: string;
46
+ /** Document revision of the owning section when the plan was derived. */
47
+ readonly sourceRevision: number;
48
+ /** Model ids that gained a declaration, notice-facing. */
49
+ readonly inherited: readonly string[];
50
+ /** `provider/model` labels the declarations came from, notice-facing. */
51
+ readonly sources: readonly string[];
52
+ }
53
+ /**
54
+ * Plan the reasoning declarations to materialize. A model entry inherits
55
+ * when it declares nothing (`reasoningEfforts` absent — a dict or `false` is
56
+ * an explicit choice and is never touched) and its live row advertises no
57
+ * efforts; the donor is the first sibling settings declaration for the same
58
+ * id, copied verbatim so dialect wire spellings survive, otherwise the first
59
+ * other-route row advertising efforts for that id, mapped by identity.
60
+ * Entries never lose fields and keep their key order; a provider appears in
61
+ * the result only when at least one entry changes.
62
+ * @param input - the live model rows and the raw pi-ai profiles from settings.
63
+ * @returns one plan per provider with at least one inheritance.
64
+ */
65
+ export declare function planCapabilitySync(input: {
66
+ readonly rows: readonly ModelRow[];
67
+ readonly profiles: ReadonlyMap<string, CapabilityProfileSource>;
68
+ }): readonly CapabilitySyncPlan[];
69
+ /**
70
+ * Materialize same-id reasoning declarations once per provider. Reads the
71
+ * configurable directory, the redacted settings document, and the live model
72
+ * rows; plans; then writes each provider's merged models array through
73
+ * `settings.mutate` under a fresh revision (writes bump the section
74
+ * revision, so per-plan revisions are re-read). Every failure converges to a
75
+ * single-line notice — the caller's promise never rejects and the session
76
+ * keeps running on the previous configuration.
77
+ * @param ctx - context carrying the `llm` and `settings` services (optional).
78
+ * @param notify - the app bridge's notice sink, when one is live.
79
+ */
80
+ export declare function syncModelCapabilities(ctx: Context, notify?: CapabilityNotice): Promise<void>;
81
+ /** Test seam: forget the applied-write fingerprints. */
82
+ export declare function resetCapabilitySyncState(): void;
@@ -48,12 +48,78 @@ export interface ProviderModelSettings {
48
48
  readonly name?: string;
49
49
  readonly contextWindow?: number;
50
50
  readonly maxTokens?: number;
51
+ /**
52
+ * Remaining entry fields the editor does not model (`reasoningEfforts`,
53
+ * `compat`, `input`, …), carried verbatim so a save preserves them.
54
+ * Populated by {@link loadProviderSettings}; never contains the four
55
+ * modelled keys.
56
+ */
57
+ readonly extras?: Readonly<Record<string, unknown>>;
51
58
  }
52
59
  /** The small, portable subset of a provider profile the terminal edits. */
53
60
  export interface ProviderConfiguration {
54
61
  readonly baseURL?: string;
62
+ /**
63
+ * Wire protocol the stored profile names (e.g. `openai-responses`), when it
64
+ * names one. Load-only: the editor never writes it, but endpoint discovery
65
+ * passes it so the listing speaks the same protocol as real requests.
66
+ */
67
+ readonly api?: string;
55
68
  readonly models: readonly ProviderModelSettings[];
56
69
  }
70
+ /** One model an endpoint reported about itself (mirrors `LlmDiscoveredModel`). */
71
+ export interface DiscoveredModelView {
72
+ /** Model id the endpoint accepts. */
73
+ readonly id: string;
74
+ /** Human-readable name when the endpoint supplies one. */
75
+ readonly name?: string;
76
+ /** Context window when disclosed; adoption still owes it if absent. */
77
+ readonly contextWindow?: number;
78
+ /** Output cap when disclosed. */
79
+ readonly maxTokens?: number;
80
+ }
81
+ /** One model an endpoint reported about itself (mirrors LlmDiscoveredModel). */
82
+ export interface DiscoveredModelView {
83
+ /** Model id the endpoint accepts. */
84
+ readonly id: string;
85
+ /** Human-readable name when the endpoint supplies one. */
86
+ readonly name?: string;
87
+ /** Context window when disclosed; adoption still owes it if absent. */
88
+ readonly contextWindow?: number;
89
+ /** Output cap when disclosed. */
90
+ readonly maxTokens?: number;
91
+ }
92
+ /**
93
+ * The seven canonical reasoning levels a reasoningEfforts key may name -
94
+ * pi-ai's THINKING_LEVELS. A pi-ai upgrade that adds or removes one fails
95
+ * upstream's own drift gate; this mirror exists so the terminal editor can
96
+ * validate drafts without importing the pi-ai package.
97
+ */
98
+ export declare const REASONING_EFFORT_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
99
+ /**
100
+ * One stored reasoningEfforts declaration: a display-level to wire-value map
101
+ * (null sends no reasoning parameter), an explicit false disabling the
102
+ * picker, or undefined leaving the entry to inherit.
103
+ */
104
+ export type ReasoningEffortsValue = Record<string, string | null> | false | undefined;
105
+ /** Whether a raw extras value is a declared efforts dict (non-empty, non-false). */
106
+ export declare function isDeclaredReasoningEfforts(value: unknown): value is Record<string, string | null>;
107
+ /**
108
+ * Parse the setup page's compact efforts draft into a storable declaration.
109
+ * Grammar: empty = clear back to inherit; the single token "false" = disable
110
+ * the picker; otherwise space-separated level:wire pairs where level is one
111
+ * of REASONING_EFFORT_LEVELS and wire is any non-empty string or the literal
112
+ * "null" (send no parameter).
113
+ */
114
+ export declare function parseReasoningEffortsDraft(draft: string): {
115
+ readonly ok: true;
116
+ readonly value: ReasoningEffortsValue;
117
+ } | {
118
+ readonly ok: false;
119
+ readonly error: string;
120
+ };
121
+ /** Serialize a stored declaration back to the compact draft form (stored key order preserved). */
122
+ export declare function serializeReasoningEfforts(value: unknown): string;
57
123
  /**
58
124
  * One provider row in the TUI provider-management panel: the configurable
59
125
  * directory entry joined with its settings profile and credential facts.
@@ -136,6 +202,24 @@ export declare function loadProviderSettings(ctx: Context): Promise<ProviderSett
136
202
  export declare function saveProviderCredential(ctx: Context, target: ProviderTargetView, rawKey: string): Promise<void>;
137
203
  /** Save the endpoint and an explicit model allow-list without rebuilding the profile. */
138
204
  export declare function saveProviderConfiguration(ctx: Context, target: ProviderTargetView, configuration: ProviderConfiguration): Promise<void>;
205
+ /**
206
+ * Interrogate a provider endpoint for the models it really serves, through
207
+ * the model-discovery capability the provider's settings namespace
208
+ * registered — the same pipe the official Web Models page uses. The request
209
+ * is a draft: a typed key forces direct endpoint interrogation (gateway
210
+ * truth), while an empty key lets the harness resolve the route's stored
211
+ * credential; with neither baseURL nor route the adapter answers from its
212
+ * own knowledge.
213
+ * @param ctx - context carrying the `llm` service (optional discovery).
214
+ * @param target - provider row whose settings namespace serves the draft.
215
+ * @param request - typed key and/or endpoint override for this one probe.
216
+ * @param signal - caller cancellation (panel navigation aborts the probe).
217
+ * @returns the advertised models in endpoint order, deduplicated.
218
+ */
219
+ export declare function discoverProviderModels(ctx: Context, target: ProviderTargetView, request: {
220
+ readonly apiKey?: string;
221
+ readonly baseURL?: string;
222
+ }, signal?: AbortSignal): Promise<readonly DiscoveredModelView[]>;
139
223
  /**
140
224
  * Remove the currently named credential without touching the provider
141
225
  * profile. Only the resolved profile's own reference is unset; a dormant or
@@ -39,3 +39,28 @@ export declare function reasoningLines(text: string, columns: number): readonly
39
39
  export declare function transcriptEntryLines(entry: TranscriptEntry, columns: number, showReasoning?: boolean, reasoningToggleHint?: boolean, showToolDetails?: boolean): readonly StyledLine[];
40
40
  /** Settled-history variant carrying the Ctrl+R reasoning fold. */
41
41
  export declare function settledEntryLines(entry: TranscriptEntry, columns: number, showReasoning: boolean): readonly StyledLine[];
42
+ /** The flexible rows of the live region; chrome (composer/notice/status) is never reduced. */
43
+ export interface LiveAllocation {
44
+ /** Settled tail rows currently rendered in the live tree. */
45
+ readonly live: number;
46
+ /** Rows reserved for the streaming reasoning tail or its marker. */
47
+ readonly reasoning: number;
48
+ /** Rows reserved for the streaming answer tail. */
49
+ readonly answer: number;
50
+ }
51
+ /** A clamped allocation plus the invariant-trip warning that triggered it. */
52
+ export interface LiveAllocationAudit {
53
+ readonly allocation: LiveAllocation;
54
+ readonly warning?: string;
55
+ }
56
+ /**
57
+ * Clamp the live-region allocation so the flexible dynamic rows never exceed
58
+ * the post-chrome budget. By construction the caller derives these rows from
59
+ * the same budget; this is the runtime tripwire for a future edit that breaks
60
+ * that derivation. Reduction order: answer first (the freshest content is the
61
+ * live tail), then reasoning, then settled live rows; nothing goes negative.
62
+ * @param allocation - the intended row allocation.
63
+ * @param dynamicRows - the post-chrome row budget.
64
+ * @returns the clamped allocation and a warning string when clamping fired.
65
+ */
66
+ export declare function clampLiveAllocation(allocation: LiveAllocation, dynamicRows: number): LiveAllocationAudit;
@@ -21,7 +21,7 @@ export interface MdSegment {
21
21
  export interface MdLine {
22
22
  segments: readonly MdSegment[];
23
23
  }
24
- /** Visible width of a run in columns (CJK counts double). */
24
+ /** Visible width of a run in columns (grapheme-cluster and emoji aware). */
25
25
  export declare function visibleColumns(text: string): number;
26
26
  /** Render markdown text into styled lines of at most `width` columns. */
27
27
  export declare function renderMarkdown(text: string, width: number, options?: {
@@ -115,6 +115,8 @@ export interface RetryEntry {
115
115
  kind: 'retry';
116
116
  /** Correlation id shared with the matching `llm/retry-started`. */
117
117
  retryId: string;
118
+ /** Retry policy mode from the event: `always` has no attempt cap. */
119
+ mode: 'normal' | 'always';
118
120
  /** Attempt ordinal and its cap. */
119
121
  attempt: number;
120
122
  max: number;
@@ -122,7 +124,11 @@ export interface RetryEntry {
122
124
  code: string;
123
125
  /** Backoff wait before the next attempt, in ms. */
124
126
  delayMs: number;
125
- /** `running` while the backoff waits, `done` once the attempt started. */
127
+ /**
128
+ * `running` while the backoff waits, `done` once the attempt started — or
129
+ * when the turn ended first (the turn-end sweep finalizes orphans so they
130
+ * never pin the settled boundary).
131
+ */
126
132
  state: 'running' | 'done';
127
133
  }
128
134
  /** Turn-tail deliverables: files mutated by the turn's diff-bearing tools. */
@@ -357,8 +363,11 @@ export declare function createReplayAccumulator(): ReplayAccumulator;
357
363
  * to a sequential fold; only the `entries` container operations are mutable.
358
364
  *
359
365
  * @internal Test-instrumentation path; `projectEvents` is the public entry.
366
+ * @returns whether the event changed the accumulated state — the live store
367
+ * stays silent and keeps its snapshot identity for ignored events, exactly
368
+ * like the copy-on-write reducer returning its input view unchanged.
360
369
  */
361
- export declare function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent): void;
370
+ export declare function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent): boolean;
362
371
  /**
363
372
  * Materialize the accumulated fold as a `TranscriptView`, compacting any
364
373
  * retired tombstones. The anchors maps are handed through as-is (their
@@ -367,6 +376,17 @@ export declare function replayProjectEvent(acc: ReplayAccumulator, event: Sessio
367
376
  * @internal Test-instrumentation path; `projectEvents` is the public entry.
368
377
  */
369
378
  export declare function finishReplay(acc: ReplayAccumulator): TranscriptView;
379
+ /**
380
+ * Materialize the accumulated fold as a fresh immutable snapshot for the
381
+ * live store. Unlike {@link finishReplay} — the one-shot replay entry, which
382
+ * hands the accumulator's own arrays through because the accumulator is
383
+ * discarded — every array a renderer can hold is copied here, so later
384
+ * folds never mutate a snapshot already handed out. Same fields, same
385
+ * tombstone compaction.
386
+ *
387
+ * @internal Live-store path; `projectEvents` is the public entry.
388
+ */
389
+ export declare function snapshotReplayView(acc: ReplayAccumulator): TranscriptView;
370
390
  /**
371
391
  * Fold a replayed event history into one view.
372
392
  *
@@ -72,28 +72,35 @@ export declare const STATUS_ITEM_SEPARATOR = " \u00B7 ";
72
72
  /** The Codex-style mode cycle hint appended to the permission badge. */
73
73
  export declare const STATUS_CYCLE_HINT = " (shift+tab to cycle)";
74
74
  /**
75
- * Interior columns of the segmented context bar (content-type segments plus
76
- * the free tail whose right edge carries the usage readout). The layout
77
- * starts every bar at this width so the drop ladder can pre-measure the
78
- * group, then shrinks the bar inside a tighter budget before dropping it
79
- * (see CONTEXT_MIN_WIDTH) rather than asking the layout for more room.
75
+ * Interior columns of the context bar. The layout starts every bar at this
76
+ * width so the drop ladder can pre-measure the group, then degrades the
77
+ * readout and shrinks the bar inside a tighter budget before dropping the
78
+ * group (see CONTEXT_MIN_WIDTH) rather than asking the layout for more room.
80
79
  */
81
80
  export declare const CONTEXT_BAR_WIDTH = 24;
82
81
  /**
83
- * Render context occupancy as ONE stepless bar: a solid DeepSeek-blue fill
84
- * run, a dim dotted free track, and the usage readout riding the track's
85
- * right edge (`12.3K/1.0M 25%`, shrinking to the bare percent as the track
86
- * narrows). No per-content-type segmentation. Column split is deterministic:
87
- * the free share is `Math.round(free/window*width)` clamped to at least
88
- * CONTEXT_MIN_FREE columns and at most the full width; the fill takes every
89
- * remaining column, so a given occupancy always renders the identical bar.
90
- * The readout flips to amber once occupancy reaches the warning threshold.
91
- * @param usedTokens - reported used tokens (drives the readout and percent).
82
+ * Render context occupancy as ONE stepless proportional bar: a solid
83
+ * DeepSeek-blue fill run tracking the occupancy and a dim dotted free
84
+ * track for the rest. Nothing else lives inside the bar the usage
85
+ * readout rides outside it (see contextGroupSpans) so the geometry
86
+ * always reads as the true remaining share. A given occupancy always
87
+ * renders the identical bar.
88
+ * @param usedTokens - reported used tokens.
92
89
  * @param contextWindow - route capacity.
93
- * @param width - total bar interior columns.
90
+ * @param width - total bar columns.
94
91
  * @returns tone-split spans for the footer to paint.
95
92
  */
96
93
  export declare function contextBar(usedTokens: number, contextWindow: number, width: number): readonly StatusSpan[];
94
+ /** How much usage detail the context group's readout carries. */
95
+ export type ContextReadoutMode = 'full' | 'percent' | 'none';
96
+ /**
97
+ * Compose the context group: the proportional bar plus the usage readout
98
+ * OUTSIDE the bar, so the dotted track keeps its proportional meaning no
99
+ * matter how wide the readout is. `full` reads `12.3K/1.0M 25%`; `percent`
100
+ * drops the absolute pair; `none` is the bare bar. The readout turns amber
101
+ * once occupancy reaches the warning threshold.
102
+ */
103
+ export declare function contextGroupSpans(usedTokens: number, contextWindow: number, barWidth: number, readout: ContextReadoutMode): readonly StatusSpan[];
97
104
  /**
98
105
  * One customizable status item (the Codex /statusline picker contract).
99
106
  * 'left' items render as pipe-separated clusters after the identity dot;