dsh-code 1.0.3 → 1.0.5

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 (45) hide show
  1. package/README.md +293 -285
  2. package/bin/deepseek.mjs +245 -12
  3. package/cordis.patch.yml +12 -14
  4. package/lib/index.mjs +1939 -903
  5. package/lib/types/app.d.ts +11 -2
  6. package/lib/types/commands.d.ts +13 -0
  7. package/lib/types/git-workflow.d.ts +7 -2
  8. package/lib/types/history.d.ts +18 -11
  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/presets.d.ts +4 -1
  14. package/lib/types/provider-settings.d.ts +77 -0
  15. package/lib/types/questions.d.ts +16 -12
  16. package/lib/types/render/projection.d.ts +9 -2
  17. package/lib/types/render/status.d.ts +22 -15
  18. package/lib/types/settings-file.d.ts +8 -0
  19. package/lib/types/skills.d.ts +1 -1
  20. package/package.json +49 -46
  21. package/src/app.ts +5459 -4900
  22. package/src/approval.ts +8 -3
  23. package/src/authorization-panel.ts +2 -4
  24. package/src/commands.ts +27 -3
  25. package/src/git-workflow.ts +29 -10
  26. package/src/history.ts +22 -13
  27. package/src/index.ts +203 -61
  28. package/src/input-split.ts +191 -0
  29. package/src/internals.ts +26 -8
  30. package/src/kernel-panels.ts +26 -10
  31. package/src/keyboard.ts +123 -88
  32. package/src/mentions.ts +42 -9
  33. package/src/permissions.ts +1 -1
  34. package/src/presets.ts +19 -6
  35. package/src/provider-settings.ts +204 -0
  36. package/src/questions.ts +58 -55
  37. package/src/render/export.ts +7 -7
  38. package/src/render/lines.ts +24 -12
  39. package/src/render/markdown.ts +15 -13
  40. package/src/render/projection.ts +101 -13
  41. package/src/render/status.ts +76 -71
  42. package/src/render/text.ts +9 -3
  43. package/src/settings-file.ts +38 -6
  44. package/src/skills.ts +19 -6
  45. 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;
