dsh-code 1.3.0 → 1.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.
Files changed (103) hide show
  1. package/README.en.md +10 -8
  2. package/README.md +10 -8
  3. package/lib/index.mjs +6980 -5942
  4. package/lib/session-query.mjs +18 -1
  5. package/lib/types/app.d.ts +38 -65
  6. package/lib/types/completion.d.ts +29 -0
  7. package/lib/types/composer.d.ts +150 -0
  8. package/lib/types/git-workflow.d.ts +6 -0
  9. package/lib/types/index.d.ts +6 -188
  10. package/lib/types/locales/en.d.ts +20 -8
  11. package/lib/types/{authorization-panel.d.ts → panels/authorization-panel.d.ts} +6 -1
  12. package/lib/types/panels/completion-panel.d.ts +13 -0
  13. package/lib/types/panels/interaction-bars.d.ts +38 -0
  14. package/lib/types/{kernel-panels.d.ts → panels/kernel-panels.d.ts} +16 -37
  15. package/lib/types/{language-panel.d.ts → panels/language-panel.d.ts} +1 -1
  16. package/lib/types/panels/model-panels.d.ts +86 -0
  17. package/lib/types/{theme-panel.d.ts → panels/theme-panel.d.ts} +1 -1
  18. package/lib/types/{update-panel.d.ts → panels/update-panel.d.ts} +1 -1
  19. package/lib/types/provider-settings.d.ts +11 -0
  20. package/lib/types/render/inspector.d.ts +16 -0
  21. package/lib/types/render/projection-events.d.ts +33 -0
  22. package/lib/types/render/text.d.ts +4 -0
  23. package/lib/types/render/transcript-viewport.d.ts +32 -0
  24. package/lib/types/runner/harness-gate.d.ts +83 -0
  25. package/lib/types/runner/input-history.d.ts +31 -0
  26. package/lib/types/runner/mode-cycle.d.ts +44 -0
  27. package/lib/types/runner/preferences.d.ts +40 -0
  28. package/lib/types/runner/quit.d.ts +27 -0
  29. package/lib/types/runner/search-rows.d.ts +38 -0
  30. package/lib/types/runner/session-io.d.ts +57 -0
  31. package/lib/types/runner/session-target.d.ts +42 -0
  32. package/lib/types/runner/startup-config.d.ts +33 -0
  33. package/lib/types/runner/submissions.d.ts +87 -0
  34. package/lib/types/session/attach.d.ts +39 -0
  35. package/lib/types/{store.d.ts → session/store.d.ts} +1 -1
  36. package/lib/types/{subagents.d.ts → session/subagents.d.ts} +8 -1
  37. package/lib/types/session-query.d.ts +11 -0
  38. package/lib/types/settings-file.d.ts +10 -0
  39. package/lib/types/{panel-accent.d.ts → ui/panel-accent.d.ts} +1 -1
  40. package/lib/types/ui/panel-gap.d.ts +6 -0
  41. package/lib/types/ui/query-editor.d.ts +10 -0
  42. package/lib/types/ui/styled-rows.d.ts +8 -0
  43. package/lib/types/{terminal-title.d.ts → ui/terminal-title.d.ts} +1 -1
  44. package/lib/types/ui/ui-contract.d.ts +12 -0
  45. package/lib/types/ui/use-frames.d.ts +6 -0
  46. package/lib/types/ui/use-stable-input.d.ts +7 -0
  47. package/package.json +3 -2
  48. package/src/app.ts +802 -4162
  49. package/src/completion.ts +117 -0
  50. package/src/composer.ts +1956 -0
  51. package/src/git-workflow.ts +18 -0
  52. package/src/index.ts +113 -633
  53. package/src/internals.ts +1 -1
  54. package/src/locales/en.ts +20 -8
  55. package/src/locales/zh.ts +20 -8
  56. package/src/{authorization-panel.ts → panels/authorization-panel.ts} +25 -6
  57. package/src/panels/completion-panel.ts +79 -0
  58. package/src/panels/interaction-bars.ts +567 -0
  59. package/src/{kernel-panels.ts → panels/kernel-panels.ts} +84 -65
  60. package/src/{language-panel.ts → panels/language-panel.ts} +4 -4
  61. package/src/panels/model-panels.ts +1021 -0
  62. package/src/{theme-panel.ts → panels/theme-panel.ts} +5 -5
  63. package/src/{update-panel.ts → panels/update-panel.ts} +6 -6
  64. package/src/provider-settings.ts +38 -0
  65. package/src/render/inspector.ts +35 -0
  66. package/src/render/lines.ts +45 -18
  67. package/src/render/projection-events.ts +109 -0
  68. package/src/render/projection.ts +11 -2
  69. package/src/render/text.ts +8 -0
  70. package/src/render/tool-preview.ts +35 -2
  71. package/src/render/transcript-viewport.ts +61 -0
  72. package/src/render/usage.ts +1 -2
  73. package/src/runner/harness-gate.ts +168 -0
  74. package/src/runner/input-history.ts +77 -0
  75. package/src/runner/mode-cycle.ts +49 -0
  76. package/src/runner/preferences.ts +67 -0
  77. package/src/runner/quit.ts +53 -0
  78. package/src/runner/search-rows.ts +55 -0
  79. package/src/runner/session-io.ts +245 -0
  80. package/src/runner/session-target.ts +81 -0
  81. package/src/runner/startup-config.ts +54 -0
  82. package/src/runner/submissions.ts +157 -0
  83. package/src/session/attach.ts +87 -0
  84. package/src/{session-directory.ts → session/session-directory.ts} +1 -1
  85. package/src/{store.ts → session/store.ts} +1 -1
  86. package/src/{subagents.ts → session/subagents.ts} +12 -1
  87. package/src/session-query.ts +22 -0
  88. package/src/settings-file.ts +19 -1
  89. package/src/{panel-accent.ts → ui/panel-accent.ts} +1 -1
  90. package/src/ui/panel-gap.ts +9 -0
  91. package/src/ui/query-editor.ts +16 -0
  92. package/src/ui/styled-rows.ts +124 -0
  93. package/src/{terminal-title.ts → ui/terminal-title.ts} +1 -1
  94. package/src/ui/ui-contract.ts +10 -0
  95. package/src/ui/use-frames.ts +23 -0
  96. package/src/ui/use-stable-input.ts +17 -0
  97. /package/lib/types/{fork.d.ts → session/fork.d.ts} +0 -0
  98. /package/lib/types/{history.d.ts → session/history.d.ts} +0 -0
  99. /package/lib/types/{session-directory.d.ts → session/session-directory.d.ts} +0 -0
  100. /package/lib/types/{session-switch.d.ts → session/session-switch.d.ts} +0 -0
  101. /package/src/{fork.ts → session/fork.ts} +0 -0
  102. /package/src/{history.ts → session/history.ts} +0 -0
  103. /package/src/{session-switch.ts → session/session-switch.ts} +0 -0
