@toddzheng024/dscode-bundle 0.7.12 → 0.7.13

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 (152) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -3
  2. package/cordis.patch.yml +1 -1
  3. package/package.json +5 -4
  4. package/plugins/session-metrics/view.mjs +10 -2
  5. package/vendor/tui/lib/app.mjs +6674 -0
  6. package/vendor/tui/lib/approval.mjs +122 -0
  7. package/vendor/tui/lib/attachments.mjs +218 -0
  8. package/vendor/tui/lib/authorization-panel.mjs +242 -0
  9. package/vendor/tui/lib/authorization.mjs +111 -0
  10. package/vendor/tui/lib/commands.mjs +123 -0
  11. package/vendor/tui/lib/dscode/chat.mjs +64 -0
  12. package/vendor/tui/lib/dscode/flags.mjs +32 -0
  13. package/vendor/tui/lib/dscode/model-search.mjs +68 -0
  14. package/vendor/tui/lib/dscode/paste.mjs +87 -0
  15. package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
  16. package/vendor/tui/lib/dscode/welcome.mjs +100 -0
  17. package/vendor/tui/lib/editor-keys.mjs +346 -0
  18. package/vendor/tui/lib/editor.mjs +50 -0
  19. package/vendor/tui/lib/fork.mjs +24 -0
  20. package/vendor/tui/lib/git-workflow.mjs +278 -0
  21. package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
  22. package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
  23. package/vendor/tui/lib/index.mjs +2198 -0
  24. package/vendor/tui/lib/input-split.mjs +170 -0
  25. package/vendor/tui/lib/internals.mjs +90 -0
  26. package/vendor/tui/lib/invariant.mjs +22 -0
  27. package/vendor/tui/lib/kernel-panels.mjs +1265 -0
  28. package/vendor/tui/lib/keyboard.mjs +283 -0
  29. package/vendor/tui/lib/language-panel.mjs +41 -0
  30. package/vendor/tui/lib/locales/en.mjs +495 -0
  31. package/vendor/tui/lib/locales/zh.mjs +495 -0
  32. package/vendor/tui/lib/mentions.mjs +150 -0
  33. package/vendor/tui/lib/model-capabilities.mjs +245 -0
  34. package/vendor/tui/lib/models.mjs +215 -0
  35. package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
  36. package/vendor/tui/lib/permissions.mjs +45 -0
  37. package/vendor/tui/lib/plugin-inventory.mjs +27 -0
  38. package/vendor/tui/lib/presets.mjs +46 -0
  39. package/vendor/tui/lib/provider-settings.mjs +600 -0
  40. package/vendor/tui/lib/questions.mjs +116 -0
  41. package/vendor/tui/lib/rainbow.mjs +181 -0
  42. package/vendor/tui/lib/render/animations.mjs +665 -0
  43. package/vendor/tui/lib/render/editor.mjs +437 -0
  44. package/vendor/tui/lib/render/export.mjs +119 -0
  45. package/vendor/tui/lib/render/fuzzy.mjs +79 -0
  46. package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
  47. package/vendor/tui/lib/render/inspector.mjs +93 -0
  48. package/vendor/tui/lib/render/lines.mjs +538 -0
  49. package/vendor/tui/lib/render/markdown.mjs +592 -0
  50. package/vendor/tui/lib/render/projection.mjs +1862 -0
  51. package/vendor/tui/lib/render/status.mjs +591 -0
  52. package/vendor/tui/lib/render/text.mjs +157 -0
  53. package/vendor/tui/lib/render/tool-detail.mjs +187 -0
  54. package/vendor/tui/lib/render/tool-preview.mjs +79 -0
  55. package/vendor/tui/lib/render/usage.mjs +336 -0
  56. package/vendor/tui/lib/render/width.mjs +191 -0
  57. package/vendor/tui/lib/session-directory.mjs +329 -0
  58. package/vendor/tui/lib/session-query.mjs +184 -0
  59. package/vendor/tui/lib/session-switch.mjs +51 -0
  60. package/vendor/tui/lib/settings-file.mjs +75 -0
  61. package/vendor/tui/lib/skills.mjs +119 -0
  62. package/vendor/tui/lib/startup.mjs +111 -0
  63. package/vendor/tui/lib/store.mjs +129 -0
  64. package/vendor/tui/lib/subagents.mjs +200 -0
  65. package/vendor/tui/lib/terminal-title.mjs +186 -0
  66. package/vendor/tui/lib/theme-panel.mjs +60 -0
  67. package/vendor/tui/lib/theme.mjs +379 -0
  68. package/vendor/tui/lib/update-panel.mjs +239 -0
  69. package/vendor/tui/lib/update.mjs +101 -0
  70. package/vendor/tui/lib/version.mjs +105 -0
  71. package/vendor/tui/lib/whale-glyph.mjs +20 -0
  72. package/vendor/tui/LICENSE +0 -21
  73. package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
  74. package/vendor/tui/dscode-email/cli.mjs +0 -32
  75. package/vendor/tui/dscode-email/contacts.mjs +0 -36
  76. package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
  77. package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
  78. package/vendor/tui/dscode-email/gmail.mjs +0 -194
  79. package/vendor/tui/dscode-email/imap.mjs +0 -136
  80. package/vendor/tui/dscode-email/inbox.d.mts +0 -25
  81. package/vendor/tui/dscode-email/inbox.mjs +0 -76
  82. package/vendor/tui/dscode-email/smtp.mjs +0 -69
  83. package/vendor/tui/dscode-email/store.mjs +0 -32
  84. package/vendor/tui/dscode-providers/catalog.mjs +0 -136
  85. package/vendor/tui/dscode-providers/effort.mjs +0 -35
  86. package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
  87. package/vendor/tui/index.mjs +0 -44441
  88. package/vendor/tui/invariant.mjs +0 -21
  89. package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
  90. package/vendor/tui/session-query.mjs +0 -150
  91. package/vendor/tui/startup.mjs +0 -109
  92. package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
  93. package/vendor/tui/types/app.d.ts +0 -348
  94. package/vendor/tui/types/approval.d.ts +0 -59
  95. package/vendor/tui/types/attachments.d.ts +0 -52
  96. package/vendor/tui/types/authorization-panel.d.ts +0 -22
  97. package/vendor/tui/types/authorization.d.ts +0 -36
  98. package/vendor/tui/types/commands.d.ts +0 -52
  99. package/vendor/tui/types/editor-keys.d.ts +0 -105
  100. package/vendor/tui/types/editor.d.ts +0 -6
  101. package/vendor/tui/types/fork.d.ts +0 -8
  102. package/vendor/tui/types/git-workflow.d.ts +0 -121
  103. package/vendor/tui/types/index.d.ts +0 -223
  104. package/vendor/tui/types/input-split.d.ts +0 -54
  105. package/vendor/tui/types/internals.d.ts +0 -26
  106. package/vendor/tui/types/invariant.d.ts +0 -15
  107. package/vendor/tui/types/kernel-panels.d.ts +0 -245
  108. package/vendor/tui/types/keyboard.d.ts +0 -80
  109. package/vendor/tui/types/language-panel.d.ts +0 -12
  110. package/vendor/tui/types/locales/en.d.ts +0 -450
  111. package/vendor/tui/types/locales/zh.d.ts +0 -9
  112. package/vendor/tui/types/mentions.d.ts +0 -85
  113. package/vendor/tui/types/model-capabilities.d.ts +0 -82
  114. package/vendor/tui/types/models.d.ts +0 -133
  115. package/vendor/tui/types/permissions.d.ts +0 -27
  116. package/vendor/tui/types/plugin-inventory.d.ts +0 -11
  117. package/vendor/tui/types/presets.d.ts +0 -22
  118. package/vendor/tui/types/provider-settings.d.ts +0 -239
  119. package/vendor/tui/types/questions.d.ts +0 -54
  120. package/vendor/tui/types/rainbow.d.ts +0 -69
  121. package/vendor/tui/types/render/animations.d.ts +0 -307
  122. package/vendor/tui/types/render/editor.d.ts +0 -163
  123. package/vendor/tui/types/render/export.d.ts +0 -9
  124. package/vendor/tui/types/render/fuzzy.d.ts +0 -21
  125. package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
  126. package/vendor/tui/types/render/inspector.d.ts +0 -62
  127. package/vendor/tui/types/render/lines.d.ts +0 -86
  128. package/vendor/tui/types/render/markdown.d.ts +0 -29
  129. package/vendor/tui/types/render/projection.d.ts +0 -587
  130. package/vendor/tui/types/render/status.d.ts +0 -196
  131. package/vendor/tui/types/render/text.d.ts +0 -64
  132. package/vendor/tui/types/render/tool-detail.d.ts +0 -94
  133. package/vendor/tui/types/render/tool-preview.d.ts +0 -28
  134. package/vendor/tui/types/render/usage.d.ts +0 -113
  135. package/vendor/tui/types/render/width.d.ts +0 -29
  136. package/vendor/tui/types/session-directory.d.ts +0 -190
  137. package/vendor/tui/types/session-query.d.ts +0 -92
  138. package/vendor/tui/types/session-switch.d.ts +0 -17
  139. package/vendor/tui/types/settings-file.d.ts +0 -41
  140. package/vendor/tui/types/skills.d.ts +0 -47
  141. package/vendor/tui/types/startup.d.ts +0 -65
  142. package/vendor/tui/types/store.d.ts +0 -58
  143. package/vendor/tui/types/subagents.d.ts +0 -70
  144. package/vendor/tui/types/terminal-title.d.ts +0 -66
  145. package/vendor/tui/types/theme-panel.d.ts +0 -24
  146. package/vendor/tui/types/theme.d.ts +0 -434
  147. package/vendor/tui/types/update-panel.d.ts +0 -49
  148. package/vendor/tui/types/update.d.ts +0 -75
  149. package/vendor/tui/types/version.d.ts +0 -19
  150. package/vendor/tui/types/whale-glyph.d.ts +0 -6
  151. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
  152. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
