dsh-code 1.0.7 → 1.3.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/README.en.md +70 -24
- package/README.md +71 -25
- package/bin/deepseek.mjs +202 -39
- package/cordis.patch.yml +13 -4
- package/lib/index.mjs +5002 -1061
- package/lib/session-query.mjs +3 -2
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-B3orFUYz.mjs} +665 -20
- package/lib/types/app.d.ts +120 -63
- package/lib/types/attachments.d.ts +16 -7
- package/lib/types/authorization-panel.d.ts +3 -3
- package/lib/types/git-workflow.d.ts +91 -2
- package/lib/types/history.d.ts +10 -0
- package/lib/types/i18n.d.ts +39 -0
- package/lib/types/index.d.ts +74 -2
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +89 -32
- package/lib/types/language-panel.d.ts +12 -0
- package/lib/types/locales/en.d.ts +498 -0
- package/lib/types/locales/zh.d.ts +9 -0
- package/lib/types/mentions.d.ts +7 -3
- package/lib/types/models.d.ts +14 -0
- package/lib/types/panel-accent.d.ts +28 -0
- package/lib/types/rainbow.d.ts +69 -0
- package/lib/types/render/animations.d.ts +42 -0
- package/lib/types/render/inspector.d.ts +26 -0
- package/lib/types/render/lines.d.ts +21 -1
- package/lib/types/render/markdown.d.ts +1 -1
- package/lib/types/render/projection.d.ts +95 -4
- package/lib/types/render/status.d.ts +12 -9
- package/lib/types/render/text.d.ts +6 -0
- package/lib/types/render/usage.d.ts +113 -0
- package/lib/types/session-directory.d.ts +42 -1
- package/lib/types/session-switch.d.ts +8 -0
- package/lib/types/startup.d.ts +1 -1
- package/lib/types/terminal-title.d.ts +8 -0
- package/lib/types/theme-panel.d.ts +2 -2
- package/lib/types/theme.d.ts +271 -52
- package/lib/types/update-panel.d.ts +27 -6
- package/lib/types/update.d.ts +10 -1
- package/lib/types/version.d.ts +6 -3
- package/package.json +26 -7
- package/src/app.ts +1426 -627
- package/src/approval.ts +166 -166
- package/src/attachments.ts +65 -19
- package/src/authorization-panel.ts +24 -18
- package/src/editor-keys.ts +371 -371
- package/src/fork.ts +11 -7
- package/src/git-workflow.ts +229 -3
- package/src/history.ts +14 -0
- package/src/i18n.ts +68 -0
- package/src/index.ts +503 -128
- package/src/input-split.ts +27 -7
- package/src/kernel-panels.ts +528 -113
- package/src/keyboard.ts +5 -4
- package/src/language-panel.ts +53 -0
- package/src/locales/en.ts +538 -0
- package/src/locales/zh.ts +537 -0
- package/src/mentions.ts +8 -4
- package/src/models.ts +264 -212
- package/src/panel-accent.ts +41 -0
- package/src/presets.ts +1 -1
- package/src/provider-settings.ts +1 -1
- package/src/rainbow.ts +218 -0
- package/src/render/animations.ts +104 -6
- package/src/render/editor.ts +20 -20
- package/src/render/export.ts +116 -95
- package/src/render/inspector.ts +42 -0
- package/src/render/lines.ts +628 -415
- package/src/render/markdown.ts +15 -3
- package/src/render/projection.ts +429 -19
- package/src/render/status.ts +119 -62
- package/src/render/text.ts +15 -0
- package/src/render/tool-preview.ts +77 -77
- package/src/render/usage.ts +430 -0
- package/src/render/width.ts +2 -2
- package/src/session-directory.ts +90 -9
- package/src/session-query.ts +8 -4
- package/src/session-switch.ts +14 -0
- package/src/startup.ts +3 -3
- package/src/store.ts +19 -1
- package/src/subagents.ts +229 -229
- package/src/terminal-title.ts +22 -5
- package/src/theme-panel.ts +17 -21
- package/src/theme.ts +281 -33
- package/src/update-panel.ts +148 -31
- package/src/update.ts +19 -3
- package/src/version.ts +63 -20
- package/src/whale-glyph.ts +23 -23
package/src/approval.ts
CHANGED
|
@@ -1,166 +1,166 @@
|
|
|
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
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
16
|
-
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
17
|
-
import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
|
|
18
|
-
|
|
19
|
-
/** The answer values a client answerer may resolve with. */
|
|
20
|
-
export type ApprovalAnswer = 'allowed-once' | 'rejected'
|
|
21
|
-
|
|
22
|
-
/** One pending approval question, derived from the request for rendering. */
|
|
23
|
-
export interface PendingApproval {
|
|
24
|
-
/** The asker's human-readable explanation, or a generic fallback. */
|
|
25
|
-
headline: string
|
|
26
|
-
/** The tool the question is about. */
|
|
27
|
-
toolName: string
|
|
28
|
-
/** Command-line preview resolved from the paired streaming tool call. */
|
|
29
|
-
command: string
|
|
30
|
-
/** Resolve the ask; calling twice is inert (one-shot latch). */
|
|
31
|
-
answer(outcome: ApprovalAnswer): void
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** The pending-question snapshot the renderer subscribes to. */
|
|
35
|
-
export interface ApprovalSnapshot {
|
|
36
|
-
/** The question on screen (queue head), or undefined when none is asked. */
|
|
37
|
-
pending: PendingApproval | undefined
|
|
38
|
-
/** Presentational: an answer was submitted, the ask has not settled yet. */
|
|
39
|
-
answered: boolean
|
|
40
|
-
/** Further asks waiting behind the on-screen one (FIFO, Codex-style). */
|
|
41
|
-
queued: number
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Store the pending question lands in; the renderer reads, the answerer writes. */
|
|
45
|
-
export interface ApprovalStore {
|
|
46
|
-
/** Subscribe to pending-state changes; returns the unsubscribe function. */
|
|
47
|
-
subscribe(listener: () => void): () => void
|
|
48
|
-
/** Read the current snapshot (identity-stable between changes). */
|
|
49
|
-
getSnapshot(): ApprovalSnapshot
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Create the approval store and mount the answerer listener on the context.
|
|
54
|
-
* The listener claims only requests for `owns`-owned agents and defers every
|
|
55
|
-
* other request back into the waterfall (`next()`), so sibling answerers stay
|
|
56
|
-
* usable. An aborted ask never reaches the human. Plugin teardown removes the
|
|
57
|
-
* listener; the service then fails its own question closed.
|
|
58
|
-
* @param ctx - plugin context whose event bus carries `approval/request`.
|
|
59
|
-
* @param owns - agents this terminal answers for.
|
|
60
|
-
* @param preview - resolves a tool-call preview for a pending request (the
|
|
61
|
-
* request contract carries no arguments; the UI self-serves from the
|
|
62
|
-
* transcript projection via `callId`).
|
|
63
|
-
* @returns the store the renderer subscribes to.
|
|
64
|
-
*/
|
|
65
|
-
export function mountApprovalAnswerer(
|
|
66
|
-
ctx: Context,
|
|
67
|
-
owns: (agent: Agent) => boolean,
|
|
68
|
-
preview: (request: ApprovalRequest) => string,
|
|
69
|
-
): ApprovalStore {
|
|
70
|
-
/** One live ask: its pending view plus the one-shot settle plumbing. */
|
|
71
|
-
interface Slot {
|
|
72
|
-
readonly pending: PendingApproval
|
|
73
|
-
answered: boolean
|
|
74
|
-
}
|
|
75
|
-
const queue: Slot[] = []
|
|
76
|
-
let snapshot: ApprovalSnapshot = { pending: undefined, answered: false, queued: 0 }
|
|
77
|
-
const listeners = new Set<() => void>()
|
|
78
|
-
const publish = (): void => {
|
|
79
|
-
const head = queue[0]
|
|
80
|
-
snapshot = {
|
|
81
|
-
pending: head === undefined ? undefined : head.pending,
|
|
82
|
-
answered: head !== undefined && head.answered,
|
|
83
|
-
queued: Math.max(0, queue.length - 1),
|
|
84
|
-
}
|
|
85
|
-
for (const listener of listeners) listener()
|
|
86
|
-
}
|
|
87
|
-
const removeSlot = (slot: Slot): void => {
|
|
88
|
-
const at = queue.indexOf(slot)
|
|
89
|
-
if (at !== -1) queue.splice(at, 1)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
ctx.on('approval/request', (request: ApprovalRequest, next: () => Promise<ApprovalOutcome>) => {
|
|
93
|
-
if (!owns(request.agent)) return next()
|
|
94
|
-
// An already-aborted ask never reaches the human (mirrors the host bridge).
|
|
95
|
-
if (request.signal?.aborted === true) return Promise.resolve<ApprovalOutcome>('cancelled')
|
|
96
|
-
|
|
97
|
-
let resolved = false
|
|
98
|
-
let settle!: (outcome: ApprovalOutcome) => void
|
|
99
|
-
// Established BEFORE the abort listener mounts: a synchronous throw
|
|
100
|
-
// between the listener registration and a later construction site would
|
|
101
|
-
// otherwise leave a subsequent abort invoking an unassigned settle from
|
|
102
|
-
// inside the AbortSignal listener (an uncaughtException).
|
|
103
|
-
const settled = new Promise<ApprovalOutcome>((resolve) => {
|
|
104
|
-
settle = resolve
|
|
105
|
-
})
|
|
106
|
-
const signal = request.signal
|
|
107
|
-
const onAbort = (): void => withdraw()
|
|
108
|
-
// Detach on every settle so an answered ask never retains a listener on
|
|
109
|
-
// the tool call's signal (long turns ask many times; each ask must let go).
|
|
110
|
-
const detachAbort = (): void => {
|
|
111
|
-
if (signal !== undefined) signal.removeEventListener('abort', onAbort)
|
|
112
|
-
}
|
|
113
|
-
const withdraw = (): void => {
|
|
114
|
-
if (resolved) return
|
|
115
|
-
resolved = true
|
|
116
|
-
detachAbort()
|
|
117
|
-
removeSlot(slot)
|
|
118
|
-
publish()
|
|
119
|
-
// The service's signal race would conclude 'cancelled' anyway; settle
|
|
120
|
-
// the same way so this listener never dangles a pending promise.
|
|
121
|
-
settle('cancelled')
|
|
122
|
-
}
|
|
123
|
-
if (signal !== undefined) {
|
|
124
|
-
signal.addEventListener('abort', onAbort, { once: true })
|
|
125
|
-
}
|
|
126
|
-
const slot: Slot = {
|
|
127
|
-
answered: false,
|
|
128
|
-
pending: {
|
|
129
|
-
headline: request.reason ?? `tool ${request.toolName} asks for your approval`,
|
|
130
|
-
toolName: request.toolName,
|
|
131
|
-
command: preview(request),
|
|
132
|
-
answer: (outcome: ApprovalAnswer): void => {
|
|
133
|
-
// One-shot latch: a second keypress after submission is inert.
|
|
134
|
-
if (resolved) return
|
|
135
|
-
resolved = true
|
|
136
|
-
detachAbort()
|
|
137
|
-
slot.answered = true
|
|
138
|
-
publish()
|
|
139
|
-
settle(outcome)
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
}
|
|
143
|
-
queue.push(slot)
|
|
144
|
-
publish()
|
|
145
|
-
|
|
146
|
-
return settled.then((outcome) => {
|
|
147
|
-
if (outcome !== 'cancelled') {
|
|
148
|
-
removeSlot(slot)
|
|
149
|
-
publish()
|
|
150
|
-
}
|
|
151
|
-
return outcome
|
|
152
|
-
})
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
subscribe(listener: () => void): () => void {
|
|
157
|
-
listeners.add(listener)
|
|
158
|
-
return () => {
|
|
159
|
-
listeners.delete(listener)
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
getSnapshot(): ApprovalSnapshot {
|
|
163
|
-
return snapshot
|
|
164
|
-
},
|
|
165
|
-
}
|
|
166
|
-
}
|
|
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
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
16
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
17
|
+
import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
|
|
18
|
+
|
|
19
|
+
/** The answer values a client answerer may resolve with. */
|
|
20
|
+
export type ApprovalAnswer = 'allowed-once' | 'rejected'
|
|
21
|
+
|
|
22
|
+
/** One pending approval question, derived from the request for rendering. */
|
|
23
|
+
export interface PendingApproval {
|
|
24
|
+
/** The asker's human-readable explanation, or a generic fallback. */
|
|
25
|
+
headline: string
|
|
26
|
+
/** The tool the question is about. */
|
|
27
|
+
toolName: string
|
|
28
|
+
/** Command-line preview resolved from the paired streaming tool call. */
|
|
29
|
+
command: string
|
|
30
|
+
/** Resolve the ask; calling twice is inert (one-shot latch). */
|
|
31
|
+
answer(outcome: ApprovalAnswer): void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The pending-question snapshot the renderer subscribes to. */
|
|
35
|
+
export interface ApprovalSnapshot {
|
|
36
|
+
/** The question on screen (queue head), or undefined when none is asked. */
|
|
37
|
+
pending: PendingApproval | undefined
|
|
38
|
+
/** Presentational: an answer was submitted, the ask has not settled yet. */
|
|
39
|
+
answered: boolean
|
|
40
|
+
/** Further asks waiting behind the on-screen one (FIFO, Codex-style). */
|
|
41
|
+
queued: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Store the pending question lands in; the renderer reads, the answerer writes. */
|
|
45
|
+
export interface ApprovalStore {
|
|
46
|
+
/** Subscribe to pending-state changes; returns the unsubscribe function. */
|
|
47
|
+
subscribe(listener: () => void): () => void
|
|
48
|
+
/** Read the current snapshot (identity-stable between changes). */
|
|
49
|
+
getSnapshot(): ApprovalSnapshot
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create the approval store and mount the answerer listener on the context.
|
|
54
|
+
* The listener claims only requests for `owns`-owned agents and defers every
|
|
55
|
+
* other request back into the waterfall (`next()`), so sibling answerers stay
|
|
56
|
+
* usable. An aborted ask never reaches the human. Plugin teardown removes the
|
|
57
|
+
* listener; the service then fails its own question closed.
|
|
58
|
+
* @param ctx - plugin context whose event bus carries `approval/request`.
|
|
59
|
+
* @param owns - agents this terminal answers for.
|
|
60
|
+
* @param preview - resolves a tool-call preview for a pending request (the
|
|
61
|
+
* request contract carries no arguments; the UI self-serves from the
|
|
62
|
+
* transcript projection via `callId`).
|
|
63
|
+
* @returns the store the renderer subscribes to.
|
|
64
|
+
*/
|
|
65
|
+
export function mountApprovalAnswerer(
|
|
66
|
+
ctx: Context,
|
|
67
|
+
owns: (agent: Agent) => boolean,
|
|
68
|
+
preview: (request: ApprovalRequest) => string,
|
|
69
|
+
): ApprovalStore {
|
|
70
|
+
/** One live ask: its pending view plus the one-shot settle plumbing. */
|
|
71
|
+
interface Slot {
|
|
72
|
+
readonly pending: PendingApproval
|
|
73
|
+
answered: boolean
|
|
74
|
+
}
|
|
75
|
+
const queue: Slot[] = []
|
|
76
|
+
let snapshot: ApprovalSnapshot = { pending: undefined, answered: false, queued: 0 }
|
|
77
|
+
const listeners = new Set<() => void>()
|
|
78
|
+
const publish = (): void => {
|
|
79
|
+
const head = queue[0]
|
|
80
|
+
snapshot = {
|
|
81
|
+
pending: head === undefined ? undefined : head.pending,
|
|
82
|
+
answered: head !== undefined && head.answered,
|
|
83
|
+
queued: Math.max(0, queue.length - 1),
|
|
84
|
+
}
|
|
85
|
+
for (const listener of listeners) listener()
|
|
86
|
+
}
|
|
87
|
+
const removeSlot = (slot: Slot): void => {
|
|
88
|
+
const at = queue.indexOf(slot)
|
|
89
|
+
if (at !== -1) queue.splice(at, 1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ctx.on('approval/request', (request: ApprovalRequest, next: () => Promise<ApprovalOutcome>) => {
|
|
93
|
+
if (!owns(request.agent)) return next()
|
|
94
|
+
// An already-aborted ask never reaches the human (mirrors the host bridge).
|
|
95
|
+
if (request.signal?.aborted === true) return Promise.resolve<ApprovalOutcome>('cancelled')
|
|
96
|
+
|
|
97
|
+
let resolved = false
|
|
98
|
+
let settle!: (outcome: ApprovalOutcome) => void
|
|
99
|
+
// Established BEFORE the abort listener mounts: a synchronous throw
|
|
100
|
+
// between the listener registration and a later construction site would
|
|
101
|
+
// otherwise leave a subsequent abort invoking an unassigned settle from
|
|
102
|
+
// inside the AbortSignal listener (an uncaughtException).
|
|
103
|
+
const settled = new Promise<ApprovalOutcome>((resolve) => {
|
|
104
|
+
settle = resolve
|
|
105
|
+
})
|
|
106
|
+
const signal = request.signal
|
|
107
|
+
const onAbort = (): void => withdraw()
|
|
108
|
+
// Detach on every settle so an answered ask never retains a listener on
|
|
109
|
+
// the tool call's signal (long turns ask many times; each ask must let go).
|
|
110
|
+
const detachAbort = (): void => {
|
|
111
|
+
if (signal !== undefined) signal.removeEventListener('abort', onAbort)
|
|
112
|
+
}
|
|
113
|
+
const withdraw = (): void => {
|
|
114
|
+
if (resolved) return
|
|
115
|
+
resolved = true
|
|
116
|
+
detachAbort()
|
|
117
|
+
removeSlot(slot)
|
|
118
|
+
publish()
|
|
119
|
+
// The service's signal race would conclude 'cancelled' anyway; settle
|
|
120
|
+
// the same way so this listener never dangles a pending promise.
|
|
121
|
+
settle('cancelled')
|
|
122
|
+
}
|
|
123
|
+
if (signal !== undefined) {
|
|
124
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
125
|
+
}
|
|
126
|
+
const slot: Slot = {
|
|
127
|
+
answered: false,
|
|
128
|
+
pending: {
|
|
129
|
+
headline: request.reason ?? `tool ${request.toolName} asks for your approval`,
|
|
130
|
+
toolName: request.toolName,
|
|
131
|
+
command: preview(request),
|
|
132
|
+
answer: (outcome: ApprovalAnswer): void => {
|
|
133
|
+
// One-shot latch: a second keypress after submission is inert.
|
|
134
|
+
if (resolved) return
|
|
135
|
+
resolved = true
|
|
136
|
+
detachAbort()
|
|
137
|
+
slot.answered = true
|
|
138
|
+
publish()
|
|
139
|
+
settle(outcome)
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
queue.push(slot)
|
|
144
|
+
publish()
|
|
145
|
+
|
|
146
|
+
return settled.then((outcome) => {
|
|
147
|
+
if (outcome !== 'cancelled') {
|
|
148
|
+
removeSlot(slot)
|
|
149
|
+
publish()
|
|
150
|
+
}
|
|
151
|
+
return outcome
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
subscribe(listener: () => void): () => void {
|
|
157
|
+
listeners.add(listener)
|
|
158
|
+
return () => {
|
|
159
|
+
listeners.delete(listener)
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
getSnapshot(): ApprovalSnapshot {
|
|
163
|
+
return snapshot
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/attachments.ts
CHANGED
|
@@ -52,17 +52,41 @@ export function looksLikeImagePath(path: string): boolean {
|
|
|
52
52
|
return IMAGE_EXTENSIONS.has(extname(path).toLowerCase())
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** Strip one layer of ASCII or Unicode quotes and shell-escaped spaces. */
|
|
56
|
+
export function unwrapDroppedPath(token: string): string {
|
|
57
|
+
const trimmed = token.trim()
|
|
58
|
+
const wrapped = /^[\u2018\u201C"'](.+)[\u2019\u201D"']$/u.exec(trimmed)
|
|
59
|
+
return (wrapped?.[1] ?? trimmed).replace(/\\ /gu, ' ')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Absolute/relative drop with a dotted leaf — including unquoted spaces. */
|
|
63
|
+
export function looksLikeFilesystemDrop(path: string): boolean {
|
|
64
|
+
if (path.startsWith('file://')) return true
|
|
65
|
+
if (!/^(?:\/|[A-Za-z]:[\\/]|\\\\|\.\.?\/)/u.test(path)) return false
|
|
66
|
+
return looksLikeImagePath(path) || /\.[A-Za-z0-9]{1,16}$/u.test(path)
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
* A composer draft that is a filesystem path, not a slash command.
|
|
71
|
+
* `/usage` stays a command; `/Users/foo.png` and `C:\temp\a.png` are drops.
|
|
72
|
+
*/
|
|
73
|
+
export function looksLikePathDraft(value: string): boolean {
|
|
74
|
+
const path = unwrapDroppedPath(value)
|
|
75
|
+
if (looksLikeFilesystemDrop(path)) return true
|
|
76
|
+
return /^\/(?:Users|home|tmp|var|etc|opt|mnt|Volumes)\//u.test(path)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse a paste or file-drop into image and file paths: image-suffixed
|
|
81
|
+
* tokens stay images, other path-shaped tokens ride as file attachments
|
|
82
|
+
* (0.1.5 file blocks), and anything that is neither leaves both empty —
|
|
83
|
+
* the caller then treats the paste as plain text.
|
|
60
84
|
*
|
|
61
85
|
* File tokens are held to an absolute-path-with-shape bar (drive/backslash
|
|
62
|
-
* or a
|
|
86
|
+
* or a dotted leaf after a separator): a dropped terminal path always
|
|
63
87
|
* carries one of those, while prose, slash commands, and option flags never
|
|
64
|
-
* do. A POSIX absolute path without
|
|
65
|
-
*
|
|
88
|
+
* do. A POSIX absolute path without a dotted leaf falls through as text —
|
|
89
|
+
* the @ mention route still attaches such files deliberately.
|
|
66
90
|
*/
|
|
67
91
|
export function parsePastedAttachmentPaths(input: string): { readonly images: readonly string[]; readonly files: readonly string[] } {
|
|
68
92
|
const text = input.trim()
|
|
@@ -73,27 +97,49 @@ export function parsePastedAttachmentPaths(input: string): { readonly images: re
|
|
|
73
97
|
/^[A-Za-z]:[\\/]/u.test(path)
|
|
74
98
|
|| /^\\\\/u.test(path)
|
|
75
99
|
|| (/^\/|^\.\.?\//u.test(path) && /\.[A-Za-z0-9]{1,16}$/u.test(path))
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
const token = match[1] ?? match[2] ?? match[3]
|
|
79
|
-
if (token === undefined) continue
|
|
80
|
-
let path = token
|
|
100
|
+
const classify = (raw: string): 'image' | 'file' | 'reject' => {
|
|
101
|
+
let path = unwrapDroppedPath(raw)
|
|
81
102
|
if (path.startsWith('file://')) {
|
|
82
103
|
try {
|
|
83
104
|
path = fileURLToPath(path)
|
|
84
105
|
} catch {
|
|
85
|
-
return
|
|
106
|
+
return 'reject'
|
|
107
|
+
}
|
|
108
|
+
} else if (/%[0-9A-Fa-f]{2}/u.test(path)) {
|
|
109
|
+
try {
|
|
110
|
+
path = decodeURIComponent(path)
|
|
111
|
+
} catch {
|
|
112
|
+
// Keep the raw token when it is not a valid percent-encoding.
|
|
86
113
|
}
|
|
87
|
-
if (looksLikeImagePath(path)) images.push(path)
|
|
88
|
-
else files.push(path)
|
|
89
|
-
continue
|
|
90
114
|
}
|
|
91
115
|
if (looksLikeImagePath(path)) {
|
|
92
116
|
images.push(path)
|
|
93
|
-
|
|
117
|
+
return 'image'
|
|
118
|
+
}
|
|
119
|
+
if (looksLikeDroppedFile(path) || looksLikeFilesystemDrop(path)) {
|
|
120
|
+
files.push(path)
|
|
121
|
+
return 'file'
|
|
94
122
|
}
|
|
95
|
-
|
|
96
|
-
|
|
123
|
+
return 'reject'
|
|
124
|
+
}
|
|
125
|
+
// A single dropped path with spaces often arrives unquoted (macOS, Windows
|
|
126
|
+
// Explorer, VS Code sendText). Use the whole paste only when there is one
|
|
127
|
+
// path start and the text actually contains spaces (or wrapping quotes) —
|
|
128
|
+
// two unquoted `C:\a.png D:\b.txt` tokens must still split.
|
|
129
|
+
const whole = unwrapDroppedPath(text)
|
|
130
|
+
const pathStarts = text.match(/(?:^|[\s"'])(?:\/|[A-Za-z]:[\\/]|\\\\|\.\.?\/|file:\/\/)/gu) ?? []
|
|
131
|
+
const spacedSingleton = pathStarts.length <= 1
|
|
132
|
+
&& looksLikeFilesystemDrop(whole)
|
|
133
|
+
&& (/\s/u.test(whole) || /^[\u2018\u201C"']/u.test(text))
|
|
134
|
+
if (spacedSingleton) {
|
|
135
|
+
classify(whole)
|
|
136
|
+
return { images, files }
|
|
137
|
+
}
|
|
138
|
+
const matcher = /"([^"]+)"|'([^']+)'|(\S+)/gu
|
|
139
|
+
for (const match of text.matchAll(matcher)) {
|
|
140
|
+
const token = match[1] ?? match[2] ?? match[3]
|
|
141
|
+
if (token === undefined) continue
|
|
142
|
+
if (classify(token) === 'reject') return { images: [], files: [] }
|
|
97
143
|
}
|
|
98
144
|
return { images, files }
|
|
99
145
|
}
|
|
@@ -13,7 +13,9 @@ import type { CredentialKey } from '@deepseek-ai/dsh-credentials'
|
|
|
13
13
|
import type { ProviderAuthorizationRow } from './authorization.ts'
|
|
14
14
|
import { panelViewport } from './render/inspector.ts'
|
|
15
15
|
import { displayText, singleLineText, truncateColumns } from './render/text.ts'
|
|
16
|
+
import { panelAccent } from './panel-accent.ts'
|
|
16
17
|
import { getPalette, inkColor } from './theme.ts'
|
|
18
|
+
import { t } from './i18n.ts'
|
|
17
19
|
|
|
18
20
|
interface PromptReply {
|
|
19
21
|
readonly resolve: (value: string) => void
|
|
@@ -73,14 +75,17 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
|
|
|
73
75
|
decline()
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
const propsRef = useRef(props)
|
|
79
|
+
propsRef.current = props
|
|
80
|
+
const rowKey = props.row.key
|
|
76
81
|
useEffect(() => () => {
|
|
77
82
|
controllerRef.current?.abort()
|
|
78
|
-
|
|
83
|
+
propsRef.current.cancel(rowKey)
|
|
79
84
|
const reply = replyRef.current
|
|
80
85
|
replyRef.current = undefined
|
|
81
86
|
reply?.detach()
|
|
82
87
|
reply?.reject(new AuthorizationDeclinedError())
|
|
83
|
-
}, [
|
|
88
|
+
}, [rowKey])
|
|
84
89
|
|
|
85
90
|
const start = (method: string): void => {
|
|
86
91
|
setPhase('running')
|
|
@@ -154,7 +159,7 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
|
|
|
154
159
|
setCursor(current => (current + 1) % props.row.methods.length)
|
|
155
160
|
return
|
|
156
161
|
}
|
|
157
|
-
if (key.return) start(props.row.methods[cursor]?.id ?? props.row.methods[0]
|
|
162
|
+
if (key.return) start(props.row.methods[cursor]?.id ?? props.row.methods[0].id)
|
|
158
163
|
return
|
|
159
164
|
}
|
|
160
165
|
|
|
@@ -182,7 +187,7 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
|
|
|
182
187
|
setPromptCursor(current => (current + 1) % prompt.options.length)
|
|
183
188
|
return
|
|
184
189
|
}
|
|
185
|
-
if (key.return) answer(prompt.options[promptCursor]?.id ?? prompt.options[0]
|
|
190
|
+
if (key.return) answer(prompt.options[promptCursor]?.id ?? prompt.options[0].id)
|
|
186
191
|
return
|
|
187
192
|
}
|
|
188
193
|
if (key.backspace || key.delete) {
|
|
@@ -197,7 +202,7 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
|
|
|
197
202
|
})
|
|
198
203
|
|
|
199
204
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
200
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
205
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.auth.compact'), viewport.contentColumns))
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
const rows: Array<{ key: string; text: string; color?: string; bold?: boolean }> = []
|
|
@@ -229,18 +234,19 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
|
|
|
229
234
|
rows.push({ key: 'draft', text: ` ${shown}▏`, color: inkColor(getPalette().text) })
|
|
230
235
|
}
|
|
231
236
|
} else {
|
|
232
|
-
rows.push({ key: 'waiting', text: '
|
|
237
|
+
rows.push({ key: 'waiting', text: ` ${t('panel.auth.waiting')}`, color: inkColor(getPalette().dim) })
|
|
233
238
|
}
|
|
234
239
|
if (copyState !== undefined) rows.push({ key: 'copy', text: ` ${singleLineText(copyState)}`, color: inkColor(copyState === 'copied' ? getPalette().success : getPalette().error) })
|
|
235
240
|
}
|
|
236
241
|
const visible = rows.slice(Math.max(0, rows.length - viewport.bodyRows))
|
|
237
242
|
const footer = phase === 'methods'
|
|
238
|
-
? '
|
|
239
|
-
: '
|
|
243
|
+
? t('panel.auth.methodsFooter')
|
|
244
|
+
: t('panel.auth.answerFooter')
|
|
245
|
+
const accent = panelAccent('auth-login', getPalette().brand)
|
|
240
246
|
return createElement(
|
|
241
247
|
Box,
|
|
242
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
243
|
-
createElement(Text, { color: inkColor(
|
|
248
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
249
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.auth.title', { provider: displayText(props.row.label) }), viewport.contentColumns)),
|
|
244
250
|
...visible.map(row => createElement(Text, { key: row.key, color: row.color, bold: row.bold, wrap: 'truncate-end' }, truncateColumns(row.text, viewport.contentColumns))),
|
|
245
251
|
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(footer, viewport.contentColumns)),
|
|
246
252
|
)
|
|
@@ -248,9 +254,9 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
|
|
|
248
254
|
|
|
249
255
|
export function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }: {
|
|
250
256
|
row: ProviderAuthorizationRow
|
|
251
|
-
confirm(row: ProviderAuthorizationRow)
|
|
252
|
-
done()
|
|
253
|
-
back()
|
|
257
|
+
confirm: (row: ProviderAuthorizationRow) => Promise<void>
|
|
258
|
+
done: () => void
|
|
259
|
+
back: () => void
|
|
254
260
|
}): ReactElement {
|
|
255
261
|
const stdout = useStdout().stdout
|
|
256
262
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -270,14 +276,14 @@ export function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }: {
|
|
|
270
276
|
setError(reason instanceof Error ? reason.message : String(reason))
|
|
271
277
|
})
|
|
272
278
|
})
|
|
273
|
-
if (viewport.maxHeight === 0 || viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
279
|
+
if (viewport.maxHeight === 0 || viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.auth.logoutCompact'), viewport.contentColumns))
|
|
274
280
|
return createElement(
|
|
275
281
|
Box,
|
|
276
282
|
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(getPalette().warn) },
|
|
277
|
-
createElement(Text, { color: inkColor(getPalette().warn), bold: true, wrap: 'truncate-end' }, truncateColumns('
|
|
278
|
-
createElement(Text, { wrap: 'truncate-end' }, truncateColumns(`
|
|
279
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
283
|
+
createElement(Text, { color: inkColor(getPalette().warn), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.auth.logoutTitle'), viewport.contentColumns)),
|
|
284
|
+
createElement(Text, { wrap: 'truncate-end' }, truncateColumns(` ${t('panel.auth.removeRecord', { provider: displayText(row.label) })}`, viewport.contentColumns)),
|
|
285
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.auth.unchanged')}`, viewport.contentColumns)),
|
|
280
286
|
error === undefined ? undefined : createElement(Text, { color: inkColor(getPalette().error), wrap: 'truncate-end' }, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns)),
|
|
281
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(busy ? 'working
|
|
287
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(busy ? t('panel.auth.working') : t('panel.auth.logoutFooter'), viewport.contentColumns)),
|
|
282
288
|
)
|
|
283
289
|
}
|