@@ -2,25 +2,29 @@
2
2
 
3
3
  import { createElement, useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
4
4
  import { Box, Text, useInput, useStdout } from 'ink'
5
- import type { ModelDirectory, ModelRow } from './models.ts'
6
- import type { SubagentRow } from './subagents.ts'
7
- import type { ScheduleRow } from './render/projection.ts'
8
- import type { PermissionRow } from './permissions.ts'
9
- import type { PresetRow } from './presets.ts'
10
- import type { PluginRow } from './plugin-inventory.ts'
11
- import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts'
12
- import { formatRelativeTime, matchSessionRow } from './session-directory.ts'
13
- import type { ReviewBranch, ReviewCommit, ReviewSelection } from './git-workflow.ts'
14
- import { panelViewport, revealRow } from './render/inspector.ts'
15
- import { markdownLines, textLines, type LineStyle, type StyledLine } from './render/lines.ts'
16
- import { usageLines, type UsageView } from './render/usage.ts'
17
- import { deleteLastGrapheme } from './render/editor.ts'
18
- import { stripPasteMarkers } from './keyboard.ts'
19
- import { DEFAULT_STATUSLINE_ITEMS, localizedStatusItems, type StatusItemId } from './render/status.ts'
20
- import { singleLineText, truncateColumns } from './render/text.ts'
21
- import { panelAccent } from './panel-accent.ts'
22
- import { t } from './i18n.ts'
23
- import { getPalette, inkColor } from './theme.ts'
5
+ import type { ModelDirectory, ModelRow } from '../models.ts'
6
+ import type { SubagentRow } from '../session/subagents.ts'
7
+ import type { SearchRow } from '../runner/search-rows.ts'
8
+ export type { SearchRow } from '../runner/search-rows.ts'
9
+ import type { ScheduleRow } from '../render/projection.ts'
10
+ import type { PermissionRow } from '../permissions.ts'
11
+ import type { PresetRow } from '../presets.ts'
12
+ import type { PluginRow } from '../plugin-inventory.ts'
13
+ import type { SessionDirectoryOptions, SessionRow } from '../session/session-directory.ts'
14
+ import { formatRelativeTime, matchSessionRow } from '../session/session-directory.ts'
15
+ import type { ReviewBranch, ReviewCommit, ReviewSelection } from '../git-workflow.ts'
16
+ import { expandedDocumentViewport, panelViewport, revealRow } from '../render/inspector.ts'
17
+ import { markdownLines, textLines, type LineStyle, type StyledLine } from '../render/lines.ts'
18
+ import { usageLines, type UsageView } from '../render/usage.ts'
19
+ import { deleteLastGrapheme } from '../render/editor.ts'
20
+ import { stripPasteMarkers } from '../keyboard.ts'
21
+ import { DEFAULT_STATUSLINE_ITEMS, localizedStatusItems, type StatusItemId } from '../render/status.ts'
22
+ import { singleLineText, truncateColumns } from '../render/text.ts'
23
+ import { panelAccent } from '../ui/panel-accent.ts'
24
+ import { editQuery } from '../ui/query-editor.ts'
25
+ export { editQuery } from '../ui/query-editor.ts'
26
+ import { t } from '../i18n.ts'
27
+ import { getPalette, inkColor } from '../theme.ts'
24
28
 
25
29
  interface ListFrameProps {
26
30
  readonly title: string
@@ -96,18 +100,6 @@ function ListFrame(props: ListFrameProps): ReactElement {
96
100
  )
97
101
  }
98
102
 
99
- /**
100
- * Apply one keystroke to a panel search query. IME commits arrive as one
101
- * multi-character chunk, so the whole printable run is appended; paste
102
- * markers are stripped and control-laden chunks are ignored.
103
- */
104
- export function editQuery(query: string, input: string, key: { backspace?: boolean; delete?: boolean }): string | undefined {
105
- if (key.backspace || key.delete) return deleteLastGrapheme(query)
106
- const text = stripPasteMarkers(input)
107
- if (text !== '' && !/[\u0000-\u001f\u007f]/u.test(text)) return query + text
108
- return undefined
109
- }
110
-
111
103
  export function ModePanel({ current, load, select, close }: {
112
104
  current: string
113
105
  load: () => Promise<readonly PresetRow[]>
@@ -472,24 +464,50 @@ function DocumentRows({ lines }: { lines: readonly StyledLine[] }): ReactElement
472
464
  )
473
465
  }
474
466
 
475
- function DocumentPanel({ title, text, error, close }: {
467
+ function DocumentPanel({ title, text, error, close, onRefresh, onExpand }: {
476
468
  title: string
477
469
  text?: string
478
470
  error?: string
479
471
  close: () => void
472
+ /** Re-read the document; offered as `r` (running subagents keep moving). */
473
+ onRefresh?: () => void
474
+ /** Replace this preview with a fuller surface (subagent attachment). */
475
+ onExpand?: () => void
480
476
  }): ReactElement {
481
477
  const stdout = useStdout().stdout
482
- const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
478
+ // Expanded mode: the document replaces the transcript area (near the whole
479
+ // terminal minus the bottom live region), entered from the preview with
480
+ // Enter/f. Esc leaves for the caller's list directly — the preview is a
481
+ // peek step, not a level to back through.
482
+ const [expanded, setExpanded] = useState(false)
483
+ const viewport = expanded
484
+ ? expandedDocumentViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
485
+ : panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
483
486
  const [scroll, setScroll] = useState(0)
484
487
  const lines = useMemo(() => text === undefined ? [] : markdownLines(text, viewport.contentColumns), [text, viewport.contentColumns])
488
+ const maxScroll = Math.max(0, lines.length - viewport.bodyRows)
485
489
  useInput((input, key) => {
486
490
  if (key.escape || input === 'q' || input === 't') return close()
491
+ // Ctrl+D keeps its step-out meaning: empty-draft exit in the composer,
492
+ // leave-the-document here.
493
+ if (expanded && key.ctrl && input === 'd') return close()
494
+ if (!expanded && (key.return || input === 'f')) {
495
+ if (onExpand !== undefined) {
496
+ close()
497
+ onExpand()
498
+ return
499
+ }
500
+ setExpanded(true)
501
+ setScroll(value => Math.min(value, Math.max(0, lines.length - expandedDocumentViewport(stdout?.columns ?? 80, stdout?.rows ?? 30).bodyRows)))
502
+ return
503
+ }
504
+ if (expanded && input === 'r' && onRefresh !== undefined) return onRefresh()
487
505
  if (key.upArrow) return setScroll(value => Math.max(0, value - 1))
488
- if (key.downArrow) return setScroll(value => Math.min(Math.max(0, lines.length - viewport.bodyRows), value + 1))
506
+ if (key.downArrow) return setScroll(value => Math.min(maxScroll, value + 1))
489
507
  if (key.pageUp) return setScroll(value => Math.max(0, value - Math.max(1, viewport.bodyRows - 1)))
490
- if (key.pageDown) return setScroll(value => Math.min(Math.max(0, lines.length - viewport.bodyRows), value + Math.max(1, viewport.bodyRows - 1)))
508
+ if (key.pageDown) return setScroll(value => Math.min(maxScroll, value + Math.max(1, viewport.bodyRows - 1)))
491
509
  if (input === 'g') return setScroll(0)
492
- if (input === 'G') return setScroll(Math.max(0, lines.length - viewport.bodyRows))
510
+ if (input === 'G') return setScroll(maxScroll)
493
511
  })
494
512
  if (viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('document.compact'), viewport.contentColumns))
495
513
  const body: readonly StyledLine[] = error !== undefined
@@ -500,10 +518,12 @@ function DocumentPanel({ title, text, error, close }: {
500
518
  const accent = panelAccent('kernel-transcript', getPalette().dim, getPalette().brandBright)
501
519
  return createElement(
502
520
  Box,
503
- { width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
504
- createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(singleLineText(title), viewport.contentColumns)),
521
+ { width: viewport.outerColumns, borderStyle: 'round', borderColor: expanded ? inkColor(accent.title) : inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
522
+ createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(singleLineText(title) + (expanded ? ` · ${t('document.expanded')}` : ''), viewport.contentColumns)),
505
523
  createElement(DocumentRows, { lines: body }),
506
- createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(t('document.footer', { from: lines.length === 0 ? 0 : scroll + 1, to: Math.min(lines.length, scroll + viewport.bodyRows), total: lines.length }), viewport.contentColumns)),
524
+ createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(expanded
525
+ ? t('document.footerExpanded', { from: lines.length === 0 ? 0 : scroll + 1, to: Math.min(lines.length, scroll + viewport.bodyRows), total: lines.length, refresh: onRefresh === undefined ? '' : ` · ${t('document.refreshHint')}` })
526
+ : t('document.footer', { from: lines.length === 0 ? 0 : scroll + 1, to: Math.min(lines.length, scroll + viewport.bodyRows), total: lines.length }), viewport.contentColumns)),
507
527
  )
508
528
  }
509
529
 
@@ -747,24 +767,6 @@ export function ReviewPickerPanel({ loadBranches, loadCommits, choose, close }:
747
767
  )
748
768
  }
749
769
 
750
- /** One cross-session full-text search hit mapped from the session-query engine. */
751
- export interface SearchRow {
752
- /** Session id (Enter resumes it through the switch machinery). */
753
- readonly id: string
754
- /** Display label: session title or the short id form. */
755
- readonly label: string
756
- /** Secondary facts line (workspace · preset markers). */
757
- readonly detail: string
758
- /** Bounded plain-text excerpt around the strongest match. */
759
- readonly snippet: string
760
- /** Match timestamp (relative labels derive from it). */
761
- readonly updatedAt: number
762
- /** Whether the hit is a delegated subagent conversation (not resumable). */
763
- readonly subagent: boolean
764
- /** Whether Enter may switch into it. */
765
- readonly resumable: boolean
766
- }
767
-
768
770
  /**
769
771
  * The /search panel: full-text search over every persisted session through
770
772
  * the in-process session-query engine (the same corpus the model's
@@ -1092,13 +1094,15 @@ interface AgentsEntry {
1092
1094
  * Enter/t opening the child's full transcript in the shared read-only
1093
1095
  * document view (the same projection the exporter uses).
1094
1096
  */
1095
- export function AgentsPanel({ live, load, readTranscript, close }: {
1097
+ export function AgentsPanel({ live, load, readTranscript, attach, close }: {
1096
1098
  /** Live feed rows (child sessions observed this process). */
1097
1099
  live: readonly SubagentRow[]
1098
1100
  /** Load this session's persisted child sessions by lineage. */
1099
1101
  load: () => Promise<readonly SessionRow[]>
1100
1102
  /** Read one child session's full transcript as markdown. */
1101
1103
  readTranscript: (id: string, signal?: AbortSignal) => Promise<string>
1104
+ /** Attach to one child as the whole view (the runner's live buses). */
1105
+ attach: ((id: string, label: string) => void) | undefined
1102
1106
  close: () => void
1103
1107
  }): ReactElement {
1104
1108
  const [dirRows, setDirRows] = useState<readonly SessionRow[] | undefined>(undefined)
@@ -1145,22 +1149,26 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
1145
1149
  return [...feedRows, ...persisted]
1146
1150
  }, [live, dirRows])
1147
1151
  useEffect(() => setCursor(value => Math.min(value, Math.max(0, rows.length - 1))), [rows.length])
1148
- const openTranscript = (): void => {
1149
- const row = rows[cursor]
1150
- if (row === undefined) return
1152
+ /** (Re-)read one child's transcript; the id is captured, not re-derived. */
1153
+ const readTranscriptOf = (id: string): void => {
1151
1154
  transcriptLoad.current?.abort()
1152
- setTranscript({ id: row.id })
1155
+ setTranscript({ id })
1153
1156
  const controller = new AbortController()
1154
1157
  transcriptLoad.current = controller
1155
- Promise.resolve().then(() => readTranscript(row.id, controller.signal)).then(
1158
+ Promise.resolve().then(() => readTranscript(id, controller.signal)).then(
1156
1159
  text => {
1157
- if (!controller.signal.aborted) setTranscript({ id: row.id, text })
1160
+ if (!controller.signal.aborted) setTranscript({ id, text })
1158
1161
  },
1159
1162
  reason => {
1160
- if (!controller.signal.aborted) setTranscript({ id: row.id, error: reason instanceof Error ? reason.message : String(reason) })
1163
+ if (!controller.signal.aborted) setTranscript({ id, error: reason instanceof Error ? reason.message : String(reason) })
1161
1164
  },
1162
1165
  )
1163
1166
  }
1167
+ const openTranscript = (): void => {
1168
+ const row = rows[cursor]
1169
+ if (row === undefined) return
1170
+ readTranscriptOf(row.id)
1171
+ }
1164
1172
  useInput((input, key) => {
1165
1173
  if (key.escape || input === 'q') return close()
1166
1174
  if (input === 'r') return refresh()
@@ -1177,6 +1185,17 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
1177
1185
  transcriptLoad.current?.abort()
1178
1186
  setTranscript(undefined)
1179
1187
  },
1188
+ // Running subagents keep moving: `r` re-reads the same child.
1189
+ onRefresh: () => readTranscriptOf(transcript.id),
1190
+ // Enter/f in the preview attaches the child as the whole view when the
1191
+ // runner wired the live buses; without them the bordered expansion
1192
+ // keeps serving the same document.
1193
+ ...(attach === undefined ? {} : {
1194
+ onExpand: () => {
1195
+ const label = rows.find(row => row.id === transcript.id)?.label ?? transcript.id.slice(-12)
1196
+ attach(transcript.id, label)
1197
+ },
1198
+ }),
1180
1199
  })
1181
1200
  }
1182
1201
  return createElement(ListFrame, {
@@ -6,10 +6,10 @@
6
6
 
7
7
  import { createElement, useState, type ReactElement } from 'react'
8
8
  import { Box, Text, useInput, useStdout } from 'ink'
9
- import { panelViewport } from './render/inspector.ts'
10
- import { truncateColumns } from './render/text.ts'
11
- import { LANGUAGES, t, type LanguageName } from './i18n.ts'
12
- import { getPalette, inkColor } from './theme.ts'
9
+ import { panelViewport } from '../render/inspector.ts'
10
+ import { truncateColumns } from '../render/text.ts'
11
+ import { LANGUAGES, t, type LanguageName } from '../i18n.ts'
12
+ import { getPalette, inkColor } from '../theme.ts'
13
13
 
14
14
  export function LanguagePanel({ current, select, close }: {
15
15
  current: LanguageName