@@ -0,0 +1,111 @@
1
+ /** Terminal adapter over the Harness provider-authorization and credential-record seams. */
2
+ import { spawn } from 'node:child_process';
3
+ import { credentialKeyId, credentialKeyScope, } from '@deepseek-ai/dsh-credentials';
4
+ /** Record scope used by the upstream pi-ai adapter for provider logins. */
5
+ const PI_AI_RECORD_SCOPE = 'llm-pi-ai';
6
+ /** Load only model-provider flows; unrelated future authorization domains stay out of `/model`. */
7
+ export async function loadProviderAuthorizations(ctx) {
8
+ const authorization = ctx.get('authorization');
9
+ const credentials = ctx.get('credentials');
10
+ if (authorization === undefined || credentials === undefined)
11
+ return { rows: [], failures: [] };
12
+ const entries = authorization.list().filter(entry => credentialKeyScope(entry.key) === PI_AI_RECORD_SCOPE);
13
+ const failures = [];
14
+ const rows = await Promise.all(entries.map(async (entry) => {
15
+ let record;
16
+ try {
17
+ record = await credentials.describeRecord(entry.key);
18
+ }
19
+ catch (error) {
20
+ failures.push(`${entry.label}: ${error instanceof Error ? error.message : String(error)}`);
21
+ record = { configured: false, writable: false };
22
+ }
23
+ return {
24
+ key: entry.key,
25
+ provider: credentialKeyId(entry.key),
26
+ label: entry.label,
27
+ methods: entry.methods,
28
+ inFlight: entry.inFlight,
29
+ record,
30
+ };
31
+ }));
32
+ return { rows, failures };
33
+ }
34
+ /** Subscribe to login settlement and credential-record changes. */
35
+ export function subscribeProviderAuthorizations(ctx, listener) {
36
+ const settled = ctx.on('authorization/settled', (key) => {
37
+ if (credentialKeyScope(key) === PI_AI_RECORD_SCOPE)
38
+ listener();
39
+ });
40
+ const records = ctx.on('credentials/record-updated', (key) => {
41
+ if (credentialKeyScope(key) === PI_AI_RECORD_SCOPE)
42
+ listener();
43
+ });
44
+ return () => {
45
+ settled();
46
+ records();
47
+ };
48
+ }
49
+ /** Begin one provider login through the interaction surface owned by the caller. */
50
+ export async function beginProviderAuthorization(ctx, row, method, interaction, signal) {
51
+ const authorization = ctx.get('authorization');
52
+ if (authorization === undefined)
53
+ throw new Error('provider login is unavailable in this profile');
54
+ return (await authorization.begin({ key: row.key, method, interaction, signal })).status;
55
+ }
56
+ /** Cancel the attempt currently serving this provider, if any. */
57
+ export function cancelProviderAuthorization(ctx, key) {
58
+ ctx.get('authorization')?.cancel(key);
59
+ }
60
+ /** Remove an authorization record without changing the provider's settings profile. */
61
+ export async function logoutProviderAuthorization(ctx, row) {
62
+ const credentials = ctx.get('credentials');
63
+ if (credentials === undefined)
64
+ throw new Error('credential storage is unavailable in this profile');
65
+ const current = await credentials.describeRecord(row.key);
66
+ if (!current.configured)
67
+ return;
68
+ if (!current.writable)
69
+ throw new Error('this login record is read-only');
70
+ await credentials.deleteRecord(row.key);
71
+ }
72
+ /** Open an authorization URL with the platform default browser, without invoking a shell. */
73
+ export function openAuthorizationUrl(raw) {
74
+ let url;
75
+ try {
76
+ url = new URL(raw);
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ if (url.protocol !== 'https:' && url.protocol !== 'http:')
82
+ return false;
83
+ const target = url.toString();
84
+ try {
85
+ const child = process.platform === 'win32'
86
+ ? spawn('explorer.exe', [target], { detached: true, stdio: 'ignore' })
87
+ : process.platform === 'darwin'
88
+ ? spawn('/usr/bin/open', [target], { detached: true, stdio: 'ignore' })
89
+ : spawn('xdg-open', [target], { detached: true, stdio: 'ignore' });
90
+ child.once('error', () => { });
91
+ child.unref();
92
+ return true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ /** Compact value-free status for the provider list. */
99
+ export function providerAuthorizationStatus(row) {
100
+ if (row === undefined)
101
+ return 'login unavailable';
102
+ if (row.inFlight)
103
+ return 'login in progress';
104
+ if (!row.record.configured)
105
+ return 'not logged in';
106
+ return row.record.kind === 'grant' ? 'OAuth' : 'interactive API key';
107
+ }
108
+ /** Find a provider's login flow from a previously loaded directory. */
109
+ export function authorizationForProvider(directory, provider) {
110
+ return directory?.rows.find(row => row.provider === provider);
111
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Slash-command bridge: forwards terminal command lines into the shared
3
+ * `ctx.commands` registry (the same surface the web composer dispatches
4
+ * through) and exposes the live descriptor list as completion candidates.
5
+ * The runner keeps only its own TUI-local commands (`/help`, `/quit`,
6
+ * `/clear`, `/model`) ahead of the registry dispatch.
7
+ *
8
+ * @module @deepseek-ai/dsh-tui/commands
9
+ */
10
+ /**
11
+ * Watch the live command registry. Reads the current list immediately and
12
+ * re-reads on every registry mutation or agent retarget; notification
13
+ * failures are contained by the registry itself, so this watcher only ever
14
+ * re-reads. Without a `commands` service the view stays empty and all lines
15
+ * fall through to normal prompts.
16
+ * @param ctx - context carrying the `commands` service (optional).
17
+ * @returns the view the completion menu subscribes to.
18
+ */
19
+ export function watchCommands(ctx) {
20
+ const commands = ctx.get('commands');
21
+ let agent;
22
+ let descriptors = [];
23
+ let error;
24
+ // The agent whose scoped view the current descriptors were read for: a
25
+ // failure before this agent ever loaded clears the list instead of keeping
26
+ // another session's commands completable here.
27
+ let loadedFor;
28
+ const listeners = new Set();
29
+ // Content gate: the host registry allocates a fresh array on every list()
30
+ // call and 0.1.5 emits commands/change for every scoped register AND
31
+ // dispose (a startup registration wave lands inside React's commit
32
+ // windows). A fresh identity per event chains nested passive updates past
33
+ // React's 50-deep limit ("Maximum update depth exceeded"), so an unchanged
34
+ // catalog keeps the previous array identity and notifies nobody — the same
35
+ // discipline the skills gate and the store's frame throttle established.
36
+ const descriptorFingerprint = (list) => JSON.stringify(list.map(descriptor => [descriptor.name, descriptor.description, descriptor.input?.hint ?? '', descriptor.input?.attachments === true]));
37
+ let lastFingerprint = '[]';
38
+ let lastNotifiedError;
39
+ const changed = (next, nextError) => descriptorFingerprint(next) !== lastFingerprint || nextError !== lastNotifiedError;
40
+ // Frame throttle: coalesce a same-tick event storm into one notification
41
+ // (the transcript store's NOTIFY_FRAME_MS contract).
42
+ let notifyScheduled = false;
43
+ const notify = () => {
44
+ if (notifyScheduled)
45
+ return;
46
+ notifyScheduled = true;
47
+ setImmediate(() => {
48
+ notifyScheduled = false;
49
+ for (const listener of listeners)
50
+ listener();
51
+ });
52
+ };
53
+ const refresh = () => {
54
+ if (commands === undefined || agent === undefined)
55
+ return;
56
+ let next;
57
+ let nextError;
58
+ try {
59
+ next = commands.list(agent);
60
+ loadedFor = agent;
61
+ }
62
+ catch (cause) {
63
+ // Keep the last good catalog for the SAME agent, but change its identity
64
+ // so subscribers can render the recoverable failure in /help; an agent
65
+ // that never loaded starts from empty.
66
+ next = loadedFor === agent ? [...descriptors] : [];
67
+ nextError = cause instanceof Error ? cause.message : String(cause);
68
+ }
69
+ if (!changed(next, nextError))
70
+ return;
71
+ descriptors = next;
72
+ error = nextError;
73
+ lastFingerprint = descriptorFingerprint(next);
74
+ lastNotifiedError = nextError;
75
+ notify();
76
+ };
77
+ if (commands !== undefined) {
78
+ ctx.on('commands/change', () => refresh());
79
+ }
80
+ return {
81
+ get descriptors() {
82
+ return descriptors;
83
+ },
84
+ get error() {
85
+ return error;
86
+ },
87
+ subscribe(listener) {
88
+ listeners.add(listener);
89
+ return () => {
90
+ listeners.delete(listener);
91
+ };
92
+ },
93
+ setAgent(next) {
94
+ agent = next;
95
+ refresh();
96
+ },
97
+ };
98
+ }
99
+ /**
100
+ * Whether one command line is a syntactically valid slash command.
101
+ * @param line - the complete candidate line.
102
+ * @returns true when the line parses as `/name` or `/name input`.
103
+ */
104
+ export function isSlashLine(line) {
105
+ return /^\/[a-z][a-z0-9_-]*(?=$|[\t ])/u.test(line);
106
+ }
107
+ /**
108
+ * The submission payload for one composer line. Trim is a blank check, not a
109
+ * rewrite: an ordinary prompt keeps its exact leading indentation, inner
110
+ * layout, and trailing spaces (pasted code must reach the model verbatim).
111
+ * Only trailing line terminators are stripped — a draft's final newline is a
112
+ * paste/Enter artifact (an open bracketed paste turns Enter into an inserted
113
+ * newline), never deliberate content. A syntactic slash line still normalizes
114
+ * fully so command routing stays stable (completion inserts a trailing space
115
+ * after `/name`).
116
+ * @param line - the complete draft text.
117
+ * @returns the text to submit verbatim.
118
+ */
119
+ export function submissionPayload(line) {
120
+ const withoutTrailingNewlines = line.replace(/[\r\n]+$/u, '');
121
+ const trimmed = withoutTrailingNewlines.trim();
122
+ return isSlashLine(trimmed) ? trimmed : withoutTrailingNewlines;
123
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * dscode: DSCODE's transcript rendering. The default view stays quiet — tool
3
+ * calls and the model's thinking only appear (dim) once verbose is on — and a
4
+ * wrapped user prompt is painted as one continuous background band.
5
+ *
6
+ * @module dsh-code/dscode/chat
7
+ */
8
+ import { visibleColumns } from '../render/markdown.mjs';
9
+ import { hangingStyledLines, hangingTextLines, lineSegment, textLines, transcriptEntryLines, } from '../render/lines.mjs';
10
+ /** Pad every physical row of a wrapped prompt so its background stays unbroken. */
11
+ export function userBackgroundRows(rows, columns, measure) {
12
+ const width = Math.max(1, Math.floor(columns));
13
+ return rows.map(row => {
14
+ const used = row.segments.reduce((sum, segment) => sum + measure(segment.text), 0);
15
+ return {
16
+ ...row,
17
+ background: 'user',
18
+ segments: [...row.segments, { text: ' '.repeat(Math.max(0, width - used)), style: 'plain' }],
19
+ };
20
+ });
21
+ }
22
+ /** The folded thinking tail: one dim paragraph, capped, with the Ctrl+O hint. */
23
+ function thinkingLines(reasoning, width) {
24
+ const lines = hangingTextLines('Thinking: ' + reasoning.replace(/\s+/g, ' ').trim(), width, '· ', 'dimItalic', ' ');
25
+ const cap = 8;
26
+ return lines.length <= cap
27
+ ? lines
28
+ : [...lines.slice(0, cap), ...textLines(' … ' + (lines.length - cap) + ' more lines · Ctrl+O opens the full history', width, 'dim')];
29
+ }
30
+ /**
31
+ * Render one transcript entry the DSCODE way: tool rows are quiet unless verbose
32
+ * is on, assistant rows fold their thinking above the answer, and user prompts
33
+ * carry the background band.
34
+ */
35
+ export function dscodeChatLines(entry, columns, verbose = false) {
36
+ const width = Math.max(1, Math.floor(columns));
37
+ if (entry.kind === 'tool') {
38
+ if (!verbose)
39
+ return [];
40
+ const state = entry.state === 'running' ? ' · running' : entry.state === 'error' ? ' · error' : '';
41
+ const lines = hangingStyledLines([
42
+ lineSegment('Tool Call: ' + entry.name, 'dim'),
43
+ lineSegment(entry.preview ? ' ' + entry.preview : '', 'dim'),
44
+ lineSegment(state, entry.state === 'error' ? 'error' : 'dim'),
45
+ ], width, '· ', 'dim', ' ', 'dim');
46
+ if (entry.summary) {
47
+ lines.push(...hangingTextLines('Output: ' + entry.summary, width, ' ', entry.state === 'error' ? 'error' : 'dim', ' '));
48
+ }
49
+ lines.push({ segments: [] });
50
+ return lines;
51
+ }
52
+ if (entry.kind === 'assistant') {
53
+ const thinking = verbose && entry.reasoning ? [...thinkingLines(entry.reasoning, width), { segments: [] }] : [];
54
+ if (!entry.text && !entry.interrupted)
55
+ return thinking;
56
+ const body = [...thinking, ...transcriptEntryLines({ ...entry, reasoning: '' }, columns, false, false, false)];
57
+ // The turn's last reply closes the turn with its own rule and breathing rows.
58
+ return entry.turnEnded === true
59
+ ? [...body, { segments: [] }, { segments: [{ text: '─'.repeat(width), style: 'dim' }] }, { segments: [] }]
60
+ : body;
61
+ }
62
+ const rows = transcriptEntryLines(entry, columns, false, false, false);
63
+ return entry.kind === 'user' && !entry.notice ? [...userBackgroundRows(rows, columns, visibleColumns), { segments: [] }] : rows;
64
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * dscode: small UI preferences that must survive a restart (the verbose
3
+ * transcript toggle). They live beside the launcher's other state, one JSON file
4
+ * per flag, and a read or write that fails never breaks the render path.
5
+ *
6
+ * @module dsh-code/dscode/flags
7
+ */
8
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ /** The directory DSCODE keeps its own terminal state in. */
12
+ function stateDirectory() {
13
+ return join(homedir(), '.dsh', 'dsh-code');
14
+ }
15
+ /** One persisted boolean; a missing or corrupt file falls back. */
16
+ export function loadFlag(name, fallback = false) {
17
+ try {
18
+ const value = JSON.parse(readFileSync(join(stateDirectory(), name + '.json'), 'utf8'))[name];
19
+ return typeof value === 'boolean' ? value : fallback;
20
+ }
21
+ catch {
22
+ return fallback;
23
+ }
24
+ }
25
+ /** Persist one boolean; a failed write is ignored so the UI never blocks. */
26
+ export function saveFlag(name, value) {
27
+ try {
28
+ mkdirSync(stateDirectory(), { recursive: true });
29
+ writeFileSync(join(stateDirectory(), name + '.json'), JSON.stringify({ [name]: value }, null, 2) + '\n');
30
+ }
31
+ catch { }
32
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * dscode: the /model picker's row filter. Only the session's current provider is
3
+ * listed, and typing narrows at once instead of acting as a shortcut: BM25 over the
4
+ * model name (weight 3), the id (2) and the provider (1). Letters and digits split
5
+ * into separate tokens, so `glm5` finds GLM 5.3, and the word still being typed
6
+ * matches the tokens it starts (`kim` finds Kimi). Rows matching every word win
7
+ * over rows matching some, and whatever matches is presented in alphabetical order of
8
+ * the displayed label with digits compared naturally.
9
+ *
10
+ * @module dsh-code/dscode/model-search
11
+ */
12
+ export function dscodeFilterModels(rows, query, provider) {
13
+ // One provider at a time: the picker lists the routes the session can actually select.
14
+ const directory = provider === void 0 ? rows : rows.filter(row => row.provider === provider);
15
+ const label = row => String(row.modelName ?? row.model ?? "");
16
+ // Display order is the label itself, so the list reads alphabetically and digits inside a
17
+ // name compare naturally ("GLM 5.2" before "GLM 5.3"); searching narrows rows, never re-ranks them.
18
+ const byLabel = (left, right) => label(left).localeCompare(label(right), void 0, {
19
+ numeric: true,
20
+ sensitivity: "base"
21
+ }) || String(left.model ?? "").localeCompare(String(right.model ?? "")) || String(left.provider ?? "").localeCompare(String(right.provider ?? ""));
22
+ const tokenize = text => String(text ?? '').normalize('NFKC').toLowerCase().match(/[a-z]+|[0-9]+|[^\s\x00-\x7f]+/g) ?? [];
23
+ const raw = String(query ?? '');
24
+ const words = tokenize(raw);
25
+ if (words.length === 0)
26
+ return [...directory].sort(byLabel);
27
+ const typing = /\s$/.test(raw) ? -1 : words.length - 1;
28
+ const fields = [['modelName', 3], ['model', 2], ['providerName', 1], ['provider', 1]];
29
+ const docs = directory.map(row => {
30
+ const counts = new Map();
31
+ let length = 0;
32
+ for (const [field, weight] of fields)
33
+ for (const token of tokenize(row[field])) {
34
+ counts.set(token, (counts.get(token) ?? 0) + weight);
35
+ length += weight;
36
+ }
37
+ return { row, counts, length };
38
+ });
39
+ const average = docs.reduce((sum, doc) => sum + doc.length, 0) / Math.max(1, docs.length);
40
+ const frequency = (doc, word, prefix) => {
41
+ if (!prefix)
42
+ return doc.counts.get(word) ?? 0;
43
+ let sum = 0;
44
+ for (const [token, count] of doc.counts)
45
+ if (token.startsWith(word))
46
+ sum += token === word ? count : count * 0.8;
47
+ return sum;
48
+ };
49
+ const k1 = 1.2, b = 0.75;
50
+ const terms = words.map((word, index) => {
51
+ const matches = docs.map(doc => frequency(doc, word, index === typing));
52
+ const found = matches.filter(value => value > 0).length;
53
+ return { matches, idf: Math.log(1 + (docs.length - found + 0.5) / (found + 0.5)) };
54
+ });
55
+ const scored = docs.map((doc, index) => {
56
+ let score = 0, hits = 0;
57
+ for (const term of terms) {
58
+ const tf = term.matches[index];
59
+ if (tf <= 0)
60
+ continue;
61
+ hits += 1;
62
+ score += term.idf * tf * (k1 + 1) / (tf + k1 * (1 - b + b * doc.length / average));
63
+ }
64
+ return { row: doc.row, score, hits };
65
+ }).filter(entry => entry.hits > 0);
66
+ const complete = scored.filter(entry => entry.hits === terms.length);
67
+ return (complete.length > 0 ? complete : scored).map(entry => entry.row).sort(byLabel);
68
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * dscode: large pastes collapse to one marker in the draft so the composer stays
3
+ * readable, then expand back to the full text on submit. The marker is atomic for
4
+ * every edit and cursor move, so a paste can never be half-deleted.
5
+ *
6
+ * @module dsh-code/dscode/paste
7
+ */
8
+ /** Pastes longer than this collapse to a marker. */
9
+ export const LARGE_PASTE_CHARS = 200;
10
+ export function collapseLargePaste(text, draft, pastes) {
11
+ const safe = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n').replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, '').replaceAll('\t', ' ');
12
+ const count = [...safe].length;
13
+ if (count <= LARGE_PASTE_CHARS)
14
+ return safe;
15
+ // Keep an executable-looking first line visible to the command router.
16
+ if (draft.trim() === '' && /^\s*[!/]/u.test(safe))
17
+ return safe;
18
+ const base = `[Pasted Content ${count} chars]`;
19
+ let marker = base;
20
+ for (let suffix = 2; draft.includes(marker) || pastes.has(marker); suffix++)
21
+ marker = `${base} #${suffix}`;
22
+ pastes.set(marker, safe);
23
+ return marker;
24
+ }
25
+ export function expandLargePastes(value, pastes) {
26
+ const markers = [...pastes.keys()].sort((left, right) => right.length - left.length);
27
+ let expanded = '';
28
+ let offset = 0;
29
+ while (offset < value.length) {
30
+ let found = -1;
31
+ let match;
32
+ for (const marker of markers) {
33
+ const at = value.indexOf(marker, offset);
34
+ if (at >= 0 && (found < 0 || at < found)) {
35
+ found = at;
36
+ match = marker;
37
+ }
38
+ }
39
+ if (match === undefined)
40
+ break;
41
+ expanded += value.slice(offset, found) + pastes.get(match);
42
+ offset = found + match.length;
43
+ }
44
+ return expanded + value.slice(offset);
45
+ }
46
+ export function pasteAtomicEdit(before, edit, pastes) {
47
+ let prefix = 0;
48
+ while (prefix < before.length && prefix < edit.value.length && before[prefix] === edit.value[prefix])
49
+ prefix++;
50
+ let suffix = 0;
51
+ while (suffix < before.length - prefix && suffix < edit.value.length - prefix && before[before.length - suffix - 1] === edit.value[edit.value.length - suffix - 1])
52
+ suffix++;
53
+ let from = prefix, to = before.length - suffix;
54
+ const inserted = edit.value.slice(prefix, edit.value.length - suffix);
55
+ let touched = false;
56
+ const originalPastes = new Map(pastes);
57
+ for (const marker of pastes.keys()) {
58
+ const start = before.indexOf(marker);
59
+ if (start < 0)
60
+ continue;
61
+ const end = start + marker.length;
62
+ if (from < end && to > start || from === to && from > start && from < end) {
63
+ from = Math.min(from, start);
64
+ to = Math.max(to, end);
65
+ pastes.delete(marker);
66
+ touched = true;
67
+ }
68
+ }
69
+ if (touched)
70
+ edit = { ...edit, value: before.slice(0, from) + inserted + before.slice(to), cursor: from + inserted.length,
71
+ killed: edit.killed === undefined ? undefined : expandLargePastes(before.slice(from, to), originalPastes) };
72
+ for (const marker of pastes.keys())
73
+ if (!edit.value.includes(marker) && before.includes(marker))
74
+ pastes.delete(marker);
75
+ return edit;
76
+ }
77
+ export function pasteCursorEdge(value, previous, next, pastes) {
78
+ for (const marker of pastes.keys()) {
79
+ const start = value.indexOf(marker);
80
+ if (start < 0)
81
+ continue;
82
+ const end = start + marker.length;
83
+ if (next > start && next < end)
84
+ return next < previous ? start : end;
85
+ }
86
+ return next;
87
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * dscode: the footer telemetry tints its live numbers — the request rate by tier
3
+ * and the cache-hit rate by how well the prompt prefix is being reused.
4
+ *
5
+ * @module dsh-code/dscode/telemetry
6
+ */
7
+ import { createElement } from 'react';
8
+ import { Text } from 'ink';
9
+ import { getPalette, getTheme, inkColor } from '../theme.mjs';
10
+ /** Request-rate tiers: yellow under 75 tps, green to 150, blue to 250, purple above. */
11
+ export function dscodeTpsTone(rate) {
12
+ if (!Number.isFinite(rate) || rate < 0)
13
+ return null;
14
+ if (rate < 75)
15
+ return 'yellow';
16
+ if (rate < 150)
17
+ return 'green';
18
+ if (rate <= 250)
19
+ return 'blue';
20
+ return 'purple';
21
+ }
22
+ /** Cache-hit tiers: red under 90%, yellow to 95, green to 98, blue above. */
23
+ export function dscodeCacheTone(rate) {
24
+ if (!Number.isFinite(rate) || rate < 0)
25
+ return null;
26
+ if (rate < 90)
27
+ return 'red';
28
+ if (rate < 95)
29
+ return 'yellow';
30
+ if (rate < 98)
31
+ return 'green';
32
+ return 'blue';
33
+ }
34
+ /** Every label the cache figure can carry, across the shipped interface languages. */
35
+ const DSCode_CACHE_LABELS = ['cache hit', '缓存命中', '快取命中', 'キャッシュヒット', '캐시 적중', 'éxitos de caché'];
36
+ /** The ink colour for a tier, theme-aware for the two ends of the scale. */
37
+ export function dscodeTpsInkColor(tone) {
38
+ const palette = getPalette();
39
+ if (tone === 'red')
40
+ return inkColor(getTheme() === 'light' ? [185, 28, 28] : [248, 113, 113]);
41
+ if (tone === 'yellow')
42
+ return inkColor(palette.warn);
43
+ if (tone === 'green')
44
+ return inkColor(palette.success);
45
+ if (tone === 'blue')
46
+ return inkColor(palette.brandBright);
47
+ if (tone === 'purple')
48
+ return inkColor(getTheme() === 'light' ? [126, 34, 206] : [192, 132, 252]);
49
+ return undefined;
50
+ }
51
+ /**
52
+ * Split a telemetry string into tintable runs. The `tps` figures and the trailing
53
+ * cache-hit rate carry a tier; everything else keeps the surrounding colour.
54
+ */
55
+ export function dscodeTelemetryParts(value) {
56
+ const result = [];
57
+ const parts = String(value).split(' | ');
58
+ for (const [index, part] of parts.entries()) {
59
+ if (index > 0)
60
+ result.push({ text: ' | ', tone: null });
61
+ const match = /^(.+?: )(~?(?:\d+(?:\.\d+)?|--)) tps$/.exec(part);
62
+ if (match) {
63
+ const display = match[2];
64
+ const rate = Number(display.startsWith('~') ? display.slice(1) : display);
65
+ result.push({ text: match[1], tone: null }, { text: display + ' tps', tone: display ? dscodeTpsTone(rate) : null });
66
+ continue;
67
+ }
68
+ // Only a recognised cache label at the very end of the line is tinted.
69
+ const labelled = index === parts.length - 1 ? /^(.*?)(\d+(?:\.\d+)?)%$/.exec(part) : null;
70
+ const cache = labelled && DSCode_CACHE_LABELS.includes(labelled[1].replace(/:\s*$/, '').trim().toLowerCase()) ? labelled : null;
71
+ if (cache) {
72
+ result.push({ text: cache[1], tone: null }, { text: cache[2] + '%', tone: dscodeCacheTone(Number(cache[2])) });
73
+ continue;
74
+ }
75
+ result.push({ text: part, tone: null });
76
+ }
77
+ return result;
78
+ }
79
+ /** The telemetry run as Ink text nodes, one per tinted segment. */
80
+ export function dscodeTelemetryNodes(value, key) {
81
+ return dscodeTelemetryParts(value).map((part, index) => createElement(Text, { key: key + 'p' + index, color: dscodeTpsInkColor(part.tone) }, part.text));
82
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * dscode: the welcome screen's pixel snowflake, its half-block row packing and
3
+ * the path folding that keeps a long directory inside the details column.
4
+ *
5
+ * @module dsh-code/dscode/welcome
6
+ */
7
+ import { singleLineText, truncateColumns } from '../render/text.mjs';
8
+ import { visibleColumns } from '../render/markdown.mjs';
9
+ export function welcomePath(path, width) {
10
+ const full = singleLineText(path || '');
11
+ if (visibleColumns(full) <= width)
12
+ return full;
13
+ const segments = full.split(/[\\/]/).filter(Boolean);
14
+ let suffix = '';
15
+ for (let index = segments.length - 1; index >= 0; index--) {
16
+ const candidate = '/' + segments[index] + suffix;
17
+ if (visibleColumns('…' + candidate) > width)
18
+ break;
19
+ suffix = candidate;
20
+ }
21
+ return suffix ? '…' + suffix : '…' + truncateColumns(segments.at(-1) || full, Math.max(1, width - 1));
22
+ }
23
+ /** Pixel rows of the welcome snowflake for terminals of 24+ rows. */
24
+ export const WELCOME_ART = [
25
+ "...........1.1..........",
26
+ "............1...........",
27
+ "............1...........",
28
+ ".........1..1..1........",
29
+ ".......1..22222..1......",
30
+ "...1...1....2....1...1..",
31
+ "..11...2....2....2...11.",
32
+ "....11.2...222...2.11...",
33
+ "......22.22.3.22.22.....",
34
+ ".....22.22.333.22.22....",
35
+ "....1...2.33332.2...1...",
36
+ "........2.33322.2.......",
37
+ "....1...2.33222.2...1...",
38
+ ".....22.22.222.22.22....",
39
+ "......22.22.3.22.22.....",
40
+ "....11.2...222...2.11...",
41
+ "..11...2....2....2...11.",
42
+ "...1...1....2....1...1..",
43
+ ".......1..22222..1......",
44
+ ".........1..1..1........",
45
+ "............1...........",
46
+ "............1...........",
47
+ "...........1.1..........",
48
+ "........................"
49
+ ];
50
+ /** The shorter snowflake used under 26 rows. */
51
+ export const WELCOME_ART_SMALL = [
52
+ "..........1.1.........",
53
+ "...........1..........",
54
+ "........1..1..1.......",
55
+ ".........1.2.1........",
56
+ "...1..1...222...1..1..",
57
+ "...1..1....2....1..1..",
58
+ "..1.1.2...222...2.1.1.",
59
+ ".....22.22.3.22.22....",
60
+ "....12.22.333.22.21...",
61
+ "...1...2.33332.2...1..",
62
+ ".......2.33322.2......",
63
+ "...1...2.33222.2...1..",
64
+ "....12.22.222.22.21...",
65
+ ".....22.22.3.22.22....",
66
+ "..1.1.2...222...2.1.1.",
67
+ "...1..1....2....1..1..",
68
+ "...1..1...222...1..1..",
69
+ ".........1.2.1........",
70
+ "........1..1..1.......",
71
+ "...........1..........",
72
+ "..........1.1.........",
73
+ "......................"
74
+ ];
75
+ export function welcomeArtRows(grid, tones) {
76
+ const rows = [];
77
+ for (let y = 0; y < grid.length; y += 2) {
78
+ const top = grid[y] || '';
79
+ const bottom = grid[y + 1] || '';
80
+ const segments = [];
81
+ for (let x = 0; x < Math.max(top.length, bottom.length); x++) {
82
+ const upper = tones[top[x]] || '';
83
+ const lower = tones[bottom[x]] || '';
84
+ const glyph = upper && lower ? upper === lower ? '\u2588' : '\u2580' : upper ? '\u2580' : lower ? '\u2584' : ' ';
85
+ const color = upper || lower;
86
+ const background = upper && lower && upper !== lower ? lower : '';
87
+ const last = segments[segments.length - 1];
88
+ if (last && last.glyph === glyph && last.color === color && last.background === background)
89
+ last.text += glyph;
90
+ else
91
+ segments.push({ glyph, text: glyph, color, background });
92
+ }
93
+ rows.push(segments);
94
+ }
95
+ return rows;
96
+ }
97
+ /** dscode: pad a label to a column width (CJK-safe). */
98
+ export function dscodePadEnd(text, width) {
99
+ return text + ' '.repeat(Math.max(1, width - visibleColumns(text)));
100
+ }