dsh-code 0.1.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.
package/lib/index.mjs ADDED
@@ -0,0 +1,623 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import { createElement, useState, useSyncExternalStore } from "react";
5
+ import { installModelSelection } from "@deepseek-ai/dsh-agent";
6
+ import { assertNever, boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
7
+ import { SessionId } from "@deepseek-ai/dsh-session";
8
+ import { Box, Text, render, useInput } from "ink";
9
+ import chalk from "chalk";
10
+ //#region src/theme.ts
11
+ /**
12
+ * Terminal color tokens for the dsh TUI, mapped from the product design
13
+ * platform's DeepSeek palette
14
+ * (`packages/client/ui-theme/src/styles/design-platform.css`). Truecolor RGB
15
+ * rides chalk, which degrades automatically on terminals without truecolor.
16
+ *
17
+ * @module @deepseek-ai/dsh-tui/theme
18
+ */
19
+ /**
20
+ * RGB triples for the TUI, one entry per design-platform token in use.
21
+ * Keep names and values in sync with the CSS custom properties cited inline.
22
+ */
23
+ const TUI_RGB = {
24
+ /** Primary brand blue — `--dsw-static-deepseek-500`. */
25
+ brand: [
26
+ 65,
27
+ 118,
28
+ 230
29
+ ],
30
+ /** Brighter brand blue for live/streaming emphasis — `--dsw-static-deepseek-400`. */
31
+ brandBright: [
32
+ 103,
33
+ 158,
34
+ 254
35
+ ],
36
+ /** Deep brand blue for secondary chrome — `--dsw-static-deepseek-600`. */
37
+ brandDeep: [
38
+ 72,
39
+ 104,
40
+ 178
41
+ ],
42
+ /** Muted caption gray — `--dsw-static-neutral-bluish-600`. */
43
+ dim: [
44
+ 129,
45
+ 133,
46
+ 140
47
+ ],
48
+ /** Success green — `--dsw-static-green-500`. */
49
+ success: [
50
+ 34,
51
+ 197,
52
+ 94
53
+ ],
54
+ /** Error red — `--dsw-static-red-500`. */
55
+ error: [
56
+ 239,
57
+ 68,
58
+ 68
59
+ ],
60
+ /** Warning amber — `--dsw-static-amber-500`. */
61
+ warn: [
62
+ 245,
63
+ 158,
64
+ 11
65
+ ]
66
+ };
67
+ /** Paint with the primary brand blue: whale, wordmark, tool names, accents. */
68
+ function brand(text) {
69
+ return chalk.rgb(...TUI_RGB.brand)(text);
70
+ }
71
+ /** Paint muted captions, hints, and meta lines. */
72
+ function dim(text) {
73
+ return chalk.rgb(...TUI_RGB.dim)(text);
74
+ }
75
+ /** Paint failures and error entries. */
76
+ function error(text) {
77
+ return chalk.rgb(...TUI_RGB.error)(text);
78
+ }
79
+ //#endregion
80
+ //#region src/whale-glyph.ts
81
+ /** Half-block whale glyph rows; render with the brand color. */
82
+ const WHALE_GLYPH = [
83
+ " ▄▄▄▄▄▄▄▄█ ▄█▄ ▄",
84
+ " ▄▄██████████▄▄ ▀███▄████",
85
+ "▄███████████████▄ ███▀▀▀ ",
86
+ "██ ▀▀█████▄▀██████ ",
87
+ "██▄ ▀████▄▄████ ",
88
+ " ██▄ ▀██████▀ ",
89
+ " ▀██▄▄ ██▄ ▀███▄▄ ",
90
+ " ▀▀███████▀▀ ▀▀▀ "
91
+ ];
92
+ //#endregion
93
+ //#region src/render/status.ts
94
+ /**
95
+ * Compact token count: 517 / 12.2K / 517K / 1.2M (one decimal under three
96
+ * digits), mirroring the web composer's StatsLine format.
97
+ * @param n - token count.
98
+ * @returns display string.
99
+ */
100
+ function formatTokens(n) {
101
+ const scaled = (v) => v >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10);
102
+ if (n < 1e3) return String(n);
103
+ if (n < 1e6) return `${scaled(n / 1e3)}K`;
104
+ return `${scaled(n / 1e6)}M`;
105
+ }
106
+ /**
107
+ * Compact duration: 45.2s under a minute, 2m42s from there on.
108
+ * @param ms - duration in milliseconds.
109
+ * @returns display string.
110
+ */
111
+ function formatDuration(ms) {
112
+ const s = ms / 1e3;
113
+ if (s < 60) return `${Math.round(s * 10) / 10}s`;
114
+ const whole = Math.round(s);
115
+ return `${Math.floor(whole / 60)}m${whole % 60}s`;
116
+ }
117
+ /**
118
+ * Cache-hit share of billed prompt-side input.
119
+ * @param usage - cumulative token totals.
120
+ * @returns rounded integer percent, or null when no input was billed.
121
+ */
122
+ function cacheHitPercent(usage) {
123
+ return usage.inputTokens === 0 ? null : Math.round(usage.cacheReadTokens / usage.inputTokens * 100);
124
+ }
125
+ /**
126
+ * Build the footer's display groups; a group with no data drops out whole.
127
+ * @param facts - identity facts resolved by the runner.
128
+ * @param stats - session figures folded from the durable log.
129
+ * @returns one string per pipe-separated group, in display order.
130
+ */
131
+ function buildStatusGroups(facts, stats) {
132
+ const groups = [];
133
+ const identity = [
134
+ facts.model,
135
+ facts.cwd,
136
+ facts.branch === "" ? void 0 : `⑂ ${facts.branch}`
137
+ ].filter((part) => part !== void 0 && part !== "");
138
+ if (identity.length > 0) groups.push(identity.join(" · "));
139
+ if (stats.turns > 0 || stats.steps > 0) {
140
+ groups.push(`T${stats.turns} · S${stats.steps}`);
141
+ const durations = [];
142
+ if (stats.llmMs > 0) durations.push(`llm ${formatDuration(stats.llmMs)}`);
143
+ if (stats.toolMs > 0) durations.push(`tool ${formatDuration(stats.toolMs)}`);
144
+ if (durations.length > 0) groups.push(durations.join(" · "));
145
+ }
146
+ const cacheHit = cacheHitPercent(stats.usage);
147
+ if (stats.usage.inputTokens > 0 || stats.usage.outputTokens > 0) {
148
+ if (cacheHit !== null) groups.push(`cache ${cacheHit}%`);
149
+ groups.push(`↑${formatTokens(stats.usage.inputTokens)} ↓${formatTokens(stats.usage.outputTokens)}`);
150
+ }
151
+ if (facts.sessionId !== "") groups.push(facts.sessionId);
152
+ return groups;
153
+ }
154
+ //#endregion
155
+ //#region src/app.ts
156
+ /**
157
+ * The Ink terminal app: whale-and-wordmark header in DeepSeek blue, the live
158
+ * transcript, the streaming line, local notices, and the input box. All state
159
+ * arrives through the transcript store (derived from the durable session log)
160
+ * plus local input state; the app owns no session mutation of its own.
161
+ *
162
+ * Element construction uses `createElement` (not JSX): the `dsh` source launch
163
+ * compiles this file through tsx's ESM-only hook, which does not adopt this
164
+ * package's `jsx: react-jsx` compiler option, and the classic JSX runtime
165
+ * would demand a React global.
166
+ *
167
+ * @module @deepseek-ai/dsh-tui/app
168
+ */
169
+ /** Ink `color` string for one palette triple. */
170
+ function inkColor(triple) {
171
+ return `rgb(${triple[0]}, ${triple[1]}, ${triple[2]})`;
172
+ }
173
+ /** One settled transcript row. */
174
+ function EntryLine({ entry }) {
175
+ switch (entry.kind) {
176
+ case "user": return createElement(Text, null, brand("❯ "), entry.text);
177
+ case "assistant": return createElement(Text, null, entry.text);
178
+ case "tool": {
179
+ const mark = entry.state === "running" ? createElement(Text, { color: inkColor(TUI_RGB.brandBright) }, "◐") : entry.state === "error" ? createElement(Text, { color: inkColor(TUI_RGB.error) }, "⨯") : createElement(Text, { color: inkColor(TUI_RGB.success) }, "⏺");
180
+ return createElement(Text, null, mark, " ", brand(entry.name), entry.summary === "" ? "" : ` ${dim(entry.summary)}`);
181
+ }
182
+ case "error": return createElement(Text, null, error(entry.text));
183
+ default: return assertNever(entry, "transcript entry kind");
184
+ }
185
+ }
186
+ /** The whale wordmark header in DeepSeek blue, hugging its content width. */
187
+ function Header() {
188
+ return createElement(Box, {
189
+ flexDirection: "row",
190
+ gap: 1,
191
+ borderStyle: "round",
192
+ borderColor: inkColor(TUI_RGB.brand),
193
+ paddingX: 1,
194
+ alignSelf: "flex-start"
195
+ }, createElement(Box, {
196
+ flexDirection: "column",
197
+ width: 26
198
+ }, ...WHALE_GLYPH.map((row, index) => createElement(Text, {
199
+ key: index,
200
+ color: inkColor(TUI_RGB.brand)
201
+ }, row))), createElement(Box, {
202
+ flexDirection: "column",
203
+ justifyContent: "center"
204
+ }, createElement(Text, {
205
+ color: inkColor(TUI_RGB.brand),
206
+ bold: true
207
+ }, "DeepSeek Harness"), createElement(Text, { dimColor: true }, "/help commands · Ctrl+C quit")));
208
+ }
209
+ /**
210
+ * The footer status line: Claude-Code-style identity facts (model, working
211
+ * directory, git branch, session) beside the web composer's session figures
212
+ * (turns/steps, model and tool wall time, cache hit, token totals), joined
213
+ * by brand-colored pipes.
214
+ */
215
+ function StatusLine({ facts, stats, busy }) {
216
+ const groups = buildStatusGroups(facts, stats);
217
+ const children = [busy ? createElement(Text, { color: inkColor(TUI_RGB.brandBright) }, "● ") : createElement(Text, { color: inkColor(TUI_RGB.brand) }, "○ ")];
218
+ groups.forEach((group, index) => {
219
+ if (index > 0) children.push(createElement(Text, { dimColor: true }, dim(" | ")));
220
+ children.push(createElement(Text, { dimColor: true }, group));
221
+ });
222
+ return createElement(Box, { paddingX: 1 }, ...children);
223
+ }
224
+ /** The prompt box: slash commands handled locally, other text submitted. */
225
+ function Input({ busy, onSubmit, onQuit }) {
226
+ const [value, setValue] = useState("");
227
+ const [notices, setNotices] = useState([]);
228
+ useInput((input, key) => {
229
+ if (key.ctrl && (input === "c" || input === "d")) {
230
+ onQuit();
231
+ return;
232
+ }
233
+ if (key.return) {
234
+ const text = value.trim();
235
+ setValue("");
236
+ if (text === "") return;
237
+ if (text === "/quit") {
238
+ onQuit();
239
+ return;
240
+ }
241
+ if (text === "/help") {
242
+ setNotices([...notices, "/help show commands · /clear clear the screen · /quit exit"]);
243
+ return;
244
+ }
245
+ if (text === "/clear") {
246
+ setNotices([]);
247
+ console.clear();
248
+ return;
249
+ }
250
+ if (busy) {
251
+ setNotices([...notices, "the agent is working — wait for the turn to finish"]);
252
+ return;
253
+ }
254
+ onSubmit(text);
255
+ return;
256
+ }
257
+ if (key.backspace || key.delete) {
258
+ setValue(value.slice(0, -1));
259
+ return;
260
+ }
261
+ if (input !== "") setValue(value + input);
262
+ });
263
+ return createElement(Box, { flexDirection: "column" }, ...notices.map((notice, index) => createElement(Text, {
264
+ key: index,
265
+ dimColor: true
266
+ }, notice)), createElement(Box, null, createElement(Text, { color: inkColor(TUI_RGB.brand) }, busy ? "… " : "❯ "), createElement(Text, null, value)));
267
+ }
268
+ /** The whole terminal app; state arrives via the store, output via Ink. */
269
+ function App({ store, model, cwd, branch, sessionId, onSubmit, onQuit }) {
270
+ const view = useSyncExternalStore(store.subscribe, store.getView);
271
+ return createElement(Box, { flexDirection: "column" }, createElement(Header), createElement(Box, {
272
+ flexDirection: "column",
273
+ paddingX: 1
274
+ }, ...view.entries.map((entry, index) => createElement(EntryLine, {
275
+ key: index,
276
+ entry
277
+ })), view.streaming !== "" ? createElement(Text, null, view.streaming) : void 0, view.busy && view.streaming === "" ? createElement(Text, { dimColor: true }, "thinking…") : void 0), createElement(Input, {
278
+ busy: view.busy,
279
+ onSubmit,
280
+ onQuit
281
+ }), createElement(StatusLine, {
282
+ facts: {
283
+ model,
284
+ cwd,
285
+ branch,
286
+ sessionId
287
+ },
288
+ stats: view.stats,
289
+ busy: view.busy
290
+ }));
291
+ }
292
+ //#endregion
293
+ //#region src/internals.ts
294
+ /**
295
+ * Injectable process-facing effects for the TUI runner. Tests substitute the
296
+ * Ink mount with a capturing fake and the streams with string sinks, keeping
297
+ * the runner's lifecycle testable without a terminal.
298
+ *
299
+ * @module @deepseek-ai/dsh-tui/internals
300
+ */
301
+ /** Substitutable runner effects; production values write to the real terminal. */
302
+ const internals = {
303
+ mount: (element) => {
304
+ const instance = render(element);
305
+ return { unmount() {
306
+ instance.unmount();
307
+ } };
308
+ },
309
+ stderr: process.stderr
310
+ };
311
+ //#endregion
312
+ //#region src/render/projection.ts
313
+ /**
314
+ * Pure session-event-to-view projection for the TUI transcript: one reducer
315
+ * over {@link SessionEvent}s producing the ordered entries the renderer draws.
316
+ * Rendering never reads the session directly — this module owns the view
317
+ * model, so tests drive it with plain event arrays.
318
+ *
319
+ * @module @deepseek-ai/dsh-tui/render/projection
320
+ */
321
+ /** Join the text blocks of a content list; non-text blocks contribute nothing. */
322
+ function textOf(content) {
323
+ return content.filter((block) => block.type === "text").map((block) => block.text).join("");
324
+ }
325
+ /** A fresh, empty transcript view. */
326
+ function createTranscriptView() {
327
+ return {
328
+ entries: [],
329
+ streaming: "",
330
+ todos: [],
331
+ busy: false,
332
+ stats: {
333
+ turns: 0,
334
+ steps: 0,
335
+ llmMs: 0,
336
+ toolMs: 0,
337
+ usage: {
338
+ inputTokens: 0,
339
+ outputTokens: 0,
340
+ cacheReadTokens: 0
341
+ }
342
+ },
343
+ anchors: {
344
+ stepStart: /* @__PURE__ */ new Map(),
345
+ toolStart: /* @__PURE__ */ new Map()
346
+ }
347
+ };
348
+ }
349
+ /**
350
+ * Fold one session event into an updated view (copy-on-write).
351
+ * @param view - the view before the event.
352
+ * @param event - one durable session event from `session/event` or the log.
353
+ * @returns the view after the event; the input view is never mutated.
354
+ */
355
+ function projectEvent(view, event) {
356
+ switch (event.type) {
357
+ case "user/message": {
358
+ const message = event.data;
359
+ if (message.source.kind === "user") return {
360
+ ...view,
361
+ entries: [...view.entries, {
362
+ kind: "user",
363
+ text: textOf(message.content)
364
+ }]
365
+ };
366
+ const notice = message.source.kind === "plugin" && message.source.form === "notice" ? message.source.summary : message.source.kind;
367
+ return {
368
+ ...view,
369
+ entries: [...view.entries, {
370
+ kind: "user",
371
+ text: boundContextSummary(notice)
372
+ }]
373
+ };
374
+ }
375
+ case "assistant/chunk": {
376
+ const chunk = event.data.chunk;
377
+ if (chunk.type !== "text-delta") return view;
378
+ return {
379
+ ...view,
380
+ streaming: view.streaming + chunk.text
381
+ };
382
+ }
383
+ case "assistant/message": {
384
+ const key = `${event.data.turn}:${event.data.step}`;
385
+ const started = view.anchors.stepStart.get(key);
386
+ view.anchors.stepStart.delete(key);
387
+ const usage = event.data.usage;
388
+ const totals = view.stats.usage;
389
+ return {
390
+ ...view,
391
+ streaming: "",
392
+ entries: [...view.entries, {
393
+ kind: "assistant",
394
+ text: textOf(event.data.message.content)
395
+ }],
396
+ stats: {
397
+ ...view.stats,
398
+ llmMs: view.stats.llmMs + (started === void 0 ? 0 : Math.max(0, event.time - started)),
399
+ usage: usage === void 0 ? totals : {
400
+ inputTokens: totals.inputTokens + usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0),
401
+ outputTokens: totals.outputTokens + usage.outputTokens,
402
+ cacheReadTokens: totals.cacheReadTokens + (usage.cacheReadTokens ?? 0)
403
+ }
404
+ }
405
+ };
406
+ }
407
+ case "tool/call": {
408
+ const data = event.data;
409
+ view.anchors.toolStart.set(data.callId, event.time);
410
+ return {
411
+ ...view,
412
+ entries: [...view.entries, {
413
+ kind: "tool",
414
+ callId: data.callId,
415
+ name: data.name,
416
+ arguments: data.arguments,
417
+ state: "running",
418
+ summary: ""
419
+ }]
420
+ };
421
+ }
422
+ case "tool/result": {
423
+ const block = event.data.message.content[0];
424
+ const started = view.anchors.toolStart.get(block.toolCallId);
425
+ view.anchors.toolStart.delete(block.toolCallId);
426
+ const summary = boundContextSummary(textOf(block.content));
427
+ const entries = view.entries.map((entry) => {
428
+ if (entry.kind !== "tool" || entry.callId !== block.toolCallId) return entry;
429
+ return {
430
+ ...entry,
431
+ state: block.isError === true ? "error" : "done",
432
+ summary
433
+ };
434
+ });
435
+ return {
436
+ ...view,
437
+ entries,
438
+ stats: {
439
+ ...view.stats,
440
+ toolMs: view.stats.toolMs + (started === void 0 ? 0 : Math.max(0, event.time - started))
441
+ }
442
+ };
443
+ }
444
+ case "todo/write": return {
445
+ ...view,
446
+ todos: event.data.todos
447
+ };
448
+ case "turn/start": return {
449
+ ...view,
450
+ busy: true,
451
+ stats: {
452
+ ...view.stats,
453
+ turns: view.stats.turns + 1
454
+ }
455
+ };
456
+ case "step/start":
457
+ view.anchors.stepStart.set(`${event.data.turn}:${event.data.step}`, event.time);
458
+ return {
459
+ ...view,
460
+ stats: {
461
+ ...view.stats,
462
+ steps: view.stats.steps + 1
463
+ }
464
+ };
465
+ case "turn/end": {
466
+ const reason = event.data.reason;
467
+ if (reason.kind !== "error") return {
468
+ ...view,
469
+ busy: false
470
+ };
471
+ return {
472
+ ...view,
473
+ busy: false,
474
+ entries: [...view.entries, {
475
+ kind: "error",
476
+ text: `${reason.error.code}: ${reason.error.message}`
477
+ }]
478
+ };
479
+ }
480
+ default: return view;
481
+ }
482
+ }
483
+ //#endregion
484
+ //#region src/store.ts
485
+ /**
486
+ * Create one transcript store.
487
+ * @returns the store the runner feeds and the renderer subscribes to.
488
+ */
489
+ function createTranscriptStore() {
490
+ let view = createTranscriptView();
491
+ const listeners = /* @__PURE__ */ new Set();
492
+ return {
493
+ getView: () => view,
494
+ subscribe(listener) {
495
+ listeners.add(listener);
496
+ return () => {
497
+ listeners.delete(listener);
498
+ };
499
+ },
500
+ apply(event) {
501
+ const next = projectEvent(view, event);
502
+ if (next === view) return;
503
+ view = next;
504
+ for (const listener of listeners) listener();
505
+ }
506
+ };
507
+ }
508
+ //#endregion
509
+ //#region src/index.ts
510
+ /**
511
+ * @deepseek-ai/dsh-tui — the interactive terminal driver. The bundle patch
512
+ * rides over dsh-base without Host, HTTP, or browser plugins; this runner
513
+ * creates one Agent through the core registry, mounts the Ink app (DeepSeek
514
+ * blue, whale wordmark), folds submitted prompts into the same durable
515
+ * session, streams `session/event` into the transcript, and on quit flushes
516
+ * and requests process exit.
517
+ *
518
+ * @module @deepseek-ai/dsh-tui
519
+ */
520
+ /** Stable Cordis plugin name. */
521
+ const name = "tui-runner";
522
+ /** Core services required before the interactive session can start. */
523
+ const inject = [
524
+ "agentDefaultModel",
525
+ "agents",
526
+ "sessions"
527
+ ];
528
+ /** Report an unexpected direct-driver failure and request a failing exit. */
529
+ function fail(io, error) {
530
+ internals.stderr.write(`dsh: ${error instanceof Error ? error.message : String(error)}\n`);
531
+ io.exit(1);
532
+ }
533
+ /**
534
+ * Resolve the working directory's git branch for the status line.
535
+ * @param cwd - the session's working directory.
536
+ * @returns the branch name, or '' outside a repository or on a detached HEAD.
537
+ */
538
+ function gitBranch(cwd) {
539
+ try {
540
+ return readFileSync(join(cwd, ".git", "HEAD"), "utf8").trim().match(/^ref: refs\/heads\/(.+)$/)?.[1] ?? "";
541
+ } catch {
542
+ return "";
543
+ }
544
+ }
545
+ /**
546
+ * Run the interactive terminal session: create one Agent, mount the app, and
547
+ * keep the process alive until the user quits.
548
+ * @param ctx - plugin context carrying the Agent, default model, Session, and launcher IO services.
549
+ * @param io - process-facing effects.
550
+ */
551
+ async function run(ctx, io) {
552
+ await ctx.get("loader")?.await();
553
+ const agents = ctx.get("agents");
554
+ const defaultModel = ctx.get("agentDefaultModel");
555
+ const sessions = ctx.get("sessions");
556
+ if (agents === void 0 || defaultModel === void 0 || sessions === void 0) return;
557
+ const selection = defaultModel.currentSelection();
558
+ const { agent } = await agents.create({
559
+ sessionId: SessionId(`session-${randomUUID()}`),
560
+ meta: { cwd: process.cwd() },
561
+ agentOptions: {
562
+ provider: selection.provider,
563
+ model: selection.model
564
+ },
565
+ setup: (agentCtx) => {
566
+ installModelSelection(agentCtx, {
567
+ current: selection,
568
+ assembled: void 0
569
+ });
570
+ }
571
+ });
572
+ const store = createTranscriptStore();
573
+ const off = ctx.on("session/event", (session, event) => {
574
+ if (session.id === agent.session.id) store.apply(event);
575
+ });
576
+ const mountRef = {};
577
+ let quitting = false;
578
+ const quit = () => {
579
+ if (quitting) return;
580
+ quitting = true;
581
+ off();
582
+ mountRef.current?.unmount();
583
+ sessions.flush(agent.session).catch((flushError) => {
584
+ internals.stderr.write(`dsh: session flush failed: ${flushError instanceof Error ? flushError.message : String(flushError)}\n`);
585
+ }).then(() => {
586
+ io.exit(0);
587
+ });
588
+ };
589
+ mountRef.current = io.mount(createElement(App, {
590
+ store,
591
+ model: `${selection.provider}/${selection.model}`,
592
+ cwd: basename(process.cwd()),
593
+ branch: gitBranch(process.cwd()),
594
+ sessionId: agent.session.id.slice(-8),
595
+ onSubmit: (text) => {
596
+ agent.followup(createUserMessage({
597
+ content: [{
598
+ type: "text",
599
+ text
600
+ }],
601
+ source: { kind: "user" }
602
+ }));
603
+ },
604
+ onQuit: quit
605
+ }));
606
+ }
607
+ /**
608
+ * Mount the interactive terminal driver.
609
+ * @param ctx - plugin context carrying core services and the launcher-provided exit request.
610
+ */
611
+ function apply(ctx) {
612
+ const exit = ctx.get("appExit");
613
+ if (exit === void 0) throw new Error("tui-runner: the launcher must provide ctx.appExit before the tree mounts");
614
+ const io = {
615
+ mount: internals.mount,
616
+ exit
617
+ };
618
+ run(ctx, io).catch((error) => {
619
+ fail(io, error);
620
+ });
621
+ }
622
+ //#endregion
623
+ export { apply, inject, name };
@@ -0,0 +1,21 @@
1
+ //#region src/invariant.ts
2
+ const PACKAGE_NAME = "@deepseek-ai/dsh-tui";
3
+ /** Cordis companion plugin name. */
4
+ const name = "tui-invariant";
5
+ /** Service required before the companion can register. */
6
+ const inject = ["invariants"];
7
+ /**
8
+ * No runtime invariant beyond the projection's own contract: the TUI renders
9
+ * only from `session/event` (model-visible means logged), so the display
10
+ * relation the renderer could desync from is already asserted by the session
11
+ * log's projection invariants; this companion registers nothing.
12
+ */
13
+ const install = () => {};
14
+ /**
15
+ * Register this package's invariant companion.
16
+ * @param ctx - Cordis context carrying the invariant service.
17
+ * @returns the installed registration's disposer after setup succeeds.
18
+ */
19
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
20
+ //#endregion
21
+ export { apply, inject, name };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The Ink terminal app: whale-and-wordmark header in DeepSeek blue, the live
3
+ * transcript, the streaming line, local notices, and the input box. All state
4
+ * arrives through the transcript store (derived from the durable session log)
5
+ * plus local input state; the app owns no session mutation of its own.
6
+ *
7
+ * Element construction uses `createElement` (not JSX): the `dsh` source launch
8
+ * compiles this file through tsx's ESM-only hook, which does not adopt this
9
+ * package's `jsx: react-jsx` compiler option, and the classic JSX runtime
10
+ * would demand a React global.
11
+ *
12
+ * @module @deepseek-ai/dsh-tui/app
13
+ */
14
+ import { type ReactElement } from 'react';
15
+ import type { TranscriptStore } from './store.ts';
16
+ /** Props the runner hands the app; callbacks stay owned by the runner. */
17
+ export interface AppProps {
18
+ /** Event-fed transcript store for the live session. */
19
+ store: TranscriptStore;
20
+ /** `provider/model` selection serving this session. */
21
+ model: string;
22
+ /** Working-directory basename the session serves. */
23
+ cwd: string;
24
+ /** Git branch name, empty outside a repository. */
25
+ branch: string;
26
+ /** Short session identifier. */
27
+ sessionId: string;
28
+ /** Submit one human prompt; the runner folds it into the session. */
29
+ onSubmit(text: string): void;
30
+ /** Quit: unmount, flush, and request process exit. */
31
+ onQuit(): void;
32
+ }
33
+ /** The whole terminal app; state arrives via the store, output via Ink. */
34
+ export declare function App({ store, model, cwd, branch, sessionId, onSubmit, onQuit }: AppProps): ReactElement;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @deepseek-ai/dsh-tui — the interactive terminal driver. The bundle patch
3
+ * rides over dsh-base without Host, HTTP, or browser plugins; this runner
4
+ * creates one Agent through the core registry, mounts the Ink app (DeepSeek
5
+ * blue, whale wordmark), folds submitted prompts into the same durable
6
+ * session, streams `session/event` into the transcript, and on quit flushes
7
+ * and requests process exit.
8
+ *
9
+ * @module @deepseek-ai/dsh-tui
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ /** Stable Cordis plugin name. */
13
+ export declare const name = "tui-runner";
14
+ /** Core services required before the interactive session can start. */
15
+ export declare const inject: string[];
16
+ /**
17
+ * Mount the interactive terminal driver.
18
+ * @param ctx - plugin context carrying core services and the launcher-provided exit request.
19
+ */
20
+ export declare function apply(ctx: Context): void;