dsh-code 0.4.0 → 0.6.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 +217 -0
- package/README.md +216 -67
- package/bin/deepseek.mjs +70 -0
- package/cordis.patch.yml +62 -6
- package/lib/devtools-CdTl3MNy.mjs +3643 -0
- package/lib/index.mjs +27467 -673
- package/lib/rolldown-runtime-CMFfr-1z.mjs +26 -0
- package/lib/startup.mjs +34 -17
- package/lib/types/app.d.ts +29 -1
- package/lib/types/commands.d.ts +2 -0
- package/lib/types/history.d.ts +79 -0
- package/lib/types/index.d.ts +5 -5
- package/lib/types/internals.d.ts +2 -0
- package/lib/types/kernel-panels.d.ts +48 -0
- package/lib/types/plugin-inventory.d.ts +11 -0
- package/lib/types/presets.d.ts +32 -0
- package/lib/types/render/animations.d.ts +10 -1
- package/lib/types/render/inspector.d.ts +6 -0
- package/lib/types/render/projection.d.ts +21 -1
- package/lib/types/render/status.d.ts +131 -14
- package/lib/types/render/text.d.ts +9 -0
- package/lib/types/session-directory.d.ts +54 -0
- package/lib/types/session-switch.d.ts +17 -0
- package/lib/types/skills.d.ts +2 -0
- package/lib/types/startup.d.ts +11 -1
- package/package.json +117 -112
- package/src/app.ts +2543 -1969
- package/src/commands.ts +15 -1
- package/src/history.ts +136 -0
- package/src/index.ts +550 -155
- package/src/internals.ts +5 -0
- package/src/kernel-panels.ts +419 -0
- package/src/plugin-inventory.ts +47 -0
- package/src/presets.ts +64 -0
- package/src/render/animations.ts +14 -1
- package/src/render/export.ts +4 -0
- package/src/render/inspector.ts +23 -5
- package/src/render/lines.ts +21 -10
- package/src/render/markdown.ts +15 -1
- package/src/render/projection.ts +71 -8
- package/src/render/status.ts +522 -65
- package/src/render/text.ts +34 -6
- package/src/session-directory.ts +102 -0
- package/src/session-switch.ts +58 -0
- package/src/skills.ts +20 -7
- package/src/startup.ts +38 -20
- package/src/whale-glyph.ts +23 -23
- package/README.zh.md +0 -65
- package/src/pictures/1.png +0 -0
package/src/render/text.ts
CHANGED
|
@@ -23,12 +23,9 @@ export function displayText(text: string): string {
|
|
|
23
23
|
return text.replace(CONTROL_ESCAPE, char => `\\x${char.charCodeAt(0).toString(16).padStart(2, '0')}`)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
text: string
|
|
30
|
-
/** Whether content before the returned suffix was omitted. */
|
|
31
|
-
truncated: boolean
|
|
26
|
+
/** Collapse external text to one terminal-safe logical row. */
|
|
27
|
+
export function singleLineText(text: string): string {
|
|
28
|
+
return displayText(text).replace(/\r?\n/gu, ' ↵ ').replace(/\t/gu, ' ')
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
/** Terminal-cell width matching the TUI's existing CJK-aware wrapping rule. */
|
|
@@ -40,6 +37,37 @@ function cellWidth(text: string): number {
|
|
|
40
37
|
return columns
|
|
41
38
|
}
|
|
42
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Truncate one display-safe row without ever exceeding its physical-column
|
|
42
|
+
* budget. The ellipsis is included inside the budget, matching Codex's popup
|
|
43
|
+
* truncation contract; the previous app-local helper appended it after the
|
|
44
|
+
* row was already full and could force an extra terminal wrap.
|
|
45
|
+
*/
|
|
46
|
+
export function truncateColumns(text: string, columns: number): string {
|
|
47
|
+
const limit = Math.max(0, Math.floor(columns))
|
|
48
|
+
if (limit === 0) return ''
|
|
49
|
+
if (cellWidth(text) <= limit) return text
|
|
50
|
+
|
|
51
|
+
const contentLimit = limit - 1
|
|
52
|
+
let used = 0
|
|
53
|
+
let result = ''
|
|
54
|
+
for (const char of text) {
|
|
55
|
+
const width = cellWidth(char)
|
|
56
|
+
if (used + width > contentLimit) break
|
|
57
|
+
result += char
|
|
58
|
+
used += width
|
|
59
|
+
}
|
|
60
|
+
return `${result}…`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A display-safe suffix bounded by terminal rows and columns. */
|
|
64
|
+
export interface DisplayTail {
|
|
65
|
+
/** Sanitized suffix suitable for direct terminal rendering. */
|
|
66
|
+
text: string
|
|
67
|
+
/** Whether content before the returned suffix was omitted. */
|
|
68
|
+
truncated: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
43
71
|
/** Read one Unicode character immediately before `end`. */
|
|
44
72
|
function previousCharacter(text: string, end: number): { char: string; start: number } {
|
|
45
73
|
const last = text.charCodeAt(end - 1)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Lightweight session-directory projection for the /resume picker. */
|
|
2
|
+
|
|
3
|
+
import { basename, resolve } from 'node:path'
|
|
4
|
+
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
|
|
5
|
+
|
|
6
|
+
export interface SessionRecord {
|
|
7
|
+
readonly header: SessionHeader
|
|
8
|
+
readonly live: boolean
|
|
9
|
+
readonly persisted: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TitleObservationResult {
|
|
13
|
+
readonly sessionId: string
|
|
14
|
+
readonly status: 'fulfilled' | 'rejected'
|
|
15
|
+
readonly value?: { readonly title?: { readonly title?: string; readonly text?: string } }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SessionLogSnapshot {
|
|
19
|
+
readonly session: SessionHeader
|
|
20
|
+
readonly events: SessionEvent[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Structural upstream SessionQuery surface used by the TUI. */
|
|
24
|
+
export interface SessionQueryService {
|
|
25
|
+
listSessions(signal?: AbortSignal): Promise<SessionRecord[]>
|
|
26
|
+
readTitleSnapshots(ids: readonly string[], signal?: AbortSignal): Promise<TitleObservationResult[]>
|
|
27
|
+
readSession(id: string, signal?: AbortSignal): Promise<SessionLogSnapshot>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SessionScope = 'roots' | 'all'
|
|
31
|
+
export type CwdScope = 'all' | 'current'
|
|
32
|
+
export type SessionSort = 'newest' | 'oldest'
|
|
33
|
+
|
|
34
|
+
export interface SessionDirectoryOptions {
|
|
35
|
+
readonly sessions: SessionScope
|
|
36
|
+
readonly cwd: CwdScope
|
|
37
|
+
readonly sort: SessionSort
|
|
38
|
+
readonly currentCwd: string
|
|
39
|
+
readonly query: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SessionRow {
|
|
43
|
+
readonly id: string
|
|
44
|
+
readonly createdAt: number
|
|
45
|
+
readonly cwd: string
|
|
46
|
+
readonly workspace: string
|
|
47
|
+
readonly parent?: string
|
|
48
|
+
readonly subagent: boolean
|
|
49
|
+
readonly resumable: boolean
|
|
50
|
+
readonly live: boolean
|
|
51
|
+
readonly persisted: boolean
|
|
52
|
+
readonly preset: string
|
|
53
|
+
readonly title?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function samePath(left: string | undefined, right: string): boolean {
|
|
57
|
+
if (left === undefined) return false
|
|
58
|
+
return resolve(left).toLowerCase() === resolve(right).toLowerCase()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Filter/sort header-only records. No session log is loaded here. */
|
|
62
|
+
export function projectSessionRows(records: readonly SessionRecord[], options: SessionDirectoryOptions): SessionRow[] {
|
|
63
|
+
const needle = options.query.trim().toLowerCase()
|
|
64
|
+
return records
|
|
65
|
+
.filter(record => options.sessions === 'all'
|
|
66
|
+
|| (record.header.parentSession === undefined && record.header.origin !== 'subagent'))
|
|
67
|
+
.filter(record => options.cwd === 'all' || samePath(record.header.cwd, options.currentCwd))
|
|
68
|
+
.map(record => {
|
|
69
|
+
const cwd = record.header.cwd ?? ''
|
|
70
|
+
const subagent = record.header.origin === 'subagent' || record.header.parentSession !== undefined
|
|
71
|
+
return {
|
|
72
|
+
id: record.header.id,
|
|
73
|
+
createdAt: record.header.createdAt,
|
|
74
|
+
cwd,
|
|
75
|
+
workspace: cwd === '' ? '(no workspace)' : basename(cwd),
|
|
76
|
+
parent: record.header.parentSession,
|
|
77
|
+
subagent,
|
|
78
|
+
resumable: !subagent,
|
|
79
|
+
live: record.live,
|
|
80
|
+
persisted: record.persisted,
|
|
81
|
+
preset: record.header.agentPreset ?? 'standard',
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.filter(row => needle === '' || `${row.id} ${row.cwd} ${row.workspace} ${row.preset}`.toLowerCase().includes(needle))
|
|
85
|
+
.sort((left, right) => options.sort === 'newest'
|
|
86
|
+
? right.createdAt - left.createdAt
|
|
87
|
+
: left.createdAt - right.createdAt)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Merge page-local title observations without disturbing directory order. */
|
|
91
|
+
export function mergeSessionTitles(
|
|
92
|
+
rows: readonly SessionRow[],
|
|
93
|
+
observations: readonly TitleObservationResult[],
|
|
94
|
+
): SessionRow[] {
|
|
95
|
+
const titles = new Map<string, string>()
|
|
96
|
+
for (const observation of observations) {
|
|
97
|
+
if (observation.status !== 'fulfilled') continue
|
|
98
|
+
const title = observation.value?.title?.title ?? observation.value?.title?.text
|
|
99
|
+
if (title !== undefined && title.trim() !== '') titles.set(observation.sessionId, title)
|
|
100
|
+
}
|
|
101
|
+
return rows.map(row => titles.has(row.id) ? { ...row, title: titles.get(row.id) } : row)
|
|
102
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Latest-wins, idle-bound queue for safe Agent session changes. */
|
|
2
|
+
|
|
3
|
+
export interface IdleActivity {
|
|
4
|
+
readonly status: 'idle' | 'running'
|
|
5
|
+
whenIdle(): Promise<void>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Request<T> {
|
|
9
|
+
readonly activity: IdleActivity
|
|
10
|
+
readonly value: T
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class SessionSwitchQueue<T> {
|
|
14
|
+
private pending: Request<T> | undefined
|
|
15
|
+
private pumping = false
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly execute: (value: T) => Promise<void>,
|
|
19
|
+
private readonly failed: (error: unknown) => void,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
/** Queue a request; a later request replaces any request still waiting. */
|
|
23
|
+
request(activity: IdleActivity, value: T): 'queued' | 'started' {
|
|
24
|
+
this.pending = { activity, value }
|
|
25
|
+
const outcome = activity.status === 'running' || this.pumping ? 'queued' : 'started'
|
|
26
|
+
if (!this.pumping) void this.pump()
|
|
27
|
+
return outcome
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Cancel only work that has not begun activation. */
|
|
31
|
+
cancel(): boolean {
|
|
32
|
+
if (this.pending === undefined) return false
|
|
33
|
+
this.pending = undefined
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async pump(): Promise<void> {
|
|
38
|
+
this.pumping = true
|
|
39
|
+
try {
|
|
40
|
+
while (this.pending !== undefined) {
|
|
41
|
+
const observed = this.pending
|
|
42
|
+
await observed.activity.whenIdle()
|
|
43
|
+
// Another request replaced this one while the turn was converging.
|
|
44
|
+
if (this.pending !== observed) continue
|
|
45
|
+
this.pending = undefined
|
|
46
|
+
try {
|
|
47
|
+
await this.execute(observed.value)
|
|
48
|
+
} catch (error: unknown) {
|
|
49
|
+
this.failed(error)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
this.pumping = false
|
|
54
|
+
// A request may land between the loop condition and finally.
|
|
55
|
+
if (this.pending !== undefined) void this.pump()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/skills.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface SkillRow {
|
|
|
28
28
|
export interface SkillsView {
|
|
29
29
|
/** Name-sorted user-invocable rows; empty until the first load lands. */
|
|
30
30
|
readonly rows: readonly SkillRow[]
|
|
31
|
+
/** Latest catalog-read failure; the help panel exposes it in place. */
|
|
32
|
+
readonly error?: string
|
|
31
33
|
/** Subscribe to catalog changes; returns the unsubscribe function. */
|
|
32
34
|
subscribe(listener: () => void): () => void
|
|
33
35
|
/** Retarget the agent whose workspace the catalog is read for. */
|
|
@@ -63,21 +65,29 @@ export function watchSkills(ctx: Context): SkillsWatch {
|
|
|
63
65
|
const skills = ctx.get('skills')
|
|
64
66
|
let agent: Agent | undefined
|
|
65
67
|
let rows: readonly SkillRow[] = []
|
|
68
|
+
let error: string | undefined
|
|
66
69
|
const listeners = new Set<() => void>()
|
|
67
70
|
|
|
68
71
|
const reload = (): void => {
|
|
69
|
-
|
|
70
|
-
skills
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
const currentAgent = agent
|
|
73
|
+
if (skills === undefined || currentAgent === undefined) return
|
|
74
|
+
Promise.resolve().then(() => skills.list({
|
|
75
|
+
cwd: currentAgent.session.header.cwd,
|
|
76
|
+
scope: currentAgent,
|
|
77
|
+
})).then((summaries: readonly SkillSummary[]) => {
|
|
74
78
|
const next = toRows(summaries)
|
|
75
|
-
|
|
79
|
+
const unchanged = next.length === rows.length && next.every((row, index) => row.name === rows[index]?.name)
|
|
76
80
|
rows = next
|
|
81
|
+
const recovered = error !== undefined
|
|
82
|
+
error = undefined
|
|
83
|
+
if (unchanged && !recovered) return
|
|
77
84
|
for (const listener of listeners) listener()
|
|
78
|
-
}
|
|
85
|
+
}).catch((cause: unknown) => {
|
|
79
86
|
// Discovery failure keeps the last good rows; the next skills/change
|
|
80
87
|
// notification is the retry surface (mirrors the web directory).
|
|
88
|
+
rows = [...rows]
|
|
89
|
+
error = cause instanceof Error ? cause.message : String(cause)
|
|
90
|
+
for (const listener of listeners) listener()
|
|
81
91
|
})
|
|
82
92
|
}
|
|
83
93
|
|
|
@@ -89,6 +99,9 @@ export function watchSkills(ctx: Context): SkillsWatch {
|
|
|
89
99
|
get rows(): readonly SkillRow[] {
|
|
90
100
|
return rows
|
|
91
101
|
},
|
|
102
|
+
get error(): string | undefined {
|
|
103
|
+
return error
|
|
104
|
+
},
|
|
92
105
|
subscribe(listener: () => void): () => void {
|
|
93
106
|
listeners.add(listener)
|
|
94
107
|
return () => {
|
package/src/startup.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The interactive terminal app's command-line provider: parses `--resume`,
|
|
3
|
-
* `--continue`, `--session`, and `--help`, then publishes
|
|
3
|
+
* `--continue`, `--session`, `--mode`, and `--help`, then publishes
|
|
4
4
|
* {@link TUI_STARTUP_SERVICE} for the runner to consume lazily. Follows the
|
|
5
5
|
* headless bundle's startup shape (a commander action publishing a service
|
|
6
6
|
* through {@link parseCmdline}).
|
|
@@ -33,11 +33,37 @@ export const TUI_STARTUP_SERVICE = 'tuiStartup'
|
|
|
33
33
|
|
|
34
34
|
/** How the runner obtains its session identity. */
|
|
35
35
|
export type TuiStartup =
|
|
36
|
-
| { readonly kind: 'fresh' }
|
|
37
|
-
| { readonly kind: 'named'; readonly sessionId: string }
|
|
36
|
+
| { readonly kind: 'fresh'; readonly mode?: string }
|
|
37
|
+
| { readonly kind: 'named'; readonly sessionId: string; readonly mode?: string }
|
|
38
38
|
| { readonly kind: 'resume'; readonly sessionId: string }
|
|
39
39
|
| { readonly kind: 'latest' }
|
|
40
40
|
|
|
41
|
+
export interface TuiStartupOptions {
|
|
42
|
+
readonly resume?: string
|
|
43
|
+
readonly continue?: boolean
|
|
44
|
+
readonly session?: string
|
|
45
|
+
readonly mode?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Pure option policy shared by Commander and tests. */
|
|
49
|
+
export function resolveTuiStartup(options: TuiStartupOptions): TuiStartup {
|
|
50
|
+
const selected = [options.resume !== undefined, options.continue === true, options.session !== undefined]
|
|
51
|
+
if (selected.filter(Boolean).length > 1) throw new Error('--resume, --continue, and --session are mutually exclusive')
|
|
52
|
+
if (options.session === '') throw new Error('--session needs an id')
|
|
53
|
+
if (options.resume === '') throw new Error('--resume needs a session id or id prefix')
|
|
54
|
+
if (options.mode === '') throw new Error('--mode needs a preset id')
|
|
55
|
+
if (options.mode !== undefined && (options.resume !== undefined || options.continue === true)) {
|
|
56
|
+
throw new Error('--mode applies only to a new session; it cannot be combined with --resume or --continue')
|
|
57
|
+
}
|
|
58
|
+
return options.resume !== undefined
|
|
59
|
+
? { kind: 'resume', sessionId: options.resume }
|
|
60
|
+
: options.continue === true
|
|
61
|
+
? { kind: 'latest' }
|
|
62
|
+
: options.session !== undefined
|
|
63
|
+
? { kind: 'named', sessionId: options.session, ...options.mode === undefined ? {} : { mode: options.mode } }
|
|
64
|
+
: { kind: 'fresh', ...options.mode === undefined ? {} : { mode: options.mode } }
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
/**
|
|
42
68
|
* This app's command: the launcher's flags this app owns, its description,
|
|
43
69
|
* and its help text.
|
|
@@ -51,11 +77,13 @@ function tuiCommand(): Command {
|
|
|
51
77
|
.option('-r, --resume <session>', 'resume the persisted session with this id (or unique id prefix)')
|
|
52
78
|
.option('-c, --continue', 'resume the most recent persisted session for this working directory')
|
|
53
79
|
.option('--session <id>', 'create a new session under this explicit id')
|
|
80
|
+
.option('--mode <preset>', 'agent preset for a newly created session')
|
|
54
81
|
.addHelpText('after', `
|
|
55
82
|
Examples:
|
|
56
83
|
dsh --profile cli fresh session, minted id
|
|
57
84
|
dsh --profile cli --resume abc123 resume session by id prefix
|
|
58
85
|
dsh --profile cli --continue resume the latest local session
|
|
86
|
+
dsh --profile cli --mode minimal fresh session using the minimal preset
|
|
59
87
|
`)
|
|
60
88
|
}
|
|
61
89
|
|
|
@@ -67,24 +95,14 @@ Examples:
|
|
|
67
95
|
export function apply(ctx: Context): void {
|
|
68
96
|
const program = tuiCommand()
|
|
69
97
|
program.action(() => {
|
|
70
|
-
const options = program.opts<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
program.error('error: --session needs an id')
|
|
77
|
-
}
|
|
78
|
-
if (options.resume !== undefined && options.resume === '') {
|
|
79
|
-
program.error('error: --resume needs a session id or id prefix')
|
|
98
|
+
const options = program.opts<TuiStartupOptions>()
|
|
99
|
+
let startup: TuiStartup | undefined
|
|
100
|
+
try {
|
|
101
|
+
startup = resolveTuiStartup(options)
|
|
102
|
+
} catch (error: unknown) {
|
|
103
|
+
program.error(`error: ${error instanceof Error ? error.message : String(error)}`)
|
|
80
104
|
}
|
|
81
|
-
|
|
82
|
-
? { kind: 'resume', sessionId: options.resume }
|
|
83
|
-
: options.continue === true
|
|
84
|
-
? { kind: 'latest' }
|
|
85
|
-
: options.session !== undefined
|
|
86
|
-
? { kind: 'named', sessionId: options.session }
|
|
87
|
-
: { kind: 'fresh' }
|
|
105
|
+
if (startup === undefined) return
|
|
88
106
|
ctx.provide(TUI_STARTUP_SERVICE, { startup } satisfies { startup: TuiStartup })
|
|
89
107
|
})
|
|
90
108
|
parseCmdline(ctx, program)
|
package/src/whale-glyph.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
// GENERATED by scripts/gen-whale-glyph.ts — do not edit by hand. Rerun the
|
|
2
|
-
// generator after changing the FishLogo path. Half-block rendering of the
|
|
3
|
-
// DeepSeek fish logo (figma I39:24057;88:8943 fillGeometry, exact extract;
|
|
4
|
-
// native 23.16x17.04 → 26 columns × 8 half-block rows).
|
|
5
|
-
// Blank cells are part of the glyph's fixed 26-column grid: pad, never trim.
|
|
6
|
-
|
|
7
|
-
/** Half-block whale glyph rows; render with the brand color. */
|
|
8
|
-
export const WHALE_GLYPH: readonly string[] = [
|
|
9
|
-
' ▄▄▄▄▄▄▄▄█ ▄█▄ ▄',
|
|
10
|
-
' ▄▄██████████▄▄ ▀███▄████',
|
|
11
|
-
'▄███████████████▄ ███▀▀▀ ',
|
|
12
|
-
'██ ▀▀█████▄▀██████ ',
|
|
13
|
-
'██▄ ▀████▄▄████ ',
|
|
14
|
-
' ██▄ ▀██████▀ ',
|
|
15
|
-
' ▀██▄▄ ██▄ ▀███▄▄ ',
|
|
16
|
-
' ▀▀███████▀▀ ▀▀▀ ',
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
/** Fixed glyph width in terminal columns. */
|
|
20
|
-
export const WHALE_GLYPH_COLUMNS = 26
|
|
21
|
-
|
|
22
|
-
/** Fixed glyph height in half-block rows. */
|
|
23
|
-
export const WHALE_GLYPH_ROWS = 8
|
|
1
|
+
// GENERATED by scripts/gen-whale-glyph.ts — do not edit by hand. Rerun the
|
|
2
|
+
// generator after changing the FishLogo path. Half-block rendering of the
|
|
3
|
+
// DeepSeek fish logo (figma I39:24057;88:8943 fillGeometry, exact extract;
|
|
4
|
+
// native 23.16x17.04 → 26 columns × 8 half-block rows).
|
|
5
|
+
// Blank cells are part of the glyph's fixed 26-column grid: pad, never trim.
|
|
6
|
+
|
|
7
|
+
/** Half-block whale glyph rows; render with the brand color. */
|
|
8
|
+
export const WHALE_GLYPH: readonly string[] = [
|
|
9
|
+
' ▄▄▄▄▄▄▄▄█ ▄█▄ ▄',
|
|
10
|
+
' ▄▄██████████▄▄ ▀███▄████',
|
|
11
|
+
'▄███████████████▄ ███▀▀▀ ',
|
|
12
|
+
'██ ▀▀█████▄▀██████ ',
|
|
13
|
+
'██▄ ▀████▄▄████ ',
|
|
14
|
+
' ██▄ ▀██████▀ ',
|
|
15
|
+
' ▀██▄▄ ██▄ ▀███▄▄ ',
|
|
16
|
+
' ▀▀███████▀▀ ▀▀▀ ',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
/** Fixed glyph width in terminal columns. */
|
|
20
|
+
export const WHALE_GLYPH_COLUMNS = 26
|
|
21
|
+
|
|
22
|
+
/** Fixed glyph height in half-block rows. */
|
|
23
|
+
export const WHALE_GLYPH_ROWS = 8
|
package/README.zh.md
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# dsh-code
|
|
2
|
-
|
|
3
|
-
[English](README.md) | 中文
|
|
4
|
-
|
|
5
|
-
为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)打造的 Claude-Code 式交互终端(TUI)bundle,以树外插件 bundle 的形式组合在官方 `@deepseek-ai/dsh-base` 之上——与官方 Web 界面同一套插件生态,零 fork。
|
|
6
|
-
|
|
7
|
-
## 功能
|
|
8
|
-
|
|
9
|
-
- DeepSeek 蓝横幅:鲸鱼字标由官方 FishLogo 精确路径半块栅格化,头部贴内容宽度、紧凑不占满
|
|
10
|
-
- 实时会话流:直接从持久会话日志投影——用户输入、流式助手文本、紧凑工具调用/斜杠命令行(运行/完成/出错标记)、todo 快照
|
|
11
|
-
- **工具审批 y/n 条**:agent 请求许可时(sandbox 升级、hook 的 ask 决策),琥珀色审批条显示原因与配对命令行;`y` 允许一次、`n` 拒绝
|
|
12
|
-
- **`/model` 面板**:列出 `llm` 注册表的全部 provider 路由,为下一步切换会话模型;恢复的会话自动还原其上次的模型
|
|
13
|
-
- **会话恢复**:`--resume <id|前缀>` 续接持久会话,`--continue` 取当前目录最新一个;完整转录从日志重放,续写同一持久会话
|
|
14
|
-
- **斜杠命令透传**:共享 `ctx.commands` 注册表(Web 作曲栏同一分发面)里的命令都可在终端执行,`/` 弹出补全菜单;用户可调用技能也进同一菜单(标注 `skill`),未知 `/name` 回退为普通提示词、由 host 的技能注入接管
|
|
15
|
-
- **todo 面板**:实时 todo 列表内联渲染,含 done/active/pending 计数与三态标记,每个新 turn 清空(对齐 Web TodoPanel)
|
|
16
|
-
- **思考行**:模型推理以 Claude Code 式 `✻` 折叠呈现——默认收起为 dim 标记 + 字符数,展开为 dim 斜体,模型思考时流式显示;Ctrl+R 全局切换
|
|
17
|
-
- **终端 markdown**:助手回复经纯 GFM 子集渲染器(标题/围栏与行内代码/强调/列表/引用/链接)按终端宽度排版;流式阶段保持纯文本直到消息落定
|
|
18
|
-
- **Ctrl+O 历史检查器**:逐条浏览保留的完整转录,同时保留输入框与状态栏;←/→ 切换条目,↑/↓ 与 PageUp/PageDown 滚动全部内容,`g`/`G` 跳到两端
|
|
19
|
-
- **结构化工具详情**:持久化的 edit/write diff、带行号 read 窗口、web 搜索来源、fetch 摘要与有界原始输出在普通转录中保持紧凑,并可在 Ctrl+O 中展开查看完整展示
|
|
20
|
-
- **ask_user_question 问答条**:模型提问呈现为选项菜单(↑/↓ 移动、space 多选、`c` 自定义答案、Esc 中断);计划评审(exit_plan_mode)走同一条并高亮 approve 选项
|
|
21
|
-
- **@ 补全**:`@` 触发工作区文件与持久会话补全;会话引用展开为有界只读快照,以带来源的上下文注入到提示词之前
|
|
22
|
-
- **plan 与权限**:状态栏 `⧉ plan` 与 `⛨ <preset>` 徽章;`/permission <name>` 切换会话预设,Shift+Tab 循环切换(registry 自带的 `/plan` 命令启用 plan 模式)
|
|
23
|
-
- **终端本地工作流**:`/help` 打开完整按键/命令/技能说明,`/export` 将折叠转录写为 Markdown,`/title` 固定会话标题,Ctrl+K 删除到行尾,Ctrl+L 重绘终端,裸工作区路径参与 Tab 补全
|
|
24
|
-
- 输入组件:历史(↑/↓)、光标编辑(←/→、Ctrl+A/E/U)、斜杠命令/技能/@ 补全 Tab 补全;运行中提交即 steering(下一个 step 边界消费),`Esc` 或 Ctrl+C 中断本轮,Ctrl+C 在空闲空输入时退出,Ctrl+D 运行中拒绝退出
|
|
25
|
-
- 融合型状态栏:Claude Code 式身份信息(模型、工作目录、git 分支、标题/会话、plan、权限预设、goal 与 sandbox 覆盖)+ Web 作曲栏指标(轮数/步数、llm 与 tool 累计时长、TTFT、解码 tok/s、上下文占用、缓存命中、token 总量)
|
|
26
|
-
- 有界动态渲染:流式输出、Ctrl+O、`/help`、`/model`、审批与问题/计划评审面板均受终端视口约束;输入框始终位于状态栏正上方,连续缩放只在最终宽度执行一次防抖重排
|
|
27
|
-
|
|
28
|
-
## 安装
|
|
29
|
-
|
|
30
|
-
需要 Node `^22.19 || >=24` 与 `dsh` CLI(`npm i -g @deepseek-ai/dsh@next`)。
|
|
31
|
-
|
|
32
|
-
```sh
|
|
33
|
-
dsh plugin --profile cli add dsh-code # npm 发布后
|
|
34
|
-
dsh plugin --profile cli add github:unlinearity/dsh-code # 跟踪本仓库
|
|
35
|
-
dsh plugin --profile cli add file:C:/path/to/dsh-code # 本地目录
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
然后:
|
|
39
|
-
|
|
40
|
-
```sh
|
|
41
|
-
dsh --profile cli # 全新会话
|
|
42
|
-
dsh --profile cli --continue # 恢复本目录最新的会话
|
|
43
|
-
dsh --profile cli --resume abc123 # 按会话 id 或唯一前缀恢复
|
|
44
|
-
dsh --profile cli --session my-id # 以显式 id 建新会话
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
在环境变量(或启动目录 / `$DSH_HOME` 的 `.env`)里设置 `DEEPSEEK_API_KEY`。
|
|
48
|
-
|
|
49
|
-
git 安装会在安装期执行构建脚本,pnpm 会先行拦截:若 `add` 失败,按提示把对应键加入 `~/.dsh/profiles/cli/pnpm-workspace.yaml` 的 `allowBuilds` 后重试。
|
|
50
|
-
|
|
51
|
-
## 开发
|
|
52
|
-
|
|
53
|
-
```sh
|
|
54
|
-
pnpm install
|
|
55
|
-
pnpm test # vitest 单元测试
|
|
56
|
-
pnpm typecheck
|
|
57
|
-
pnpm build # tsdown 打包 lib/*.mjs,tsc 产出 lib/types
|
|
58
|
-
pnpm run gen:whale # 从 vendor 的官方路径重新生成 src/whale-glyph.ts
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
鲸鱼点阵由 `scripts/fish-logo.ts` 中 vendor 的 DeepSeek 鱼形 Logo 路径生成(来源:[deepseek-harness](https://github.com/deepseek-ai/deepseek-harness),MIT)。
|
|
62
|
-
|
|
63
|
-
## 许可
|
|
64
|
-
|
|
65
|
-
[MIT](LICENSE)。vendor 的鱼形 Logo 几何数据来自 DeepSeek Harness(MIT)。
|
package/src/pictures/1.png
DELETED
|
Binary file
|