@toddzheng024/dscode-bundle 0.7.11 → 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 (154) 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/i18n/messages.mjs +6 -6
  5. package/plugins/session-metrics/view.mjs +16 -5
  6. package/plugins/tui-tools/index.mjs +14 -1
  7. package/vendor/tui/lib/app.mjs +6674 -0
  8. package/vendor/tui/lib/approval.mjs +122 -0
  9. package/vendor/tui/lib/attachments.mjs +218 -0
  10. package/vendor/tui/lib/authorization-panel.mjs +242 -0
  11. package/vendor/tui/lib/authorization.mjs +111 -0
  12. package/vendor/tui/lib/commands.mjs +123 -0
  13. package/vendor/tui/lib/dscode/chat.mjs +64 -0
  14. package/vendor/tui/lib/dscode/flags.mjs +32 -0
  15. package/vendor/tui/lib/dscode/model-search.mjs +68 -0
  16. package/vendor/tui/lib/dscode/paste.mjs +87 -0
  17. package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
  18. package/vendor/tui/lib/dscode/welcome.mjs +100 -0
  19. package/vendor/tui/lib/editor-keys.mjs +346 -0
  20. package/vendor/tui/lib/editor.mjs +50 -0
  21. package/vendor/tui/lib/fork.mjs +24 -0
  22. package/vendor/tui/lib/git-workflow.mjs +278 -0
  23. package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
  24. package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
  25. package/vendor/tui/lib/index.mjs +2198 -0
  26. package/vendor/tui/lib/input-split.mjs +170 -0
  27. package/vendor/tui/lib/internals.mjs +90 -0
  28. package/vendor/tui/lib/invariant.mjs +22 -0
  29. package/vendor/tui/lib/kernel-panels.mjs +1265 -0
  30. package/vendor/tui/lib/keyboard.mjs +283 -0
  31. package/vendor/tui/lib/language-panel.mjs +41 -0
  32. package/vendor/tui/lib/locales/en.mjs +495 -0
  33. package/vendor/tui/lib/locales/zh.mjs +495 -0
  34. package/vendor/tui/lib/mentions.mjs +150 -0
  35. package/vendor/tui/lib/model-capabilities.mjs +245 -0
  36. package/vendor/tui/lib/models.mjs +215 -0
  37. package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
  38. package/vendor/tui/lib/permissions.mjs +45 -0
  39. package/vendor/tui/lib/plugin-inventory.mjs +27 -0
  40. package/vendor/tui/lib/presets.mjs +46 -0
  41. package/vendor/tui/lib/provider-settings.mjs +600 -0
  42. package/vendor/tui/lib/questions.mjs +116 -0
  43. package/vendor/tui/lib/rainbow.mjs +181 -0
  44. package/vendor/tui/lib/render/animations.mjs +665 -0
  45. package/vendor/tui/lib/render/editor.mjs +437 -0
  46. package/vendor/tui/lib/render/export.mjs +119 -0
  47. package/vendor/tui/lib/render/fuzzy.mjs +79 -0
  48. package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
  49. package/vendor/tui/lib/render/inspector.mjs +93 -0
  50. package/vendor/tui/lib/render/lines.mjs +538 -0
  51. package/vendor/tui/lib/render/markdown.mjs +592 -0
  52. package/vendor/tui/lib/render/projection.mjs +1862 -0
  53. package/vendor/tui/lib/render/status.mjs +591 -0
  54. package/vendor/tui/lib/render/text.mjs +157 -0
  55. package/vendor/tui/lib/render/tool-detail.mjs +187 -0
  56. package/vendor/tui/lib/render/tool-preview.mjs +79 -0
  57. package/vendor/tui/lib/render/usage.mjs +336 -0
  58. package/vendor/tui/lib/render/width.mjs +191 -0
  59. package/vendor/tui/lib/session-directory.mjs +329 -0
  60. package/vendor/tui/lib/session-query.mjs +184 -0
  61. package/vendor/tui/lib/session-switch.mjs +51 -0
  62. package/vendor/tui/lib/settings-file.mjs +75 -0
  63. package/vendor/tui/lib/skills.mjs +119 -0
  64. package/vendor/tui/lib/startup.mjs +111 -0
  65. package/vendor/tui/lib/store.mjs +129 -0
  66. package/vendor/tui/lib/subagents.mjs +200 -0
  67. package/vendor/tui/lib/terminal-title.mjs +186 -0
  68. package/vendor/tui/lib/theme-panel.mjs +60 -0
  69. package/vendor/tui/lib/theme.mjs +379 -0
  70. package/vendor/tui/lib/update-panel.mjs +239 -0
  71. package/vendor/tui/lib/update.mjs +101 -0
  72. package/vendor/tui/lib/version.mjs +105 -0
  73. package/vendor/tui/lib/whale-glyph.mjs +20 -0
  74. package/vendor/tui/LICENSE +0 -21
  75. package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
  76. package/vendor/tui/dscode-email/cli.mjs +0 -32
  77. package/vendor/tui/dscode-email/contacts.mjs +0 -36
  78. package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
  79. package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
  80. package/vendor/tui/dscode-email/gmail.mjs +0 -194
  81. package/vendor/tui/dscode-email/imap.mjs +0 -136
  82. package/vendor/tui/dscode-email/inbox.d.mts +0 -25
  83. package/vendor/tui/dscode-email/inbox.mjs +0 -76
  84. package/vendor/tui/dscode-email/smtp.mjs +0 -69
  85. package/vendor/tui/dscode-email/store.mjs +0 -32
  86. package/vendor/tui/dscode-providers/catalog.mjs +0 -136
  87. package/vendor/tui/dscode-providers/effort.mjs +0 -35
  88. package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
  89. package/vendor/tui/index.mjs +0 -44483
  90. package/vendor/tui/invariant.mjs +0 -21
  91. package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
  92. package/vendor/tui/session-query.mjs +0 -150
  93. package/vendor/tui/startup.mjs +0 -109
  94. package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
  95. package/vendor/tui/types/app.d.ts +0 -348
  96. package/vendor/tui/types/approval.d.ts +0 -59
  97. package/vendor/tui/types/attachments.d.ts +0 -52
  98. package/vendor/tui/types/authorization-panel.d.ts +0 -22
  99. package/vendor/tui/types/authorization.d.ts +0 -36
  100. package/vendor/tui/types/commands.d.ts +0 -52
  101. package/vendor/tui/types/editor-keys.d.ts +0 -105
  102. package/vendor/tui/types/editor.d.ts +0 -6
  103. package/vendor/tui/types/fork.d.ts +0 -8
  104. package/vendor/tui/types/git-workflow.d.ts +0 -121
  105. package/vendor/tui/types/index.d.ts +0 -223
  106. package/vendor/tui/types/input-split.d.ts +0 -54
  107. package/vendor/tui/types/internals.d.ts +0 -26
  108. package/vendor/tui/types/invariant.d.ts +0 -15
  109. package/vendor/tui/types/kernel-panels.d.ts +0 -245
  110. package/vendor/tui/types/keyboard.d.ts +0 -80
  111. package/vendor/tui/types/language-panel.d.ts +0 -12
  112. package/vendor/tui/types/locales/en.d.ts +0 -450
  113. package/vendor/tui/types/locales/zh.d.ts +0 -9
  114. package/vendor/tui/types/mentions.d.ts +0 -85
  115. package/vendor/tui/types/model-capabilities.d.ts +0 -82
  116. package/vendor/tui/types/models.d.ts +0 -133
  117. package/vendor/tui/types/permissions.d.ts +0 -27
  118. package/vendor/tui/types/plugin-inventory.d.ts +0 -11
  119. package/vendor/tui/types/presets.d.ts +0 -22
  120. package/vendor/tui/types/provider-settings.d.ts +0 -239
  121. package/vendor/tui/types/questions.d.ts +0 -54
  122. package/vendor/tui/types/rainbow.d.ts +0 -69
  123. package/vendor/tui/types/render/animations.d.ts +0 -307
  124. package/vendor/tui/types/render/editor.d.ts +0 -163
  125. package/vendor/tui/types/render/export.d.ts +0 -9
  126. package/vendor/tui/types/render/fuzzy.d.ts +0 -21
  127. package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
  128. package/vendor/tui/types/render/inspector.d.ts +0 -62
  129. package/vendor/tui/types/render/lines.d.ts +0 -86
  130. package/vendor/tui/types/render/markdown.d.ts +0 -29
  131. package/vendor/tui/types/render/projection.d.ts +0 -587
  132. package/vendor/tui/types/render/status.d.ts +0 -196
  133. package/vendor/tui/types/render/text.d.ts +0 -64
  134. package/vendor/tui/types/render/tool-detail.d.ts +0 -94
  135. package/vendor/tui/types/render/tool-preview.d.ts +0 -28
  136. package/vendor/tui/types/render/usage.d.ts +0 -113
  137. package/vendor/tui/types/render/width.d.ts +0 -29
  138. package/vendor/tui/types/session-directory.d.ts +0 -190
  139. package/vendor/tui/types/session-query.d.ts +0 -92
  140. package/vendor/tui/types/session-switch.d.ts +0 -17
  141. package/vendor/tui/types/settings-file.d.ts +0 -41
  142. package/vendor/tui/types/skills.d.ts +0 -47
  143. package/vendor/tui/types/startup.d.ts +0 -65
  144. package/vendor/tui/types/store.d.ts +0 -58
  145. package/vendor/tui/types/subagents.d.ts +0 -70
  146. package/vendor/tui/types/terminal-title.d.ts +0 -66
  147. package/vendor/tui/types/theme-panel.d.ts +0 -24
  148. package/vendor/tui/types/theme.d.ts +0 -434
  149. package/vendor/tui/types/update-panel.d.ts +0 -49
  150. package/vendor/tui/types/update.d.ts +0 -75
  151. package/vendor/tui/types/version.d.ts +0 -19
  152. package/vendor/tui/types/whale-glyph.d.ts +0 -6
  153. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
  154. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The terminal approval answerer: one `approval/request` waterfall listener