@@ -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;
@@ -1,4 +1,9 @@
1
- /** Read-only Git inspection used by /diff and /review. */
1
+ /**
2
+ * Read-only Git inspection used by /diff and /review. Every diff
3
+ * invocation carries --no-ext-diff and --no-textconv, so configured
4
+ * external diff drivers and text converters can never execute as a
5
+ * side effect of reading a diff.
6
+ */
2
7
  export interface GitDiffSpec {
3
8
  readonly label: string;
4
9
  readonly args: readonly string[];
@@ -18,7 +23,7 @@ export declare function parseGitDiffFiles(text: string): readonly GitDiffFile[];
18
23
  /** Parse the intentionally small, option-safe /diff argument vocabulary. */
19
24
  export declare function parseGitDiffSpec(argument: string): GitDiffSpec;
20
25
  /**
21
- * Load one complete textual diff without invoking external diff drivers.
26
+ * Load one complete textual diff without invoking external programs.
22
27
  * @param signal - aborted by the caller on session switches/quit, killing the
23
28
  * git subprocess instead of letting a stale repository's diff land later.
24
29
  */
@@ -21,14 +21,21 @@ export declare function serializeHistoryEntry(text: string): string;
21
21
  */
22
22
  export declare function parseHistoryFile(raw: string, max?: number): readonly string[];
23
23
  /**
24
- * Append one entry to the persistent file content: JSON line, capped to the
25
- * newest `max` entries with a trailing newline.
26
- * @param current - existing file content.
27
- * @param text - submission to persist.
28
- * @param max - entry cap.
29
- * @returns the new file content.
24
+ * The append unit for the persistent file: one JSON line, so a multi-line
25
+ * draft still occupies exactly one physical line. Each submission appends
26
+ * this unit at the end of the file, so concurrent terminals add entries
27
+ * after each other. Node chunks one append at 512 KiB: a pasted entry
28
+ * beyond that size could interleave mid-line with another writer's
29
+ * chunks, and the damaged line then drops out at the next parse —
30
+ * recall tolerates the loss by design.
31
+ */
32
+ export declare function historyLine(text: string): string;
33
+ /**
34
+ * Whether the file on disk differs from its canonical form (deduped and
35
+ * capped). True means stale lines have accumulated and the next boot
36
+ * should rewrite it once, atomically.
30
37
  */
31
- export declare function appendHistoryContent(current: string, text: string, max?: number): string;
38
+ export declare function needsCompaction(raw: string, max?: number): boolean;
32
39
  /**
33
40
  * Record one in-session submission: empty text is ignored and an adjacent
34
41
  * duplicate collapses (Codex `record_local_submission` semantics). The local
@@ -40,10 +47,10 @@ export declare function appendHistoryContent(current: string, text: string, max?
40
47
  */
41
48
  export declare function recordLocalEntry(local: readonly string[], text: string, max?: number): readonly string[];
42
49
  /**
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).
50
+ * Serialize a capped entry list to the history file format (one JSON line
51
+ * per entry, trailing newline). The boot-time compaction writes this
52
+ * canonical form once when stale lines have accumulated; submissions
53
+ * themselves only ever append a single line.
47
54
  * @param entries - the entries to persist, oldest first.
48
55
  * @returns the file content, '' for an empty list.
49
56
  */
@@ -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
@@ -11,8 +11,11 @@ export type AgentPresetsService = AgentPresets;
11
11
  export declare function agentPresetsFrom(ctx: Context): AgentPresetsService | undefined;
12
12
  /** A preset may change only before the first durable turn begins. */
13
13
  export declare function isBlankSession(events: readonly SessionEvent[]): boolean;
14
+ /** Translate a preset id recorded before an upstream rename to its current id. */
15
+ export declare function normalizePresetId(id: string): string;
16
+ export declare function normalizePresetId(id: string | undefined): string | undefined;
14
17
  /** Latest logged selection wins; legacy sessions deliberately fall back to standard. */
15
- export declare function resolvePreset(session: Pick<Session, 'header' | 'events'>): string;
18
+ export declare function resolvePreset(session: Pick<Session, 'header' | 'snapshotEvents'>): string;
16
19
  /** Resolve a pre-session choice, or recompose an active blank Agent. */
17
20
  export declare function selectPreset(service: AgentPresetsService, agent: Agent | undefined, presetId: string): Promise<PresetRow>;
18
21
  /** Recompose atomically from the caller's perspective, logging only success. */
@@ -59,8 +59,67 @@ export interface ProviderModelSettings {
59
59
  /** The small, portable subset of a provider profile the terminal edits. */
60
60
  export interface ProviderConfiguration {
61
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;
62
68
  readonly models: readonly ProviderModelSettings[];
63
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;
64
123
  /**
65
124
  * One provider row in the TUI provider-management panel: the configurable
66
125
  * directory entry joined with its settings profile and credential facts.
@@ -143,6 +202,24 @@ export declare function loadProviderSettings(ctx: Context): Promise<ProviderSett
143
202
  export declare function saveProviderCredential(ctx: Context, target: ProviderTargetView, rawKey: string): Promise<void>;
144
203
  /** Save the endpoint and an explicit model allow-list without rebuilding the profile. */
145
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[]>;
146
223
  /**
147
224
  * Remove the currently named credential without touching the provider
148
225
  * profile. Only the resolved profile's own reference is unset; a dormant or
@@ -1,18 +1,19 @@
1
1
  /**
2
- * The terminal ask_user_question provider: registers the single UI provider
3
- * on `ctx.userQuestions` and drives it with a FIFO queue — one question
4
- * request on screen at a time, everything else waiting — then resolves the
5
- * collected answers back into the tool's promise. The community TUI proved
6
- * this exact pipeline shape; here the dialog is an Ink bar instead of a
7
- * pi-tui inline modal.
2
+ * The terminal ask_user_question answerer: one `user-questions/request`
3
+ * waterfall listener that drives a FIFO queue — one question request on
4
+ * screen at a time, everything else waiting — then resolves the collected
5
+ * answers back into the waterfall. Mirrors the approval answerer's claim/
6
+ * defer split: only agents this TUI owns are answered, every other request
7
+ * falls through to the next answerer.
8
8
  *
9
- * Plan reviews (`exit_plan_mode`) arrive through the same service with an
9
+ * Plan reviews (`exit_plan_mode`) arrive through the same waterfall with an
10
10
  * `intent: { kind: 'plan-review' }` — the renderer highlights the approve
11
11
  * option; the answer encoding is identical either way.
12
12
  *
13
13
  * @module @deepseek-ai/dsh-code/questions
14
14
  */
15
15
  import type { Context } from '@deepseek-ai/cordis';
16
+ import type { Agent } from '@deepseek-ai/dsh-agent';
16
17
  import { type AskUserQuestionAnswer, type AskUserQuestionRequest } from '@deepseek-ai/dsh-user-questions';
17
18
  /** One question request waiting on the human, with its settle channels. */
18
19
  export interface PendingQuestion {
@@ -42,9 +43,12 @@ export interface QuestionStore {
42
43
  cancel(pending: PendingQuestion): void;
43
44
  }
44
45
  /**
45
- * Mount the single `ctx.userQuestions` UI provider over a FIFO queue.
46
- * @param ctx - context carrying the `userQuestions` service (dsh-base).
47
- * @returns the store the renderer subscribes to; a context without the
48
- * service yields a permanently empty store.
46
+ * Mount the `user-questions/request` answerer over a FIFO queue.
47
+ * @param ctx - plugin context whose event bus carries the waterfall.
48
+ * @param owns - agents this terminal answers for; every other request is
49
+ * deferred back into the waterfall (`next()`), so sibling answerers stay
50
+ * usable. Agent-less asks are claimed: this TUI is the only human surface
51
+ * in the process.
52
+ * @returns the store the renderer subscribes to.
49
53
  */
50
- export declare function mountQuestionProvider(ctx: Context): QuestionStore;
54
+ export declare function mountQuestionProvider(ctx: Context, owns: (agent: Agent) => boolean): QuestionStore;
@@ -7,7 +7,8 @@
7
7
  * @module @deepseek-ai/dsh-tui/render/projection
8
8
  */
9
9
  import { type ImageBlock, type MessageId } from '@deepseek-ai/dsh-llm';
10
- import type { SessionEvent, TodoItem } from '@deepseek-ai/dsh-session';
10
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
11
+ import type { TodoItem } from '@deepseek-ai/dsh-tool-todo';
11
12
  import { type ToolDetail } from './tool-detail.ts';
12
13
  /** One user prompt line. */
13
14
  export interface UserEntry {
@@ -115,6 +116,8 @@ export interface RetryEntry {
115
116
  kind: 'retry';
116
117
  /** Correlation id shared with the matching `llm/retry-started`. */
117
118
  retryId: string;
119
+ /** Retry policy mode from the event: `always` has no attempt cap. */
120
+ mode: 'normal' | 'always';
118
121
  /** Attempt ordinal and its cap. */
119
122
  attempt: number;
120
123
  max: number;
@@ -122,7 +125,11 @@ export interface RetryEntry {
122
125
  code: string;
123
126
  /** Backoff wait before the next attempt, in ms. */
124
127
  delayMs: number;
125
- /** `running` while the backoff waits, `done` once the attempt started. */
128
+ /**
129
+ * `running` while the backoff waits, `done` once the attempt started — or
130
+ * when the turn ended first (the turn-end sweep finalizes orphans so they
131
+ * never pin the settled boundary).
132
+ */
126
133
  state: 'running' | 'done';
127
134
  }
128
135
  /** Turn-tail deliverables: files mutated by the turn's diff-bearing tools. */
@@ -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;
@@ -15,6 +15,14 @@
15
15
  *
16
16
  * @module @deepseek-ai/dsh-code/settings-file
17
17
  */
18
+ /**
19
+ * Write one file atomically: create the parent directory, write to a
20
+ * uniquely named temp file, and rename it into place. A crash midway
21
+ * can never leave a half-written document behind. Unique temp names
22
+ * keep concurrent writers (two terminals, two chains in one process)
23
+ * from sharing one temp path.
24
+ */
25
+ export declare function writeFileAtomically(path: string, text: string): Promise<void>;
18
26
  /** The serialized persistence surface; flush() is handed to the quit sequence. */
19
27
  export interface UserSettingsPersistence {
20
28
  /**
@@ -43,5 +43,5 @@ interface SkillsWatch extends SkillsView {
43
43
  * @param ctx - context carrying the `skills` service (optional).
44
44
  * @returns the view the completion menu subscribes to.
45
45
  */
46
- export declare function watchSkills(ctx: Context): SkillsWatch;
46
+ export declare function watchSkills(ctx: Context, fallbackCwd?: string): SkillsWatch;
47
47
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-code",
3
3
  "description": "DeepSeek Harness CLI core bundle: interactive coding terminal, durable sessions, and model management for dsh --profile cli",
4
- "version": "1.0.3",
4
+ "version": "1.0.5",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "deepseek": "./bin/deepseek.mjs",
@@ -63,8 +63,9 @@
63
63
  "node": "^22.19 || >=24"
64
64
  },
65
65
  "dependencies": {
66
- "@deepseek-ai/dsh-authorization": "0.1.1-rc.2",
67
- "@deepseek-ai/dsh-web-fetch-http": "0.1.1-rc.2",
66
+ "@deepseek-ai/dsh-authorization": "0.1.2-rc.1",
67
+ "@deepseek-ai/dsh-web-fetch-http": "0.1.2-rc.1",
68
+ "@deepseek-ai/dsh-util-values": "0.1.2-rc.1",
68
69
  "@deepseek-ai/schemastery": "^3.18.1",
69
70
  "chalk": "^5.6.2",
70
71
  "commander": "^15.0.0",
@@ -74,32 +75,32 @@
74
75
  "peerDependencies": {
75
76
  "@deepseek-ai/cordis": "^4.0.1",
76
77
  "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
77
- "@deepseek-ai/dsh-attachment": "0.1.1-rc.2",
78
- "@deepseek-ai/dsh-agent": "0.1.1-rc.2",
79
- "@deepseek-ai/dsh-agent-default-model": "0.1.1-rc.2",
80
- "@deepseek-ai/dsh-agent-presets": "0.1.1-rc.2",
81
- "@deepseek-ai/dsh-cmdline": "0.1.1-rc.2",
82
- "@deepseek-ai/dsh-code-runtime-worker-thread": "0.1.1-rc.2",
83
- "@deepseek-ai/dsh-commands": "0.1.1-rc.2",
84
- "@deepseek-ai/dsh-compaction": "0.1.1-rc.2",
85
- "@deepseek-ai/dsh-cordis-host-runner": "0.1.1-rc.2",
86
- "@deepseek-ai/dsh-credentials": "0.1.1-rc.2",
87
- "@deepseek-ai/dsh-file-reference-local": "0.1.1-rc.2",
88
- "@deepseek-ai/dsh-goal": "0.1.1-rc.2",
89
- "@deepseek-ai/dsh-invariants": "0.1.1-rc.2",
90
- "@deepseek-ai/dsh-jobs": "0.1.1-rc.2",
91
- "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
92
- "@deepseek-ai/dsh-llm-retry": "0.1.1-rc.2",
93
- "@deepseek-ai/dsh-permission-presets": "0.1.1-rc.2",
94
- "@deepseek-ai/dsh-plan-mode": "0.1.1-rc.2",
95
- "@deepseek-ai/dsh-sandbox-policy": "0.1.1-rc.2",
96
- "@deepseek-ai/dsh-session": "0.1.1-rc.2",
97
- "@deepseek-ai/dsh-session-persistence": "0.1.1-rc.2",
98
- "@deepseek-ai/dsh-session-reference": "0.1.1-rc.2",
99
- "@deepseek-ai/dsh-session-title": "0.1.1-rc.2",
100
- "@deepseek-ai/dsh-skill": "0.1.1-rc.2",
101
- "@deepseek-ai/dsh-user-approval": "0.1.1-rc.2",
102
- "@deepseek-ai/dsh-user-questions": "0.1.1-rc.2"
78
+ "@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
79
+ "@deepseek-ai/dsh-agent": "0.1.2-rc.1",
80
+ "@deepseek-ai/dsh-agent-default-model": "0.1.2-rc.1",
81
+ "@deepseek-ai/dsh-agent-presets": "0.1.2-rc.1",
82
+ "@deepseek-ai/dsh-cmdline": "0.1.2-rc.1",
83
+ "@deepseek-ai/dsh-code-runtime-worker-thread": "0.1.2-rc.1",
84
+ "@deepseek-ai/dsh-commands": "0.1.2-rc.1",
85
+ "@deepseek-ai/dsh-compaction": "0.1.2-rc.1",
86
+ "@deepseek-ai/dsh-cordis-host-runner": "0.1.2-rc.1",
87
+ "@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
88
+ "@deepseek-ai/dsh-file-reference-local": "0.1.2-rc.1",
89
+ "@deepseek-ai/dsh-goal": "0.1.2-rc.1",
90
+ "@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
91
+ "@deepseek-ai/dsh-jobs": "0.1.2-rc.1",
92
+ "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
93
+ "@deepseek-ai/dsh-llm-retry": "0.1.2-rc.1",
94
+ "@deepseek-ai/dsh-permission-presets": "0.1.2-rc.1",
95
+ "@deepseek-ai/dsh-plan-mode": "0.1.2-rc.1",
96
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.2-rc.1",
97
+ "@deepseek-ai/dsh-session": "0.1.2-rc.1",
98
+ "@deepseek-ai/dsh-session-persistence": "0.1.2-rc.1",
99
+ "@deepseek-ai/dsh-session-reference": "0.1.2-rc.1",
100
+ "@deepseek-ai/dsh-session-title": "0.1.2-rc.1",
101
+ "@deepseek-ai/dsh-skill": "0.1.2-rc.1",
102
+ "@deepseek-ai/dsh-user-approval": "0.1.2-rc.1",
103
+ "@deepseek-ai/dsh-user-questions": "0.1.2-rc.1"
103
104
  },
104
105
  "peerDependenciesMeta": {
105
106
  "@deepseek-ai/cordis": { "optional": true },
@@ -132,23 +133,25 @@
132
133
  "@deepseek-ai/dsh-user-questions": { "optional": true }
133
134
  },
134
135
  "devDependencies": {
135
- "@deepseek-ai/dsh-agent-presets": "0.1.1-rc.2",
136
- "@deepseek-ai/dsh-attachment": "0.1.1-rc.2",
137
- "@deepseek-ai/dsh-compaction": "0.1.1-rc.2",
138
- "@deepseek-ai/dsh-commands": "0.1.1-rc.2",
139
- "@deepseek-ai/dsh-credentials": "0.1.1-rc.2",
140
- "@deepseek-ai/dsh-goal": "0.1.1-rc.2",
141
- "@deepseek-ai/dsh-jobs": "0.1.1-rc.2",
142
- "@deepseek-ai/dsh-llm-retry": "0.1.1-rc.2",
143
- "@deepseek-ai/dsh-permission-presets": "0.1.1-rc.2",
144
- "@deepseek-ai/dsh-plan-mode": "0.1.1-rc.2",
145
- "@deepseek-ai/dsh-sandbox-policy": "0.1.1-rc.2",
146
- "@deepseek-ai/dsh-session-persistence": "0.1.1-rc.2",
147
- "@deepseek-ai/dsh-session-reference": "0.1.1-rc.2",
148
- "@deepseek-ai/dsh-session-title": "0.1.1-rc.2",
149
- "@deepseek-ai/dsh-skill": "0.1.1-rc.2",
150
- "@deepseek-ai/dsh-user-approval": "0.1.1-rc.2",
151
- "@deepseek-ai/dsh-user-questions": "0.1.1-rc.2",
136
+ "@deepseek-ai/dsh-agent-presets": "0.1.2-rc.1",
137
+ "@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
138
+ "@deepseek-ai/dsh-compaction": "0.1.2-rc.1",
139
+ "@deepseek-ai/dsh-commands": "0.1.2-rc.1",
140
+ "@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
141
+ "@deepseek-ai/dsh-settings": "0.1.2-rc.1",
142
+ "@deepseek-ai/dsh-tool-todo": "0.1.2-rc.1",
143
+ "@deepseek-ai/dsh-goal": "0.1.2-rc.1",
144
+ "@deepseek-ai/dsh-jobs": "0.1.2-rc.1",
145
+ "@deepseek-ai/dsh-llm-retry": "0.1.2-rc.1",
146
+ "@deepseek-ai/dsh-permission-presets": "0.1.2-rc.1",
147
+ "@deepseek-ai/dsh-plan-mode": "0.1.2-rc.1",
148
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.2-rc.1",
149
+ "@deepseek-ai/dsh-session-persistence": "0.1.2-rc.1",
150
+ "@deepseek-ai/dsh-session-reference": "0.1.2-rc.1",
151
+ "@deepseek-ai/dsh-session-title": "0.1.2-rc.1",
152
+ "@deepseek-ai/dsh-skill": "0.1.2-rc.1",
153
+ "@deepseek-ai/dsh-user-approval": "0.1.2-rc.1",
154
+ "@deepseek-ai/dsh-user-questions": "0.1.2-rc.1",
152
155
  "@types/node": "^24.0.0",
153
156
  "@types/react": "~18.3.1",
154
157
  "tsdown": "^0.22.2",