dsh-code 0.9.1 → 1.0.1
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 +278 -249
- package/README.md +131 -102
- package/bin/deepseek.mjs +100 -6
- package/cordis.patch.yml +36 -1
- package/lib/index.mjs +3055 -819
- package/lib/startup.mjs +21 -11
- package/lib/{theme-BEi4i_aN.mjs → theme-DCT8Y2xf.mjs} +13 -9
- package/lib/types/app.d.ts +84 -16
- package/lib/types/attachments.d.ts +20 -0
- package/lib/types/authorization-panel.d.ts +22 -0
- package/lib/types/authorization.d.ts +36 -0
- package/lib/types/editor.d.ts +6 -0
- package/lib/types/fork.d.ts +8 -0
- package/lib/types/git-workflow.d.ts +23 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/kernel-panels.d.ts +39 -0
- package/lib/types/keyboard.d.ts +41 -0
- package/lib/types/mentions.d.ts +30 -38
- package/lib/types/models.d.ts +3 -1
- package/lib/types/permissions.d.ts +4 -14
- package/lib/types/presets.d.ts +5 -20
- package/lib/types/provider-settings.d.ts +16 -0
- package/lib/types/render/animations.d.ts +10 -39
- package/lib/types/render/editor.d.ts +137 -0
- package/lib/types/render/export.d.ts +1 -1
- package/lib/types/render/lines.d.ts +6 -2
- package/lib/types/render/markdown.d.ts +3 -1
- package/lib/types/render/projection.d.ts +29 -3
- package/lib/types/render/status.d.ts +6 -13
- package/lib/types/session-directory.d.ts +1 -3
- package/lib/types/startup.d.ts +14 -11
- package/lib/types/store.d.ts +11 -9
- package/lib/types/subagents.d.ts +3 -3
- package/lib/types/theme.d.ts +14 -1
- package/lib/types/version.d.ts +15 -2
- package/package.json +159 -141
- package/src/app.ts +1490 -663
- package/src/attachments.ts +128 -0
- package/src/authorization-panel.ts +285 -0
- package/src/authorization.ts +147 -0
- package/src/editor.ts +51 -0
- package/src/fork.ts +31 -0
- package/src/git-workflow.ts +87 -0
- package/src/index.ts +1523 -1374
- package/src/internals.ts +14 -1
- package/src/kernel-panels.ts +914 -798
- package/src/keyboard.ts +126 -0
- package/src/mentions.ts +78 -117
- package/src/models.ts +20 -14
- package/src/permissions.ts +5 -13
- package/src/presets.ts +6 -22
- package/src/provider-settings.ts +95 -1
- package/src/render/animations.ts +420 -450
- package/src/render/editor.ts +398 -0
- package/src/render/export.ts +79 -79
- package/src/render/lines.ts +342 -236
- package/src/render/markdown.ts +99 -26
- package/src/render/projection.ts +106 -19
- package/src/render/status.ts +713 -650
- package/src/render/text.ts +150 -150
- package/src/render/tool-detail.ts +3 -1
- package/src/session-directory.ts +4 -4
- package/src/startup.ts +136 -119
- package/src/store.ts +23 -11
- package/src/subagents.ts +13 -5
- package/src/theme.ts +214 -206
- package/src/version.ts +58 -1
package/src/keyboard.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard enhancement protocol (Codex `keyboard_modes` parity) and the
|
|
3
|
+
* kitty CSI-u normalization layer.
|
|
4
|
+
*
|
|
5
|
+
* The TUI pushes the kitty keyboard protocol with DISAMBIGUATE_ESCAPE_CODES
|
|
6
|
+
* and REPORT_ALTERNATE_KEYS (flags 1|4 = `\x1b[>5u`). Event types are
|
|
7
|
+
* deliberately NOT requested: Ink 5's parser cannot decode the
|
|
8
|
+
* `:event-type` suffix, and repeat/release reporting buys this surface
|
|
9
|
+
* nothing.
|
|
10
|
+
*
|
|
11
|
+
* Ink 5 also cannot parse most CSI-u forms at all — they fall through its
|
|
12
|
+
* regex as unnamed sequences and get INSERTED AS DRAFT TEXT. The composer's
|
|
13
|
+
* stdin read patch therefore rewrites every CSI-u form it can decode back
|
|
14
|
+
* to the legacy byte or canonical sequence the existing key handling
|
|
15
|
+
* already understands, before Ink ever parses the chunk.
|
|
16
|
+
* @module @deepseek-ai/dsh-code/keyboard
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Push keyboard enhancement (modifyOtherKeys off, kitty flags 1|4). */
|
|
20
|
+
export const KEYBOARD_ENHANCE_ENABLE = '\x1b[>4;0m\x1b[>5u'
|
|
21
|
+
|
|
22
|
+
/** Pop the enhancement stack and reset modifyOtherKeys (exit path). */
|
|
23
|
+
export const KEYBOARD_ENHANCE_DISABLE = '\x1b[<u\x1b[>4;0m'
|
|
24
|
+
|
|
25
|
+
/** Enable bracketed paste reporting. */
|
|
26
|
+
export const BRACKETED_PASTE_ENABLE = '\x1b[?2004h'
|
|
27
|
+
|
|
28
|
+
/** Disable bracketed paste reporting. */
|
|
29
|
+
export const BRACKETED_PASTE_DISABLE = '\x1b[?2004l'
|
|
30
|
+
|
|
31
|
+
/** Bracketed paste markers as Ink delivers them (it strips the leading ESC). */
|
|
32
|
+
export const PASTE_START_MARKER = '[200~'
|
|
33
|
+
export const PASTE_END_MARKER = '[201~'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Remove bracketed paste markers from one input chunk. Panel drafts accept raw
|
|
37
|
+
* `input` text, where an unhandled paste would otherwise persist the literal
|
|
38
|
+
* "[200~"/"[201~" markers Ink leaves after stripping the ESC byte.
|
|
39
|
+
*/
|
|
40
|
+
export function stripPasteMarkers(text: string): string {
|
|
41
|
+
return text
|
|
42
|
+
.replaceAll(`\x1b${PASTE_START_MARKER}`, '')
|
|
43
|
+
.replaceAll(`\x1b${PASTE_END_MARKER}`, '')
|
|
44
|
+
.replaceAll(PASTE_START_MARKER, '')
|
|
45
|
+
.replaceAll(PASTE_END_MARKER, '')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One decoded CSI-u keypress: key code, 1-based modifier param, alternate code. */
|
|
49
|
+
interface CsiUKey {
|
|
50
|
+
code: number
|
|
51
|
+
modifiers: number
|
|
52
|
+
alternate?: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Match one CSI-u sequence (code, optional ;modifiers, then :event or ;alternate). */
|
|
56
|
+
const CSI_U_SOURCE = '\x1b\\[(\\d+)(?:;(\\d+))?(?:[:;](\\d+))?u'
|
|
57
|
+
|
|
58
|
+
/** Legacy equivalent for one decoded CSI-u key, or undefined to pass through. */
|
|
59
|
+
function legacyForKey(key: CsiUKey): string | undefined {
|
|
60
|
+
const bits = Math.max(0, key.modifiers - 1)
|
|
61
|
+
const shift = (bits & 1) !== 0
|
|
62
|
+
const alt = (bits & 2) !== 0
|
|
63
|
+
const ctrl = (bits & 4) !== 0
|
|
64
|
+
if (key.code === 13) {
|
|
65
|
+
// Modified Enter has no dedicated composer behavior. Preserve the legacy
|
|
66
|
+
// Ctrl/Alt bytes and collapse every other form to ordinary Enter.
|
|
67
|
+
if (ctrl) return '\n'
|
|
68
|
+
if (alt) return '\x1b\r'
|
|
69
|
+
return '\r'
|
|
70
|
+
}
|
|
71
|
+
if (key.code === 27) return '\x1b'
|
|
72
|
+
if (key.code === 9) return shift ? '\x1b[Z' : '\t'
|
|
73
|
+
if (key.code === 127) return alt || ctrl ? '\x1b\x7f' : '\x7f'
|
|
74
|
+
// Kitty disambiguate mode reports the six legacy functional keys as CSI u
|
|
75
|
+
// codes 1-6 (Home, Insert, Delete, End, PageUp, PageDown). Ink 5 cannot
|
|
76
|
+
// parse these forms and would insert literal "[3u" text into the draft, so
|
|
77
|
+
// rewrite them to the legacy sequences the input layer already annotates.
|
|
78
|
+
// The modifier parameter passes through: kitty and xterm share the same
|
|
79
|
+
// 1+bit-field encoding (shift 2, alt 3, ctrl 5, ...). Lock-key bits are
|
|
80
|
+
// dropped because the legacy sequences cannot express them.
|
|
81
|
+
if (key.code >= 1 && key.code <= 6) {
|
|
82
|
+
const mask = (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0)
|
|
83
|
+
const mods = mask === 0 ? '' : `;${mask + 1}`
|
|
84
|
+
if (key.code === 1) return mods === '' ? '\x1b[H' : `\x1b[1${mods}H`
|
|
85
|
+
if (key.code === 4) return mods === '' ? '\x1b[F' : `\x1b[1${mods}F`
|
|
86
|
+
return `\x1b[${key.code}${mods}~`
|
|
87
|
+
}
|
|
88
|
+
if (key.code >= 97 && key.code <= 122) {
|
|
89
|
+
const letter = String.fromCodePoint(key.code)
|
|
90
|
+
if (ctrl) return String.fromCodePoint(key.code - 96)
|
|
91
|
+
if (alt) return '\x1b' + letter
|
|
92
|
+
if (shift) return String.fromCodePoint(key.alternate ?? key.code - 32)
|
|
93
|
+
return letter
|
|
94
|
+
}
|
|
95
|
+
if (key.code >= 65 && key.code <= 90) {
|
|
96
|
+
if (ctrl) return String.fromCodePoint(key.code + 32 - 96)
|
|
97
|
+
if (alt) return '\x1b' + String.fromCodePoint(key.code + 32)
|
|
98
|
+
return String.fromCodePoint(key.code)
|
|
99
|
+
}
|
|
100
|
+
if (key.code >= 32 && key.code <= 126 && key.alternate !== undefined) {
|
|
101
|
+
const base = key.alternate >= 97 && key.alternate <= 122 ? key.alternate : key.code
|
|
102
|
+
if (ctrl && base - 96 >= 1 && base - 96 <= 26) return String.fromCodePoint(base - 96)
|
|
103
|
+
if (alt) return '\x1b' + String.fromCodePoint(key.alternate)
|
|
104
|
+
return String.fromCodePoint(key.alternate)
|
|
105
|
+
}
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Rewrite every decodable kitty CSI-u sequence in one stdin chunk to the
|
|
111
|
+
* legacy form the input layer already handles. Undecodable or non-key
|
|
112
|
+
* sequences pass through untouched, so terminals without the protocol are
|
|
113
|
+
* unaffected.
|
|
114
|
+
*/
|
|
115
|
+
export function normalizeKeyboardChunk(chunk: string): string {
|
|
116
|
+
if (!chunk.includes('\x1b[') || !chunk.includes('u')) return chunk
|
|
117
|
+
const pattern = new RegExp(CSI_U_SOURCE, 'g')
|
|
118
|
+
return chunk.replace(pattern, (whole, code: string, mods?: string, third?: string) => {
|
|
119
|
+
const legacy = legacyForKey({
|
|
120
|
+
code: Number.parseInt(code, 10),
|
|
121
|
+
modifiers: mods === undefined || mods === '' ? 1 : Math.max(1, Number.parseInt(mods, 10)),
|
|
122
|
+
alternate: third !== undefined && third !== '' ? Number.parseInt(third, 10) : undefined,
|
|
123
|
+
})
|
|
124
|
+
return legacy ?? whole
|
|
125
|
+
})
|
|
126
|
+
}
|
package/src/mentions.ts
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workspace @mention support: file and directory candidates from
|
|
3
|
-
*
|
|
4
|
-
* `sessionReferenceResolver` service, and submission
|
|
5
|
-
* `prepare()` API. Picked session mentions land as
|
|
6
|
-
* `@[label](dsh-session:…)` tokens; on submit the text is parsed
|
|
7
|
-
* readable `@label` text plus structured references, snapshots are
|
|
8
|
-
* via `agent.inject()` before the readable message wakes the driver
|
|
2
|
+
* Workspace @mention support: file and directory candidates from the
|
|
3
|
+
* `fileReferences` service (dsh-file-reference-local), session candidates
|
|
4
|
+
* from the opt-in `sessionReferenceResolver` service, and submission
|
|
5
|
+
* preparation through its `prepare()` API. Picked session mentions land as
|
|
6
|
+
* canonical `@[label](dsh-session:…)` tokens; on submit the text is parsed
|
|
7
|
+
* back into readable `@label` text plus structured references, snapshots are
|
|
8
|
+
* injected via `agent.inject()` before the readable message wakes the driver
|
|
9
9
|
* (`followup` idle, `steer` running) — exactly the upstream README's wiring.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* File discovery lives entirely in the Harness service (per-agent bounded
|
|
12
|
+
* index, `@dir/` listing, symlink guards, tool/result invalidation); this
|
|
13
|
+
* module only maps candidates to menu rows and never re-implements scanning.
|
|
14
|
+
* The service is agent-scoped (the agent supplies the session cwd and the
|
|
15
|
+
* cache key), so before the first session creates an agent the SAME official
|
|
16
|
+
* search class runs against the launch cwd — @ file completion works on a
|
|
17
|
+
* bare launch, model- and session-independent, and the agent-scoped service
|
|
18
|
+
* takes over once a session exists.
|
|
15
19
|
*
|
|
16
20
|
* @module @deepseek-ai/dsh-code/mentions
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
|
-
import { readdir } from 'node:fs/promises'
|
|
20
|
-
import { join } from 'node:path'
|
|
21
23
|
import type { Context } from '@deepseek-ai/cordis'
|
|
22
24
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
25
|
+
import { isAbsolute, resolve } from 'node:path'
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES,
|
|
28
|
+
DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
|
29
|
+
DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
|
30
|
+
WorkspaceFileSearch,
|
|
31
|
+
} from '@deepseek-ai/dsh-file-reference-local'
|
|
23
32
|
import {
|
|
24
33
|
formatSessionReferenceMention,
|
|
25
34
|
parseSessionReferenceText,
|
|
@@ -30,14 +39,6 @@ import {
|
|
|
30
39
|
/** Parsed submission text: readable text plus structured references. */
|
|
31
40
|
type ParsedSessionReferenceText = ReturnType<typeof parseSessionReferenceText>
|
|
32
41
|
|
|
33
|
-
/** One filesystem entry the @ menu can complete. */
|
|
34
|
-
export interface FileCandidate {
|
|
35
|
-
/** Workspace-relative path with forward slashes. */
|
|
36
|
-
path: string
|
|
37
|
-
/** Entry kind; directories insert with a trailing slash. */
|
|
38
|
-
kind: 'file' | 'directory'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
42
|
/** One merged menu candidate (files and sessions, already ranked). */
|
|
42
43
|
export interface MentionCandidate {
|
|
43
44
|
/** Text inserted after the `@` (directories carry a trailing slash). */
|
|
@@ -46,6 +47,8 @@ export interface MentionCandidate {
|
|
|
46
47
|
description: string
|
|
47
48
|
/** Origin kind for icon/coloring decisions. */
|
|
48
49
|
kind: 'file' | 'directory' | 'session'
|
|
50
|
+
/** Absolute path for file candidates; never rendered or persisted directly. */
|
|
51
|
+
path?: string
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
/** Prepared submission: readable content plus optional injected context. */
|
|
@@ -58,76 +61,22 @@ export interface PreparedMention {
|
|
|
58
61
|
additionalContext?: import('@deepseek-ai/dsh-session').UserMessage
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/** Empty-query default rows: proves the index exists without typing (Codex's
|
|
66
|
-
* popups show something on a bare sigil too). */
|
|
67
|
-
const EMPTY_QUERY_ROWS = 20
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Bounded async BFS scan of a workspace; unreadable entries are skipped.
|
|
71
|
-
* Both files and directories are indexed (directories insert with a trailing
|
|
72
|
-
* slash), mirroring Codex's `MatchType::{File,Directory}` index. Dotfiles and
|
|
73
|
-
* the {@link SKIP_DIRS} list are excluded, which is a coarser filter than
|
|
74
|
-
* Codex's gitignore-aware walker but stays dependency-free and bounded.
|
|
75
|
-
*/
|
|
76
|
-
export async function scanWorkspaceFiles(root: string, signal?: AbortSignal): Promise<readonly FileCandidate[]> {
|
|
77
|
-
const found: FileCandidate[] = []
|
|
78
|
-
const pending: Array<{ absolute: string; relative: string; depth: number }> = [{ absolute: root, relative: '', depth: 0 }]
|
|
79
|
-
const aborted = (): boolean => signal?.aborted === true
|
|
80
|
-
while (pending.length > 0 && found.length < MAX_FILES && !aborted()) {
|
|
81
|
-
const current = pending.shift()
|
|
82
|
-
if (current === undefined) break
|
|
83
|
-
let entries
|
|
84
|
-
try {
|
|
85
|
-
entries = await readdir(current.absolute, { withFileTypes: true })
|
|
86
|
-
} catch {
|
|
87
|
-
continue
|
|
88
|
-
}
|
|
89
|
-
for (const entry of entries) {
|
|
90
|
-
if (found.length >= MAX_FILES || aborted()) return found
|
|
91
|
-
if (entry.name.startsWith('.')) continue
|
|
92
|
-
const relative = current.relative === '' ? entry.name : `${current.relative}/${entry.name}`
|
|
93
|
-
if (entry.isDirectory()) {
|
|
94
|
-
if (SKIP_DIRS.has(entry.name) || current.depth + 1 > MAX_DEPTH) continue
|
|
95
|
-
found.push({ path: relative, kind: 'directory' })
|
|
96
|
-
pending.push({ absolute: join(current.absolute, entry.name), relative, depth: current.depth + 1 })
|
|
97
|
-
} else if (entry.isFile()) {
|
|
98
|
-
found.push({ path: relative, kind: 'file' })
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return found.sort((left, right) => left.path < right.path ? -1 : 1)
|
|
64
|
+
/** One path candidate the `ctx.fileReferences` service returns. */
|
|
65
|
+
interface ServiceFileCandidate {
|
|
66
|
+
readonly path: string
|
|
67
|
+
readonly kind: 'file' | 'directory'
|
|
103
68
|
}
|
|
104
69
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
for (const char of haystack) {
|
|
109
|
-
if (char === query[at]) at += 1
|
|
110
|
-
if (at >= query.length) return true
|
|
111
|
-
}
|
|
112
|
-
return at >= query.length
|
|
70
|
+
/** The `ctx.fileReferences` service face (dsh-file-reference-local). */
|
|
71
|
+
interface FileReferenceServiceLike {
|
|
72
|
+
list(agent: Agent, query: string, signal: AbortSignal): Promise<readonly ServiceFileCandidate[]>
|
|
113
73
|
}
|
|
114
74
|
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
const name = path.slice(path.lastIndexOf('/') + 1)
|
|
118
|
-
if (query === '') return 0
|
|
119
|
-
if (name === query) return 1000
|
|
120
|
-
if (name.startsWith(query)) return 900
|
|
121
|
-
if (name.includes(query)) return 700
|
|
122
|
-
if (path.includes(query)) return 500
|
|
123
|
-
if (isSubsequence(query, name)) return 300
|
|
124
|
-
return 0
|
|
125
|
-
}
|
|
75
|
+
/** Menu cap on file rows; the service owns ranking and default rows. */
|
|
76
|
+
const MAX_FILE_ROWS = 20
|
|
126
77
|
|
|
127
78
|
/** The mention API the input editor and the runner share. */
|
|
128
79
|
export interface MentionsApi {
|
|
129
|
-
/** Scanned workspace files and directories, cached across one session. */
|
|
130
|
-
files(): Promise<readonly FileCandidate[]>
|
|
131
80
|
/** Ranked menu candidates for the typed `@` query. */
|
|
132
81
|
candidates(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>
|
|
133
82
|
/** Parse submission text into readable text plus structured references. */
|
|
@@ -143,53 +92,65 @@ export interface MentionsApi {
|
|
|
143
92
|
|
|
144
93
|
/**
|
|
145
94
|
* Create the mention API for one agent's workspace. A missing
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* (a bare launch before any session
|
|
149
|
-
*
|
|
95
|
+
* `fileReferences` service (with an agent present) or `sessionReferenceResolver`
|
|
96
|
+
* degrades that half to empty rows; `prepare` passes text through untouched
|
|
97
|
+
* without references. An undefined agent (a bare launch before any session
|
|
98
|
+
* exists) runs the official WorkspaceFileSearch over the launch cwd — the
|
|
99
|
+
* same class the mounted service uses per agent — so `@` file completion
|
|
100
|
+
* works from the first keystroke; session references wait for the session.
|
|
150
101
|
*
|
|
151
|
-
* `
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* @param ctx - context carrying the optional `
|
|
155
|
-
*
|
|
156
|
-
* @param
|
|
102
|
+
* `candidates` never reaches for `this` — the runner hands it to the input
|
|
103
|
+
* editor as a detached callback, and a `this`-bound method would throw on
|
|
104
|
+
* every `@` key.
|
|
105
|
+
* @param ctx - context carrying the optional `fileReferences` and
|
|
106
|
+
* `sessionReferenceResolver` services.
|
|
107
|
+
* @param agent - the session owner; excluded from its own session candidates.
|
|
108
|
+
* @param cwd - launch working directory; bounds the pre-session search.
|
|
157
109
|
*/
|
|
158
110
|
export function createMentions(ctx: Context, agent: Agent | undefined, cwd: string): MentionsApi {
|
|
159
111
|
const resolver = ctx.get('sessionReferenceResolver')
|
|
112
|
+
const fileReferences = (ctx as unknown as { get(name: string): unknown }).get('fileReferences') as
|
|
113
|
+
| FileReferenceServiceLike
|
|
114
|
+
| undefined
|
|
160
115
|
const sessionCapable = agent !== undefined && resolver !== undefined
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
116
|
+
// Pre-session fallback: one lazily built search over the launch cwd with
|
|
117
|
+
// the official defaults — pure in-memory index, no handles to release.
|
|
118
|
+
let preSessionSearch: WorkspaceFileSearch | undefined
|
|
119
|
+
const preSessionFiles = (query: string, signal?: AbortSignal): Promise<readonly ServiceFileCandidate[]> => {
|
|
120
|
+
preSessionSearch ??= new WorkspaceFileSearch(cwd, {
|
|
121
|
+
maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
|
122
|
+
maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
|
123
|
+
excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES],
|
|
124
|
+
})
|
|
125
|
+
return preSessionSearch.list(query, signal ?? new AbortController().signal)
|
|
165
126
|
}
|
|
166
127
|
|
|
167
128
|
return {
|
|
168
|
-
files,
|
|
169
129
|
async candidates(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]> {
|
|
170
130
|
const needle = query.trim()
|
|
171
|
-
const [
|
|
172
|
-
|
|
131
|
+
const [files, sessions] = await Promise.all([
|
|
132
|
+
agent !== undefined && fileReferences !== undefined
|
|
133
|
+
? fileReferences
|
|
134
|
+
.list(agent, needle, signal ?? new AbortController().signal)
|
|
135
|
+
.catch(() => [] as readonly ServiceFileCandidate[])
|
|
136
|
+
: agent === undefined
|
|
137
|
+
? preSessionFiles(needle, signal).catch(() => [] as readonly ServiceFileCandidate[])
|
|
138
|
+
: Promise.resolve([] as readonly ServiceFileCandidate[]),
|
|
173
139
|
sessionCapable && needle !== '' && agent !== undefined
|
|
174
140
|
? resolver!.listCandidates(agent, needle, 10, signal).catch(() => [] as readonly SessionReferenceCandidate[])
|
|
175
141
|
: Promise.resolve([] as readonly SessionReferenceCandidate[]),
|
|
176
142
|
])
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
kind: candidate.kind,
|
|
189
|
-
}))
|
|
190
|
-
// Sessions join only with a typed needle and always AFTER the file
|
|
191
|
-
// rows: `@` is a file mention first (Codex's semantics), and session
|
|
192
|
-
// references are the secondary vocabulary.
|
|
143
|
+
// The service owns ranking (and the bare-@ default rows); the menu caps
|
|
144
|
+
// file rows and always places sessions after files — `@` is a file
|
|
145
|
+
// mention first (Codex's semantics), session references second.
|
|
146
|
+
const fileRows: MentionCandidate[] = files.slice(0, MAX_FILE_ROWS).map(candidate => ({
|
|
147
|
+
label: candidate.path,
|
|
148
|
+
description: candidate.kind === 'directory' ? 'Folder' : 'File',
|
|
149
|
+
kind: candidate.kind,
|
|
150
|
+
...candidate.kind === 'file'
|
|
151
|
+
? { path: isAbsolute(candidate.path) ? candidate.path : resolve(cwd, candidate.path) }
|
|
152
|
+
: {},
|
|
153
|
+
}))
|
|
193
154
|
const sessionRows: MentionCandidate[] = sessions.map(candidate => ({
|
|
194
155
|
label: formatSessionReferenceMention(candidate),
|
|
195
156
|
description: `Session · ${candidate.cwd ?? '(no cwd)'}`,
|
package/src/models.ts
CHANGED
|
@@ -13,10 +13,11 @@ import type { ModelSelection } from '@deepseek-ai/dsh-agent'
|
|
|
13
13
|
import {
|
|
14
14
|
ReasoningEffortId,
|
|
15
15
|
type LlmCallConfig,
|
|
16
|
-
type LlmModelInfo,
|
|
17
|
-
type LlmModelReasoningInfo,
|
|
18
|
-
type LlmResolvedModelInfo,
|
|
19
|
-
|
|
16
|
+
type LlmModelInfo,
|
|
17
|
+
type LlmModelReasoningInfo,
|
|
18
|
+
type LlmResolvedModelInfo,
|
|
19
|
+
type ModelModality,
|
|
20
|
+
} from '@deepseek-ai/dsh-llm'
|
|
20
21
|
|
|
21
22
|
/** Display metadata for one adapter-owned reasoning effort (mirrors `LlmReasoningEffortInfo`). */
|
|
22
23
|
export interface ModelReasoningEffort {
|
|
@@ -44,9 +45,11 @@ export interface ModelRow {
|
|
|
44
45
|
providerName: string
|
|
45
46
|
/** Provider-owned model id. */
|
|
46
47
|
model: string
|
|
47
|
-
/** Human-readable model name. */
|
|
48
|
-
modelName: string
|
|
49
|
-
/**
|
|
48
|
+
/** Human-readable model name. */
|
|
49
|
+
modelName: string
|
|
50
|
+
/** Request modalities advertised for this exact route; absent means unknown. */
|
|
51
|
+
inputModalities?: readonly ModelModality[]
|
|
52
|
+
/** Adapter-owned selectable reasoning levels when the model exposes any. */
|
|
50
53
|
reasoning?: ModelReasoning
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -194,16 +197,19 @@ export async function loadModelDirectory(ctx: Context): Promise<ModelDirectory>
|
|
|
194
197
|
const rows = await Promise.all(models.map(async (model): Promise<ModelRow> => {
|
|
195
198
|
const row: ModelRow = {
|
|
196
199
|
provider: provider.id,
|
|
197
|
-
providerName: provider.name,
|
|
198
|
-
model: model.id,
|
|
199
|
-
modelName: model.name,
|
|
200
|
-
|
|
200
|
+
providerName: provider.name,
|
|
201
|
+
model: model.id,
|
|
202
|
+
modelName: model.name,
|
|
203
|
+
...model.inputModalities === undefined ? {} : { inputModalities: [...model.inputModalities] },
|
|
204
|
+
}
|
|
201
205
|
if (llmResolve.resolveModelInfo === undefined) return row
|
|
202
206
|
try {
|
|
203
207
|
const resolved = await llmResolve.resolveModelInfo(provider.id, model.id)
|
|
204
|
-
return
|
|
205
|
-
|
|
206
|
-
: {
|
|
208
|
+
return {
|
|
209
|
+
...row,
|
|
210
|
+
...resolved.inputModalities === undefined ? {} : { inputModalities: [...resolved.inputModalities] },
|
|
211
|
+
...resolved.reasoning === undefined ? {} : { reasoning: mapReasoning(resolved.reasoning) },
|
|
212
|
+
}
|
|
207
213
|
} catch {
|
|
208
214
|
reasoningFailures.push(`${provider.id}/${model.id}`)
|
|
209
215
|
return row
|
package/src/permissions.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Permission-preset policy for pending and active TUI sessions. */
|
|
2
2
|
|
|
3
3
|
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
-
import type {
|
|
4
|
+
import type { PermissionPresetService } from '@deepseek-ai/dsh-permission-presets'
|
|
5
|
+
import type { Session } from '@deepseek-ai/dsh-session'
|
|
5
6
|
|
|
6
7
|
/** One selectable permission preset row for the /permission panel. */
|
|
7
8
|
export interface PermissionRow {
|
|
@@ -9,20 +10,12 @@ export interface PermissionRow {
|
|
|
9
10
|
readonly description?: string
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
export
|
|
14
|
-
readonly names: readonly string[]
|
|
15
|
-
readonly defaultPreset: string
|
|
16
|
-
resolve(name: string): unknown
|
|
17
|
-
current(events: readonly SessionEvent[]): string
|
|
18
|
-
set(session: Session, preset: string): void
|
|
19
|
-
/** Client presentation metadata for one preset; may reject unknown names. */
|
|
20
|
-
optionOf?(name: string): { name: string; description?: string } | undefined
|
|
21
|
-
}
|
|
13
|
+
/** Public compatibility alias for the official upstream permission service. */
|
|
14
|
+
export type PermissionPresetsService = PermissionPresetService
|
|
22
15
|
|
|
23
16
|
/** Read the optional Harness service without importing its runtime package. */
|
|
24
17
|
export function permissionPresetsFrom(ctx: Context): PermissionPresetsService | undefined {
|
|
25
|
-
return
|
|
18
|
+
return ctx.get('permissionPresets')
|
|
26
19
|
}
|
|
27
20
|
|
|
28
21
|
/** Effective label for either an active session or the not-yet-created first one. */
|
|
@@ -75,7 +68,6 @@ export function applyPendingPermission(
|
|
|
75
68
|
*/
|
|
76
69
|
export function listPermissionRows(service: PermissionPresetsService): readonly PermissionRow[] {
|
|
77
70
|
return service.names.map((id) => {
|
|
78
|
-
if (service.optionOf === undefined) return { id }
|
|
79
71
|
try {
|
|
80
72
|
return { id, description: service.optionOf(id)?.description }
|
|
81
73
|
} catch {
|
package/src/presets.ts
CHANGED
|
@@ -1,32 +1,19 @@
|
|
|
1
1
|
/** Agent-preset policy kept independent from the Ink surface. */
|
|
2
2
|
|
|
3
3
|
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
-
import type { Agent
|
|
4
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
5
|
+
import type { AgentPreset, AgentPresets } from '@deepseek-ai/dsh-agent-presets'
|
|
5
6
|
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
6
7
|
|
|
7
8
|
/** One discoverable agent composition. */
|
|
8
|
-
export
|
|
9
|
-
readonly id: string
|
|
10
|
-
readonly trust: 'system' | 'user'
|
|
11
|
-
readonly name?: string
|
|
12
|
-
readonly description?: string
|
|
13
|
-
readonly order?: number
|
|
14
|
-
readonly broken?: string
|
|
15
|
-
}
|
|
9
|
+
export type PresetRow = AgentPreset
|
|
16
10
|
|
|
17
|
-
/**
|
|
18
|
-
export
|
|
19
|
-
readonly defaultId: string
|
|
20
|
-
list(): Promise<PresetRow[]>
|
|
21
|
-
resolve(id?: string): Promise<PresetRow>
|
|
22
|
-
mount(agentCtx: Context, id?: string): Promise<PresetRow>
|
|
23
|
-
recompose(agentCtx: Context, id: string): Promise<PresetRow>
|
|
24
|
-
composedPreset(agentCtx: Context): string | undefined
|
|
25
|
-
}
|
|
11
|
+
/** Public compatibility alias for the official upstream service type. */
|
|
12
|
+
export type AgentPresetsService = AgentPresets
|
|
26
13
|
|
|
27
14
|
/** Read an optional Cordis service without requiring its package at build time. */
|
|
28
15
|
export function agentPresetsFrom(ctx: Context): AgentPresetsService | undefined {
|
|
29
|
-
return
|
|
16
|
+
return ctx.get('agentPresets')
|
|
30
17
|
}
|
|
31
18
|
|
|
32
19
|
/** A preset may change only before the first durable turn begins. */
|
|
@@ -71,6 +58,3 @@ export async function switchPreset(
|
|
|
71
58
|
writable.append('agent-preset/selected', { agentPreset: preset.id })
|
|
72
59
|
return preset
|
|
73
60
|
}
|
|
74
|
-
|
|
75
|
-
/** Minimal handle shape used by lifecycle tests without exposing Agent internals. */
|
|
76
|
-
export type OwnedAgent = Pick<AgentHandle, 'agent' | 'dispose'>
|