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.
Files changed (49) hide show
  1. package/README.en.md +217 -0
  2. package/README.md +216 -67
  3. package/bin/deepseek.mjs +70 -0
  4. package/cordis.patch.yml +62 -6
  5. package/lib/devtools-CdTl3MNy.mjs +3643 -0
  6. package/lib/index.mjs +27467 -673
  7. package/lib/rolldown-runtime-CMFfr-1z.mjs +26 -0
  8. package/lib/startup.mjs +34 -17
  9. package/lib/types/app.d.ts +29 -1
  10. package/lib/types/commands.d.ts +2 -0
  11. package/lib/types/history.d.ts +79 -0
  12. package/lib/types/index.d.ts +5 -5
  13. package/lib/types/internals.d.ts +2 -0
  14. package/lib/types/kernel-panels.d.ts +48 -0
  15. package/lib/types/plugin-inventory.d.ts +11 -0
  16. package/lib/types/presets.d.ts +32 -0
  17. package/lib/types/render/animations.d.ts +10 -1
  18. package/lib/types/render/inspector.d.ts +6 -0
  19. package/lib/types/render/projection.d.ts +21 -1
  20. package/lib/types/render/status.d.ts +131 -14
  21. package/lib/types/render/text.d.ts +9 -0
  22. package/lib/types/session-directory.d.ts +54 -0
  23. package/lib/types/session-switch.d.ts +17 -0
  24. package/lib/types/skills.d.ts +2 -0
  25. package/lib/types/startup.d.ts +11 -1
  26. package/package.json +117 -112
  27. package/src/app.ts +2543 -1969
  28. package/src/commands.ts +15 -1
  29. package/src/history.ts +136 -0
  30. package/src/index.ts +550 -155
  31. package/src/internals.ts +5 -0
  32. package/src/kernel-panels.ts +419 -0
  33. package/src/plugin-inventory.ts +47 -0
  34. package/src/presets.ts +64 -0
  35. package/src/render/animations.ts +14 -1
  36. package/src/render/export.ts +4 -0
  37. package/src/render/inspector.ts +23 -5
  38. package/src/render/lines.ts +21 -10
  39. package/src/render/markdown.ts +15 -1
  40. package/src/render/projection.ts +71 -8
  41. package/src/render/status.ts +522 -65
  42. package/src/render/text.ts +34 -6
  43. package/src/session-directory.ts +102 -0
  44. package/src/session-switch.ts +58 -0
  45. package/src/skills.ts +20 -7
  46. package/src/startup.ts +38 -20
  47. package/src/whale-glyph.ts +23 -23
  48. package/README.zh.md +0 -65
  49. package/src/pictures/1.png +0 -0
@@ -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
- /** A display-safe suffix bounded by terminal rows and columns. */
27
- export interface DisplayTail {
28
- /** Sanitized suffix suitable for direct terminal rendering. */
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
- if (skills === undefined || agent === undefined) return
70
- skills.list({
71
- cwd: agent.session.header.cwd,
72
- scope: agent,
73
- }).then((summaries: readonly SkillSummary[]) => {
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
- if (next.length === rows.length && next.every((row, index) => row.name === rows[index]?.name)) return
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<{ resume?: string; continue?: boolean; session?: string }>()
71
- const selected = [options.resume !== undefined, options.continue === true, options.session !== undefined]
72
- if (selected.filter(Boolean).length > 1) {
73
- program.error('error: --resume, --continue, and --session are mutually exclusive')
74
- }
75
- if (options.session !== undefined && options.session === '') {
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
- const startup: TuiStartup = options.resume !== undefined
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)
@@ -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)。
Binary file