3
+ * that renders the pending question as a y/n bar and resolves the decision
4
+ * back into the waterfall. Mirrors the web host's composer takeover — the
5
+ * service (audit pair, policy gate, fail-closed defaults) all live in
6
+ * dsh-base; this module only answers for agents this TUI owns.
7
+ *
8
+ * Vocabulary note: a client answerer may only ever resolve `'allowed-once'`
9
+ * or `'rejected'`; `'cancelled'` belongs to the request signal and
10
+ * `'unavailable'` to the fail-closed waterfall default.
11
+ *
12
+ * @module @deepseek-ai/dsh-code/approval
13
+ */
14
+ /**
15
+ * Create the approval store and mount the answerer listener on the context.
16
+ * The listener claims only requests for `owns`-owned agents and defers every
17
+ * other request back into the waterfall (`next()`), so sibling answerers stay
18
+ * usable. An aborted ask never reaches the human. Plugin teardown removes the
19
+ * listener; the service then fails its own question closed.
20
+ * @param ctx - plugin context whose event bus carries `approval/request`.
21
+ * @param owns - agents this terminal answers for.
22
+ * @param preview - resolves a tool-call preview for a pending request (the
23
+ * request contract carries no arguments; the UI self-serves from the
24
+ * transcript projection via `callId`).
25
+ * @returns the store the renderer subscribes to.
26
+ */
27
+ export function mountApprovalAnswerer(ctx, owns, preview) {
28
+ const queue = [];
29
+ let snapshot = { pending: undefined, answered: false, queued: 0 };
30
+ const listeners = new Set();
31
+ const publish = () => {
32
+ const head = queue[0];
33
+ snapshot = {
34
+ pending: head === undefined ? undefined : head.pending,
35
+ answered: head !== undefined && head.answered,
36
+ queued: Math.max(0, queue.length - 1),
37
+ };
38
+ for (const listener of listeners)
39
+ listener();
40
+ };
41
+ const removeSlot = (slot) => {
42
+ const at = queue.indexOf(slot);
43
+ if (at !== -1)
44
+ queue.splice(at, 1);
45
+ };
46
+ ctx.on('approval/request', (request, next) => {
47
+ if (!owns(request.agent))
48
+ return next();
49
+ // An already-aborted ask never reaches the human (mirrors the host bridge).
50
+ if (request.signal?.aborted === true)
51
+ return Promise.resolve('cancelled');
52
+ let resolved = false;
53
+ let settle;
54
+ // Established BEFORE the abort listener mounts: a synchronous throw
55
+ // between the listener registration and a later construction site would
56
+ // otherwise leave a subsequent abort invoking an unassigned settle from
57
+ // inside the AbortSignal listener (an uncaughtException).
58
+ const settled = new Promise((resolve) => {
59
+ settle = resolve;
60
+ });
61
+ const signal = request.signal;
62
+ const onAbort = () => withdraw();
63
+ // Detach on every settle so an answered ask never retains a listener on
64
+ // the tool call's signal (long turns ask many times; each ask must let go).
65
+ const detachAbort = () => {
66
+ if (signal !== undefined)
67
+ signal.removeEventListener('abort', onAbort);
68
+ };
69
+ const withdraw = () => {
70
+ if (resolved)
71
+ return;
72
+ resolved = true;
73
+ detachAbort();
74
+ removeSlot(slot);
75
+ publish();
76
+ // The service's signal race would conclude 'cancelled' anyway; settle
77
+ // the same way so this listener never dangles a pending promise.
78
+ settle('cancelled');
79
+ };
80
+ if (signal !== undefined) {
81
+ signal.addEventListener('abort', onAbort, { once: true });
82
+ }
83
+ const slot = {
84
+ answered: false,
85
+ pending: {
86
+ headline: request.reason ?? `tool ${request.toolName} asks for your approval`,
87
+ toolName: request.toolName,
88
+ command: preview(request),
89
+ answer: (outcome) => {
90
+ // One-shot latch: a second keypress after submission is inert.
91
+ if (resolved)
92
+ return;
93
+ resolved = true;
94
+ detachAbort();
95
+ slot.answered = true;
96
+ publish();
97
+ settle(outcome);
98
+ },
99
+ },
100
+ };
101
+ queue.push(slot);
102
+ publish();
103
+ return settled.then((outcome) => {
104
+ if (outcome !== 'cancelled') {
105
+ removeSlot(slot);
106
+ publish();
107
+ }
108
+ return outcome;
109
+ });
110
+ });
111
+ return {
112
+ subscribe(listener) {
113
+ listeners.add(listener);
114
+ return () => {
115
+ listeners.delete(listener);
116
+ };
117
+ },
118
+ getSnapshot() {
119
+ return snapshot;
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,218 @@
1
+ /** Terminal image- and file-attachment adapter over the Harness durable attachment service. */
2
+ import { open, readFile, stat } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { basename, extname, isAbsolute, resolve } from 'node:path';
5
+ /**
6
+ * Terminal-side file admission bounds. Upstream exposes image limits through
7
+ * the attachment service but no file limits (files ride verbatim storage);
8
+ * these keep a dragged file from silently ingesting a disk-sized blob and
9
+ * bound one message the way the image batch is bounded.
10
+ */
11
+ export const MAX_FILE_BYTES = 8 * 1024 * 1024;
12
+ export const MAX_FILES_PER_MESSAGE = 8;
13
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
14
+ /** Detect the supported encoded raster formats from bytes, never from a path suffix. */
15
+ export function detectImageMediaType(data) {
16
+ if (data.length >= 8 && data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47
17
+ && data[4] === 0x0d && data[5] === 0x0a && data[6] === 0x1a && data[7] === 0x0a)
18
+ return 'image/png';
19
+ if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff)
20
+ return 'image/jpeg';
21
+ if (data.length >= 6) {
22
+ const signature = String.fromCharCode(...data.subarray(0, 6));
23
+ if (signature === 'GIF87a' || signature === 'GIF89a')
24
+ return 'image/gif';
25
+ }
26
+ if (data.length >= 12
27
+ && String.fromCharCode(...data.subarray(0, 4)) === 'RIFF'
28
+ && String.fromCharCode(...data.subarray(8, 12)) === 'WEBP')
29
+ return 'image/webp';
30
+ return undefined;
31
+ }
32
+ /** Whether a path-like token is worth probing as an image attachment. */
33
+ export function looksLikeImagePath(path) {
34
+ return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
35
+ }
36
+ /**
37
+ * Parse a paste/drop into its image and file paths: image-suffixed tokens
38
+ * stay images, other path-shaped tokens ride as file attachments (0.1.5
39
+ * file blocks), and anything that is neither leaves both empty — the caller
40
+ * then treats the paste as plain text.
41
+ *
42
+ * File tokens are held to an absolute-path-with-shape bar (drive/backslash
43
+ * or a dot-suffixed leaf after a separator): a dropped terminal path always
44
+ * carries one of those, while prose, slash commands, and option flags never
45
+ * do. A POSIX absolute path without any dot-suffixed leaf falls through as
46
+ * text — the @ mention route still attaches such files deliberately.
47
+ */
48
+ export function parsePastedAttachmentPaths(input) {
49
+ const text = input.trim();
50
+ if (text === '')
51
+ return { images: [], files: [] };
52
+ const images = [];
53
+ const files = [];
54
+ const looksLikeDroppedFile = (path) => /^[A-Za-z]:[\\/]/u.test(path)
55
+ || /^\\\\/u.test(path)
56
+ || (/^\/|^\.\.?\//u.test(path) && /\.[A-Za-z0-9]{1,16}$/u.test(path));
57
+ const matcher = /"([^"]+)"|'([^']+)'|(\S+)/gu;
58
+ for (const match of text.matchAll(matcher)) {
59
+ const token = match[1] ?? match[2] ?? match[3];
60
+ if (token === undefined)
61
+ continue;
62
+ let path = token;
63
+ if (path.startsWith('file://')) {
64
+ try {
65
+ path = fileURLToPath(path);
66
+ }
67
+ catch {
68
+ return { images: [], files: [] };
69
+ }
70
+ if (looksLikeImagePath(path))
71
+ images.push(path);
72
+ else
73
+ files.push(path);
74
+ continue;
75
+ }
76
+ if (looksLikeImagePath(path)) {
77
+ images.push(path);
78
+ continue;
79
+ }
80
+ if (!looksLikeDroppedFile(path))
81
+ return { images: [], files: [] };
82
+ files.push(path);
83
+ }
84
+ return { images, files };
85
+ }
86
+ /** Validate path, byte size and encoded signature without writing an attachment object. */
87
+ export async function inspectImagePaths(paths, attachments, cwd = process.cwd()) {
88
+ if (paths.length === 0)
89
+ return [];
90
+ if (attachments === undefined)
91
+ throw new Error('image attachments are unavailable in this profile');
92
+ if (paths.length > attachments.imageLimits.maxImagesPerMessage) {
93
+ throw new Error(`too many images (${paths.length}; limit ${attachments.imageLimits.maxImagesPerMessage})`);
94
+ }
95
+ const inspected = [];
96
+ let totalBytes = 0;
97
+ for (const raw of paths) {
98
+ const path = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw);
99
+ let facts;
100
+ try {
101
+ facts = await stat(path);
102
+ }
103
+ catch (error) {
104
+ throw new Error(`cannot read image "${raw}": ${error instanceof Error ? error.message : String(error)}`);
105
+ }
106
+ if (!facts.isFile())
107
+ throw new Error(`image path is not a file: "${raw}"`);
108
+ if (facts.size > attachments.imageLimits.maxImageBytes) {
109
+ throw new Error(`image "${basename(path)}" is ${facts.size} bytes; limit ${attachments.imageLimits.maxImageBytes}`);
110
+ }
111
+ totalBytes += facts.size;
112
+ if (totalBytes > attachments.imageLimits.maxMessageImageBytes) {
113
+ throw new Error(`image batch is ${totalBytes} bytes; limit ${attachments.imageLimits.maxMessageImageBytes}`);
114
+ }
115
+ const handle = await open(path, 'r');
116
+ try {
117
+ const signature = new Uint8Array(16);
118
+ const { bytesRead } = await handle.read(signature, 0, signature.length, 0);
119
+ const mediaType = detectImageMediaType(signature.subarray(0, bytesRead));
120
+ if (mediaType === undefined || !attachments.imageLimits.mediaTypes.includes(mediaType)) {
121
+ throw new Error(`unsupported image file "${raw}" (expected PNG, JPEG, WebP, or GIF)`);
122
+ }
123
+ inspected.push({ path, name: basename(path), mediaType, bytes: facts.size });
124
+ }
125
+ finally {
126
+ await handle.close();
127
+ }
128
+ }
129
+ return inspected;
130
+ }
131
+ /** Read, validate, and persist an ordered image path list as model content blocks. */
132
+ export async function saveImagePaths(paths, attachments, signal) {
133
+ if (paths.length === 0)
134
+ return [];
135
+ if (attachments === undefined)
136
+ throw new Error('image attachments are unavailable in this profile');
137
+ const checkCancelled = () => {
138
+ if (signal?.aborted === true)
139
+ throw new Error('image submission cancelled');
140
+ };
141
+ const inputs = [];
142
+ for (const path of paths) {
143
+ checkCancelled();
144
+ let data;
145
+ try {
146
+ data = await readFile(path);
147
+ }
148
+ catch (error) {
149
+ throw new Error(`cannot read image "${path}": ${error instanceof Error ? error.message : String(error)}`);
150
+ }
151
+ const mediaType = detectImageMediaType(data);
152
+ if (mediaType === undefined)
153
+ throw new Error(`unsupported image file "${path}" (expected PNG, JPEG, WebP, or GIF)`);
154
+ inputs.push({ data, mediaType, name: basename(path) });
155
+ }
156
+ checkCancelled();
157
+ const refs = await attachments.saveImages(inputs);
158
+ checkCancelled();
159
+ return refs.map(attachment => ({ type: 'image', attachment }));
160
+ }
161
+ /** Validate path and byte size for non-image file attachments without writing. */
162
+ export async function inspectFilePaths(paths, attachments, cwd = process.cwd()) {
163
+ if (paths.length === 0)
164
+ return [];
165
+ if (attachments === undefined)
166
+ throw new Error('file attachments are unavailable in this profile');
167
+ if (paths.length > MAX_FILES_PER_MESSAGE) {
168
+ throw new Error(`too many files (${paths.length}; limit ${MAX_FILES_PER_MESSAGE})`);
169
+ }
170
+ const inspected = [];
171
+ for (const raw of paths) {
172
+ const path = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw);
173
+ let facts;
174
+ try {
175
+ facts = await stat(path);
176
+ }
177
+ catch (error) {
178
+ throw new Error(`cannot read file "${raw}": ${error instanceof Error ? error.message : String(error)}`);
179
+ }
180
+ if (!facts.isFile())
181
+ throw new Error(`file path is not a file: "${raw}"`);
182
+ if (facts.size > MAX_FILE_BYTES) {
183
+ throw new Error(`file "${basename(path)}" is ${facts.size} bytes; limit ${MAX_FILE_BYTES}`);
184
+ }
185
+ inspected.push({ path, name: basename(path), bytes: facts.size });
186
+ }
187
+ return inspected;
188
+ }
189
+ /** Read and persist an ordered non-image file path list as model file blocks. */
190
+ export async function saveFilePaths(paths, attachments, signal) {
191
+ if (paths.length === 0)
192
+ return [];
193
+ if (attachments === undefined)
194
+ throw new Error('file attachments are unavailable in this profile');
195
+ // The bounds are re-checked here so a draft inspected earlier still guards
196
+ // the actual read at submission time.
197
+ await inspectFilePaths(paths, attachments);
198
+ const checkCancelled = () => {
199
+ if (signal?.aborted === true)
200
+ throw new Error('file submission cancelled');
201
+ };
202
+ const inputs = [];
203
+ for (const path of paths) {
204
+ checkCancelled();
205
+ let data;
206
+ try {
207
+ data = await readFile(path);
208
+ }
209
+ catch (error) {
210
+ throw new Error(`cannot read file "${path}": ${error instanceof Error ? error.message : String(error)}`);
211
+ }
212
+ inputs.push({ data, name: basename(path) });
213
+ }
214
+ checkCancelled();
215
+ const refs = await Promise.all(inputs.map(input => attachments.saveFile(input)));
216
+ checkCancelled();
217
+ return refs.map(attachment => ({ type: 'file', attachment }));
218
+ }
@@ -0,0 +1,242 @@
1
+ /** Bounded Ink surfaces for provider login and logout. */
2
+ import { createElement, useEffect, useRef, useState } from 'react';
3
+ import { Box, Text, useInput, useStdout } from 'ink';
4
+ import { AuthorizationDeclinedError, } from '@deepseek-ai/dsh-authorization';
5
+ import { panelViewport } from './render/inspector.mjs';
6
+ import { displayText, singleLineText, truncateColumns } from './render/text.mjs';
7
+ import { panelAccent } from './panel-accent.mjs';
8
+ import { getPalette, inkColor } from './theme.mjs';
9
+ import { t } from './i18n.mjs';
10
+ /** Run one upstream authorization flow without letting notices or prompts exceed the panel budget. */
11
+ export function ProviderAuthorizationPanel(props) {
12
+ const stdout = useStdout().stdout;
13
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
14
+ const [phase, setPhase] = useState('methods');
15
+ const [cursor, setCursor] = useState(0);
16
+ const [notices, setNotices] = useState([]);
17
+ const [prompt, setPrompt] = useState(undefined);
18
+ const [draft, setDraft] = useState('');
19
+ const [promptCursor, setPromptCursor] = useState(0);
20
+ const [error, setError] = useState(undefined);
21
+ const [copyState, setCopyState] = useState(undefined);
22
+ const controllerRef = useRef(undefined);
23
+ const replyRef = useRef(undefined);
24
+ const openedUrls = useRef(new Set());
25
+ const clearReply = () => {
26
+ replyRef.current?.detach();
27
+ replyRef.current = undefined;
28
+ setPrompt(undefined);
29
+ setDraft('');
30
+ setPromptCursor(0);
31
+ };
32
+ const decline = () => {
33
+ const reply = replyRef.current;
34
+ clearReply();
35
+ reply?.reject(new AuthorizationDeclinedError());
36
+ };
37
+ const stop = () => {
38
+ controllerRef.current?.abort();
39
+ controllerRef.current = undefined;
40
+ props.cancel(props.row.key);
41
+ decline();
42
+ };
43
+ useEffect(() => () => {
44
+ controllerRef.current?.abort();
45
+ props.cancel(props.row.key);
46
+ const reply = replyRef.current;
47
+ replyRef.current = undefined;
48
+ reply?.detach();
49
+ reply?.reject(new AuthorizationDeclinedError());
50
+ }, [props.row.key]);
51
+ const start = (method) => {
52
+ setPhase('running');
53
+ setError(undefined);
54
+ setNotices([]);
55
+ setCopyState(undefined);
56
+ const controller = new AbortController();
57
+ controllerRef.current = controller;
58
+ const interaction = {
59
+ notify(notice) {
60
+ setNotices(current => [...current.slice(-19), notice]);
61
+ if (notice.url !== undefined && !openedUrls.current.has(notice.url)) {
62
+ openedUrls.current.add(notice.url);
63
+ props.openUrl(notice.url);
64
+ }
65
+ },
66
+ prompt(next) {
67
+ return new Promise((resolve, reject) => {
68
+ const onWithdraw = () => {
69
+ if (replyRef.current?.reject !== reject)
70
+ return;
71
+ clearReply();
72
+ reject(new Error('authorization prompt was withdrawn'));
73
+ };
74
+ next.signal?.addEventListener('abort', onWithdraw, { once: true });
75
+ replyRef.current = {
76
+ resolve,
77
+ reject,
78
+ detach: () => next.signal?.removeEventListener('abort', onWithdraw),
79
+ };
80
+ setPrompt(next);
81
+ setDraft('');
82
+ setPromptCursor(0);
83
+ });
84
+ },
85
+ };
86
+ void props.begin(props.row, method, interaction, controller.signal).then((status) => {
87
+ controllerRef.current = undefined;
88
+ clearReply();
89
+ if (status === 'authorized')
90
+ props.done();
91
+ else
92
+ props.back();
93
+ }, (reason) => {
94
+ controllerRef.current = undefined;
95
+ clearReply();
96
+ if (controller.signal.aborted) {
97
+ props.back();
98
+ return;
99
+ }
100
+ setError(reason instanceof Error ? reason.message : String(reason));
101
+ setPhase('methods');
102
+ });
103
+ };
104
+ const answer = (value) => {
105
+ const reply = replyRef.current;
106
+ clearReply();
107
+ reply?.resolve(value);
108
+ };
109
+ useInput((input, key) => {
110
+ if (phase === 'methods') {
111
+ if (key.escape || input === 'q') {
112
+ props.back();
113
+ return;
114
+ }
115
+ if (props.row.methods.length === 0)
116
+ return;
117
+ if (key.upArrow) {
118
+ setCursor(current => (current + props.row.methods.length - 1) % props.row.methods.length);
119
+ return;
120
+ }
121
+ if (key.downArrow) {
122
+ setCursor(current => (current + 1) % props.row.methods.length);
123
+ return;
124
+ }
125
+ if (key.return)
126
+ start(props.row.methods[cursor]?.id ?? props.row.methods[0].id);
127
+ return;
128
+ }
129
+ if (key.escape) {
130
+ stop();
131
+ props.back();
132
+ return;
133
+ }
134
+ const copyValue = notices.at(-1)?.code ?? notices.at(-1)?.url;
135
+ if ((input === 'c' || input === 'C') && copyValue !== undefined) {
136
+ void props.copy(copyValue).then(() => setCopyState('copied'), reason => setCopyState(`copy failed: ${reason instanceof Error ? reason.message : String(reason)}`));
137
+ return;
138
+ }
139
+ if (prompt === undefined)
140
+ return;
141
+ if (prompt.kind === 'select') {
142
+ if (prompt.options.length === 0)
143
+ return;
144
+ if (key.upArrow) {
145
+ setPromptCursor(current => (current + prompt.options.length - 1) % prompt.options.length);
146
+ return;
147
+ }
148
+ if (key.downArrow) {
149
+ setPromptCursor(current => (current + 1) % prompt.options.length);
150
+ return;
151
+ }
152
+ if (key.return)
153
+ answer(prompt.options[promptCursor]?.id ?? prompt.options[0].id);
154
+ return;
155
+ }
156
+ if (key.backspace || key.delete) {
157
+ setDraft(current => [...current].slice(0, -1).join(''));
158
+ return;
159
+ }
160
+ if (key.return) {
161
+ if (draft.trim() !== '')
162
+ answer(draft);
163
+ return;
164
+ }
165
+ if (input !== '' && !key.ctrl && !key.meta)
166
+ setDraft(current => current + input);
167
+ });
168
+ if (viewport.maxHeight === 0 || viewport.compact) {
169
+ return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.auth.compact'), viewport.contentColumns));
170
+ }
171
+ const rows = [];
172
+ if (phase === 'methods') {
173
+ if (error !== undefined)
174
+ rows.push({ key: 'error', text: ` ${singleLineText(error)}`, color: inkColor(getPalette().error) });
175
+ props.row.methods.forEach((method, index) => {
176
+ rows.push({
177
+ key: method.id,
178
+ text: `${index === cursor ? '› ' : ' '}${displayText(method.label)}`,
179
+ color: inkColor(index === cursor ? getPalette().brandBright : getPalette().dim),
180
+ });
181
+ });
182
+ }
183
+ else {
184
+ notices.forEach((notice, index) => {
185
+ rows.push({ key: `notice-${index}`, text: ` ${displayText(notice.message)}` });
186
+ if (notice.url !== undefined)
187
+ rows.push({ key: `url-${index}`, text: ` ${displayText(notice.url)}`, color: inkColor(getPalette().brandBright) });
188
+ if (notice.code !== undefined)
189
+ rows.push({ key: `code-${index}`, text: ` code ${displayText(notice.code)}`, color: inkColor(getPalette().success), bold: true });
190
+ });
191
+ if (prompt !== undefined) {
192
+ rows.push({ key: 'prompt', text: ` ${displayText(prompt.message)}`, color: inkColor(getPalette().brandBright) });
193
+ if (prompt.kind === 'select') {
194
+ prompt.options.forEach((option, index) => rows.push({
195
+ key: `option-${option.id}`,
196
+ text: `${index === promptCursor ? '› ' : ' '}${displayText(option.label)}${option.description === undefined ? '' : ` · ${displayText(option.description)}`}`,
197
+ color: inkColor(index === promptCursor ? getPalette().brandBright : getPalette().dim),
198
+ }));
199
+ }
200
+ else {
201
+ const shown = prompt.kind === 'secret' ? '•'.repeat([...draft].length) : displayText(draft);
202
+ rows.push({ key: 'draft', text: ` ${shown}▏`, color: inkColor(getPalette().text) });
203
+ }
204
+ }
205
+ else {
206
+ rows.push({ key: 'waiting', text: ` ${t('panel.auth.waiting')}`, color: inkColor(getPalette().dim) });
207
+ }
208
+ if (copyState !== undefined)
209
+ rows.push({ key: 'copy', text: ` ${singleLineText(copyState)}`, color: inkColor(copyState === 'copied' ? getPalette().success : getPalette().error) });
210
+ }
211
+ const visible = rows.slice(Math.max(0, rows.length - viewport.bodyRows));
212
+ const footer = phase === 'methods'
213
+ ? t('panel.auth.methodsFooter')
214
+ : t('panel.auth.answerFooter');
215
+ const accent = panelAccent('auth-login', getPalette().brand);
216
+ return createElement(Box, { flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) }, createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.auth.title', { provider: displayText(props.row.label) }), viewport.contentColumns)), ...visible.map(row => createElement(Text, { key: row.key, color: row.color, bold: row.bold, wrap: 'truncate-end' }, truncateColumns(row.text, viewport.contentColumns))), createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(footer, viewport.contentColumns)));
217
+ }
218
+ export function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }) {
219
+ const stdout = useStdout().stdout;
220
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
221
+ const [busy, setBusy] = useState(false);
222
+ const [error, setError] = useState(undefined);
223
+ useInput((input, key) => {
224
+ if (busy)
225
+ return;
226
+ if (key.escape || input === 'n' || input === 'N') {
227
+ back();
228
+ return;
229
+ }
230
+ if (input !== 'y' && input !== 'Y')
231
+ return;
232
+ setBusy(true);
233
+ setError(undefined);
234
+ void confirm(row).then(done, (reason) => {
235
+ setBusy(false);
236
+ setError(reason instanceof Error ? reason.message : String(reason));
237
+ });
238
+ });
239
+ if (viewport.maxHeight === 0 || viewport.compact)
240
+ return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.auth.logoutCompact'), viewport.contentColumns));
241
+ return createElement(Box, { flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(getPalette().warn) }, createElement(Text, { color: inkColor(getPalette().warn), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.auth.logoutTitle'), viewport.contentColumns)), createElement(Text, { wrap: 'truncate-end' }, truncateColumns(` ${t('panel.auth.removeRecord', { provider: displayText(row.label) })}`, viewport.contentColumns)), createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.auth.unchanged')}`, viewport.contentColumns)), error === undefined ? undefined : createElement(Text, { color: inkColor(getPalette().error), wrap: 'truncate-end' }, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns)), createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(busy ? t('panel.auth.working') : t('panel.auth.logoutFooter'), viewport.contentColumns)));
242
+ }