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.
Files changed (44) hide show
  1. package/README.en.md +20 -6
  2. package/README.md +20 -6
  3. package/lib/index.mjs +2685 -622
  4. package/lib/types/app.d.ts +77 -1
  5. package/lib/types/history.d.ts +15 -4
  6. package/lib/types/index.d.ts +48 -0
  7. package/lib/types/kernel-panels.d.ts +7 -0
  8. package/lib/types/permissions.d.ts +37 -0
  9. package/lib/types/presets.d.ts +2 -0
  10. package/lib/types/provider-settings.d.ts +144 -0
  11. package/lib/types/questions.d.ts +2 -0
  12. package/lib/types/render/animations.d.ts +8 -6
  13. package/lib/types/render/lines.d.ts +6 -0
  14. package/lib/types/render/markdown.d.ts +3 -3
  15. package/lib/types/render/projection.d.ts +95 -3
  16. package/lib/types/render/status.d.ts +26 -36
  17. package/lib/types/render/text.d.ts +14 -7
  18. package/lib/types/render/tool-detail.d.ts +3 -1
  19. package/lib/types/render/tool-preview.d.ts +4 -1
  20. package/lib/types/session-directory.d.ts +15 -0
  21. package/lib/types/store.d.ts +13 -2
  22. package/lib/types/version.d.ts +5 -0
  23. package/package.json +1 -1
  24. package/src/app.ts +847 -150
  25. package/src/approval.ts +11 -2
  26. package/src/history.ts +20 -5
  27. package/src/index.ts +402 -159
  28. package/src/kernel-panels.ts +45 -8
  29. package/src/permissions.ts +85 -0
  30. package/src/presets.ts +12 -0
  31. package/src/provider-settings.ts +520 -0
  32. package/src/questions.ts +15 -5
  33. package/src/render/animations.ts +32 -18
  34. package/src/render/lines.ts +21 -6
  35. package/src/render/markdown.ts +302 -4
  36. package/src/render/projection.ts +665 -10
  37. package/src/render/status.ts +68 -162
  38. package/src/render/text.ts +28 -9
  39. package/src/render/tool-detail.ts +81 -40
  40. package/src/render/tool-preview.ts +18 -2
  41. package/src/session-directory.ts +44 -5
  42. package/src/skills.ts +8 -4
  43. package/src/store.ts +26 -8
  44. package/src/version.ts +16 -0
@@ -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 { displayText, singleLineText, truncateColumns } from './render/text.ts'
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`, viewport.contentColumns))
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}`, viewport.contentColumns)),
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 ? '› ' : ' ') + displayText(entry), viewport.contentColumns),
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,