dsh-code 0.7.0 → 0.8.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 +20 -6
- package/README.md +20 -6
- package/lib/index.mjs +2685 -622
- package/lib/types/app.d.ts +77 -1
- package/lib/types/history.d.ts +15 -4
- package/lib/types/index.d.ts +48 -0
- package/lib/types/kernel-panels.d.ts +7 -0
- package/lib/types/permissions.d.ts +37 -0
- package/lib/types/presets.d.ts +2 -0
- package/lib/types/provider-settings.d.ts +144 -0
- package/lib/types/questions.d.ts +2 -0
- package/lib/types/render/animations.d.ts +8 -6
- package/lib/types/render/lines.d.ts +6 -0
- package/lib/types/render/markdown.d.ts +3 -3
- package/lib/types/render/projection.d.ts +95 -3
- package/lib/types/render/status.d.ts +26 -36
- package/lib/types/render/text.d.ts +14 -7
- package/lib/types/render/tool-detail.d.ts +3 -1
- package/lib/types/render/tool-preview.d.ts +4 -1
- package/lib/types/session-directory.d.ts +15 -0
- package/lib/types/store.d.ts +13 -2
- package/lib/types/version.d.ts +5 -0
- package/package.json +1 -1
- package/src/app.ts +847 -150
- package/src/approval.ts +11 -2
- package/src/history.ts +20 -5
- package/src/index.ts +402 -159
- package/src/kernel-panels.ts +45 -8
- package/src/permissions.ts +85 -0
- package/src/presets.ts +12 -0
- package/src/provider-settings.ts +520 -0
- package/src/questions.ts +15 -5
- package/src/render/animations.ts +32 -18
- package/src/render/lines.ts +21 -6
- package/src/render/markdown.ts +302 -4
- package/src/render/projection.ts +665 -10
- package/src/render/status.ts +68 -162
- package/src/render/text.ts +28 -9
- package/src/render/tool-detail.ts +81 -40
- package/src/render/tool-preview.ts +18 -2
- package/src/session-directory.ts +44 -5
- package/src/skills.ts +8 -4
- package/src/store.ts +26 -8
- package/src/version.ts +16 -0
package/src/kernel-panels.ts
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import { createElement, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
|
|
4
4
|
import { Box, Text, useInput, useStdout } from 'ink'
|
|
5
5
|
import type { ModelRow } from './models.ts'
|
|
6
|
+
import type { PermissionRow } from './permissions.ts'
|
|
6
7
|
import type { PresetRow } from './presets.ts'
|
|
7
8
|
import type { PluginRow } from './plugin-inventory.ts'
|
|
8
9
|
import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts'
|
|
9
10
|
import { panelViewport, revealRow } from './render/inspector.ts'
|
|
10
11
|
import { textLines } from './render/lines.ts'
|
|
11
12
|
import { DEFAULT_STATUSLINE_ITEMS, STATUS_ITEMS, type StatusItemId } from './render/status.ts'
|
|
12
|
-
import {
|
|
13
|
+
import { singleLineText, truncateColumns } from './render/text.ts'
|
|
13
14
|
import { getPalette, inkColor } from './theme.ts'
|
|
14
15
|
|
|
15
16
|
interface ListFrameProps {
|
|
@@ -27,7 +28,7 @@ function ListFrame(props: ListFrameProps): ReactElement {
|
|
|
27
28
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
28
29
|
if (viewport.maxHeight === 0) return createElement(Box, { display: 'none' })
|
|
29
30
|
if (viewport.compact) {
|
|
30
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(`${props.title} · esc close
|
|
31
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(singleLineText(`${props.title} · esc close`), viewport.contentColumns))
|
|
31
32
|
}
|
|
32
33
|
const stateRows = props.loading
|
|
33
34
|
? [{ key: 'loading', text: ' loading…' }]
|
|
@@ -42,8 +43,8 @@ function ListFrame(props: ListFrameProps): ReactElement {
|
|
|
42
43
|
return createElement(
|
|
43
44
|
Box,
|
|
44
45
|
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(getPalette().dim), flexDirection: 'column', paddingX: 1 },
|
|
45
|
-
createElement(Text, { color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns(props.title, viewport.contentColumns)),
|
|
46
|
-
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(`search: ${props.query === '' ? 'type to filter' : props.query}
|
|
46
|
+
createElement(Text, { color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns(singleLineText(props.title), viewport.contentColumns)),
|
|
47
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(singleLineText(`search: ${props.query === '' ? 'type to filter' : props.query}`), viewport.contentColumns)),
|
|
47
48
|
...visible.map((row, index) => {
|
|
48
49
|
const absolute = offset + index
|
|
49
50
|
const selected = !props.loading && props.error === undefined && props.rows.length > 0 && absolute === props.cursor
|
|
@@ -52,9 +53,9 @@ function ListFrame(props: ListFrameProps): ReactElement {
|
|
|
52
53
|
color: selected ? inkColor(getPalette().brandBright) : row.disabled ? inkColor(getPalette().dim) : undefined,
|
|
53
54
|
dimColor: row.disabled,
|
|
54
55
|
wrap: 'truncate-end',
|
|
55
|
-
}, truncateColumns(`${selected ? '› ' : ' '}${row.text}`, viewport.contentColumns))
|
|
56
|
+
}, truncateColumns(`${selected ? '› ' : ' '}${singleLineText(row.text)}`, viewport.contentColumns))
|
|
56
57
|
}),
|
|
57
|
-
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(props.footer, viewport.contentColumns)),
|
|
58
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(singleLineText(props.footer), viewport.contentColumns)),
|
|
58
59
|
)
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -100,6 +101,42 @@ export function ModePanel({ current, load, select, close }: {
|
|
|
100
101
|
})
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
export function PermissionPanel({ current, load, select, close }: {
|
|
105
|
+
current: string
|
|
106
|
+
load(): Promise<readonly PermissionRow[]>
|
|
107
|
+
select(id: string): void
|
|
108
|
+
close(): void
|
|
109
|
+
}): ReactElement {
|
|
110
|
+
const [rows, setRows] = useState<readonly PermissionRow[]>([])
|
|
111
|
+
const [query, setQuery] = useState('')
|
|
112
|
+
const [cursor, setCursor] = useState(0)
|
|
113
|
+
const [loading, setLoading] = useState(true)
|
|
114
|
+
const [error, setError] = useState<string>()
|
|
115
|
+
const refresh = (): void => {
|
|
116
|
+
setLoading(true); setError(undefined)
|
|
117
|
+
Promise.resolve().then(load).then(value => { setRows(value); setLoading(false) }, reason => {
|
|
118
|
+
setError(reason instanceof Error ? reason.message : String(reason)); setLoading(false)
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
useEffect(refresh, [])
|
|
122
|
+
const visible = useMemo(() => rows.filter(row => `${row.id} ${row.description ?? ''}`.toLowerCase().includes(query.toLowerCase())), [rows, query])
|
|
123
|
+
useEffect(() => setCursor(value => Math.min(value, Math.max(0, visible.length - 1))), [visible.length])
|
|
124
|
+
useInput((input, key) => {
|
|
125
|
+
if (key.escape || input === 'q') return close()
|
|
126
|
+
if (input === 'r' && query === '') return refresh()
|
|
127
|
+
if (key.upArrow) return setCursor(value => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length)
|
|
128
|
+
if (key.downArrow) return setCursor(value => visible.length === 0 ? 0 : (value + 1) % visible.length)
|
|
129
|
+
if (key.return && visible[cursor] !== undefined) return select(visible[cursor]!.id)
|
|
130
|
+
const next = editQuery(query, input, key)
|
|
131
|
+
if (next !== undefined) { setQuery(next); setCursor(0) }
|
|
132
|
+
})
|
|
133
|
+
return createElement(ListFrame, {
|
|
134
|
+
title: `/permission · current ${current}`,
|
|
135
|
+
rows: visible.map(row => ({ key: row.id, text: `${row.id === current ? '●' : '○'} ${row.id}${row.description === undefined ? '' : ` · ${row.description}`}` })),
|
|
136
|
+
cursor, loading, error, query, footer: '↑↓ choose · enter select · r refresh · esc close',
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
103
140
|
export function PluginPanel({ load, close, initialQuery = '' }: { load(): readonly PluginRow[]; close(): void; initialQuery?: string }): ReactElement {
|
|
104
141
|
const [epoch, setEpoch] = useState(0)
|
|
105
142
|
const [query, setQuery] = useState(initialQuery)
|
|
@@ -245,7 +282,7 @@ function DocumentPanel({ title, text, error, close }: {
|
|
|
245
282
|
return createElement(
|
|
246
283
|
Box,
|
|
247
284
|
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(getPalette().dim), flexDirection: 'column', paddingX: 1 },
|
|
248
|
-
createElement(Text, { color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns(title, viewport.contentColumns)),
|
|
285
|
+
createElement(Text, { color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns(singleLineText(title), viewport.contentColumns)),
|
|
249
286
|
...body.map((line, index) => createElement(Text, { key: `${scroll}-${index}`, wrap: 'truncate-end' }, truncateColumns(line, viewport.contentColumns))),
|
|
250
287
|
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(`lines ${lines.length === 0 ? 0 : scroll + 1}-${Math.min(lines.length, scroll + viewport.bodyRows)}/${lines.length} · ↑↓/pg/g/G · t/esc close`, viewport.contentColumns)),
|
|
251
288
|
)
|
|
@@ -319,7 +356,7 @@ export function HistoryPanel({ entries, fill, close }: {
|
|
|
319
356
|
color: selected ? inkColor(getPalette().brandBright) : undefined,
|
|
320
357
|
wrap: 'truncate-end',
|
|
321
358
|
},
|
|
322
|
-
truncateColumns((selected ? '› ' : ' ') +
|
|
359
|
+
truncateColumns((selected ? '› ' : ' ') + singleLineText(entry), viewport.contentColumns),
|
|
323
360
|
)
|
|
324
361
|
})),
|
|
325
362
|
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns('↑↓ move · g/G ends · enter fill · esc close', viewport.contentColumns)),
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Permission-preset policy for pending and active TUI sessions. */
|
|
2
|
+
|
|
3
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
+
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
5
|
+
|
|
6
|
+
/** One selectable permission preset row for the /permission panel. */
|
|
7
|
+
export interface PermissionRow {
|
|
8
|
+
readonly id: string
|
|
9
|
+
readonly description?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Structural boundary over Harness permission presets; values stay service-owned. */
|
|
13
|
+
export interface PermissionPresetsService {
|
|
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
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Read the optional Harness service without importing its runtime package. */
|
|
24
|
+
export function permissionPresetsFrom(ctx: Context): PermissionPresetsService | undefined {
|
|
25
|
+
return (ctx as unknown as { get(name: string): unknown }).get('permissionPresets') as PermissionPresetsService | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Effective label for either an active session or the not-yet-created first one. */
|
|
29
|
+
export function effectivePermission(
|
|
30
|
+
service: PermissionPresetsService,
|
|
31
|
+
session: Session | undefined,
|
|
32
|
+
pending: string | undefined,
|
|
33
|
+
): string {
|
|
34
|
+
return session === undefined ? pending ?? service.defaultPreset : service.current(session.events)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Validate a preset and write it only when a durable session already exists. */
|
|
38
|
+
export function selectPermission(
|
|
39
|
+
service: PermissionPresetsService,
|
|
40
|
+
session: Session | undefined,
|
|
41
|
+
preset: string,
|
|
42
|
+
): string {
|
|
43
|
+
service.resolve(preset)
|
|
44
|
+
if (session !== undefined) service.set(session, preset)
|
|
45
|
+
return preset
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Cycle table order from the active, pending, or configured-default value. */
|
|
49
|
+
export function cyclePermission(
|
|
50
|
+
service: PermissionPresetsService,
|
|
51
|
+
session: Session | undefined,
|
|
52
|
+
pending: string | undefined,
|
|
53
|
+
): string {
|
|
54
|
+
if (service.names.length === 0) return ''
|
|
55
|
+
const at = service.names.indexOf(effectivePermission(service, session, pending))
|
|
56
|
+
const next = service.names[(at + 1) % service.names.length] ?? ''
|
|
57
|
+
return next === '' ? '' : selectPermission(service, session, next)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Materialize a pre-session choice after Harness creates the first session. */
|
|
61
|
+
export function applyPendingPermission(
|
|
62
|
+
service: PermissionPresetsService,
|
|
63
|
+
session: Session,
|
|
64
|
+
pending: string | undefined,
|
|
65
|
+
): void {
|
|
66
|
+
if (pending !== undefined && effectivePermission(service, session, undefined) !== pending) {
|
|
67
|
+
selectPermission(service, session, pending)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* List every switchable preset for the /permission panel, table order kept.
|
|
73
|
+
* Description lookup failures degrade to an undocumented row, never a failed
|
|
74
|
+
* panel load — `optionOf` rejects names its table no longer knows.
|
|
75
|
+
*/
|
|
76
|
+
export function listPermissionRows(service: PermissionPresetsService): readonly PermissionRow[] {
|
|
77
|
+
return service.names.map((id) => {
|
|
78
|
+
if (service.optionOf === undefined) return { id }
|
|
79
|
+
try {
|
|
80
|
+
return { id, description: service.optionOf(id)?.description }
|
|
81
|
+
} catch {
|
|
82
|
+
return { id }
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
}
|
package/src/presets.ts
CHANGED
|
@@ -45,6 +45,18 @@ export function resolvePreset(session: Pick<Session, 'header' | 'events'>): stri
|
|
|
45
45
|
return session.header.agentPreset ?? 'standard'
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Resolve a pre-session choice, or recompose an active blank Agent. */
|
|
49
|
+
export async function selectPreset(
|
|
50
|
+
service: AgentPresetsService,
|
|
51
|
+
agent: Agent | undefined,
|
|
52
|
+
presetId: string,
|
|
53
|
+
): Promise<PresetRow> {
|
|
54
|
+
if (agent !== undefined) return switchPreset(service, agent, presetId)
|
|
55
|
+
const preset = await service.resolve(presetId)
|
|
56
|
+
if (preset.broken !== undefined) throw new Error(preset.broken)
|
|
57
|
+
return preset
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
/** Recompose atomically from the caller's perspective, logging only success. */
|
|
49
61
|
export async function switchPreset(
|
|
50
62
|
service: AgentPresetsService,
|