dsh-code 0.4.0 → 0.5.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.md +201 -61
- package/README.zh.md +204 -65
- package/bin/deepseek.mjs +70 -0
- package/cordis.patch.yml +62 -6
- package/lib/index.mjs +1108 -250
- package/lib/startup.mjs +34 -17
- package/lib/types/app.d.ts +19 -1
- package/lib/types/commands.d.ts +2 -0
- package/lib/types/index.d.ts +5 -5
- package/lib/types/internals.d.ts +2 -0
- package/lib/types/kernel-panels.d.ts +23 -0
- package/lib/types/plugin-inventory.d.ts +11 -0
- package/lib/types/presets.d.ts +32 -0
- package/lib/types/render/inspector.d.ts +4 -0
- package/lib/types/render/status.d.ts +2 -0
- 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 +6 -1
- package/src/app.ts +311 -75
- package/src/commands.ts +15 -1
- package/src/index.ts +332 -133
- package/src/internals.ts +5 -0
- package/src/kernel-panels.ts +254 -0
- package/src/plugin-inventory.ts +47 -0
- package/src/presets.ts +64 -0
- package/src/render/inspector.ts +13 -4
- package/src/render/markdown.ts +15 -1
- package/src/render/status.ts +3 -0
- 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/pictures/1.png +0 -0
|
@@ -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/pictures/1.png
DELETED
|
Binary file
|