@xp266/dshtui 0.1.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 (159) hide show
  1. package/bin/dshtui.js +65 -0
  2. package/cordis.patch.yml +81 -0
  3. package/lib/contract/index.d.mts +2 -0
  4. package/lib/contract/index.mjs +5 -0
  5. package/lib/dialog.d.mts +156 -0
  6. package/lib/dialog.mjs +3 -0
  7. package/lib/index-DqnaSXqP.d.mts +823 -0
  8. package/lib/index.d.mts +17 -0
  9. package/lib/index.mjs +11404 -0
  10. package/lib/prod-react-Dzof4qJx.mjs +4 -0
  11. package/lib/surface-DdxzIgIY.mjs +3126 -0
  12. package/lib/vendor.d.mts +3 -0
  13. package/lib/vendor.mjs +4 -0
  14. package/package.json +128 -0
  15. package/src/apply-theme.ts +8 -0
  16. package/src/boot-log.ts +35 -0
  17. package/src/chat/blocks.ts +41 -0
  18. package/src/chat/bridge.ts +1076 -0
  19. package/src/chat/builtin-tool-views.ts +53 -0
  20. package/src/chat/chat-face.ts +16 -0
  21. package/src/chat/chat-nodes.ts +53 -0
  22. package/src/chat/efforts.ts +4 -0
  23. package/src/chat/interactions.ts +329 -0
  24. package/src/chat/models.ts +263 -0
  25. package/src/chat/partial-json.ts +125 -0
  26. package/src/chat/presets.ts +26 -0
  27. package/src/chat/question-view.ts +106 -0
  28. package/src/chat/retry-status.ts +33 -0
  29. package/src/chat/session-groups.ts +34 -0
  30. package/src/chat/session-list.ts +311 -0
  31. package/src/chat/store.ts +636 -0
  32. package/src/chat/todo-view.ts +66 -0
  33. package/src/chat/tool-view.ts +311 -0
  34. package/src/chat/tool-views.ts +114 -0
  35. package/src/contract/index.ts +704 -0
  36. package/src/contract/upstream.ts +165 -0
  37. package/src/core/caret-nonce.ts +19 -0
  38. package/src/core/composer-layout.ts +65 -0
  39. package/src/core/edit.ts +152 -0
  40. package/src/core/field-view.ts +52 -0
  41. package/src/core/fields.ts +185 -0
  42. package/src/core/metrics.ts +26 -0
  43. package/src/core/paste.ts +149 -0
  44. package/src/core/segments.ts +112 -0
  45. package/src/core/text.ts +290 -0
  46. package/src/dialog.ts +11 -0
  47. package/src/env.ts +38 -0
  48. package/src/harness-home.ts +22 -0
  49. package/src/hot-theme.ts +56 -0
  50. package/src/index.tsx +185 -0
  51. package/src/kernel/registry.ts +102 -0
  52. package/src/kernel/surface.ts +33 -0
  53. package/src/log.ts +249 -0
  54. package/src/model/message.ts +89 -0
  55. package/src/model/selection.ts +63 -0
  56. package/src/performance-guard.ts +28 -0
  57. package/src/runtime/prod-react.ts +10 -0
  58. package/src/terminal/background.ts +66 -0
  59. package/src/terminal/capabilities.ts +54 -0
  60. package/src/terminal/clipboard-backends.ts +69 -0
  61. package/src/terminal/clipboard.ts +161 -0
  62. package/src/terminal/cursor-shape.ts +13 -0
  63. package/src/terminal/glyphs.ts +44 -0
  64. package/src/terminal/mouse.ts +154 -0
  65. package/src/terminal/probe.ts +61 -0
  66. package/src/terminal/screen.ts +349 -0
  67. package/src/theme-settings.ts +31 -0
  68. package/src/theme.ts +319 -0
  69. package/src/ui/app.tsx +454 -0
  70. package/src/ui/chrome/caps.tsx +9 -0
  71. package/src/ui/chrome/hint-service.ts +87 -0
  72. package/src/ui/chrome/input-status.ts +27 -0
  73. package/src/ui/chrome/key-gate.tsx +29 -0
  74. package/src/ui/chrome/status-bar.tsx +147 -0
  75. package/src/ui/contributions.ts +47 -0
  76. package/src/ui/dialog/defaults-dialog.tsx +152 -0
  77. package/src/ui/dialog/dialog-item.tsx +38 -0
  78. package/src/ui/dialog/dialog.tsx +386 -0
  79. package/src/ui/dialog/effort-dialog.tsx +45 -0
  80. package/src/ui/dialog/geometry.ts +131 -0
  81. package/src/ui/dialog/items.ts +124 -0
  82. package/src/ui/dialog/list-dialog.tsx +63 -0
  83. package/src/ui/dialog/models-dialog.tsx +203 -0
  84. package/src/ui/dialog/presets-dialog.tsx +46 -0
  85. package/src/ui/dialog/providers-dialog.tsx +313 -0
  86. package/src/ui/dialog/sessions-dialog.tsx +163 -0
  87. package/src/ui/dialog/sizes.ts +7 -0
  88. package/src/ui/dialog/status-lines.ts +11 -0
  89. package/src/ui/dialog/todo-dialog.tsx +29 -0
  90. package/src/ui/dialog/use-dialog-input.ts +186 -0
  91. package/src/ui/extension-point.ts +188 -0
  92. package/src/ui/home-logo.ts +132 -0
  93. package/src/ui/hooks/use-async-action.ts +25 -0
  94. package/src/ui/hooks/use-async-list.ts +71 -0
  95. package/src/ui/hooks/use-caret.ts +24 -0
  96. package/src/ui/hooks/use-chat-events.ts +153 -0
  97. package/src/ui/hooks/use-mouse-selection.ts +352 -0
  98. package/src/ui/hooks/use-scroll.ts +49 -0
  99. package/src/ui/hooks/use-terminal-size.ts +20 -0
  100. package/src/ui/input/commands.ts +118 -0
  101. package/src/ui/input/composer-bus.ts +14 -0
  102. package/src/ui/input/composer-fields.ts +111 -0
  103. package/src/ui/input/composer-keys.ts +21 -0
  104. package/src/ui/input/composer-paste.ts +20 -0
  105. package/src/ui/input/input-bar.tsx +279 -0
  106. package/src/ui/input/use-composer.ts +495 -0
  107. package/src/ui/key-arbiter.ts +37 -0
  108. package/src/ui/keymap.ts +15 -0
  109. package/src/ui/layout-service.ts +77 -0
  110. package/src/ui/message/layout.ts +763 -0
  111. package/src/ui/message/md/block.ts +247 -0
  112. package/src/ui/message/md/engine.ts +430 -0
  113. package/src/ui/message/md/extensions.ts +58 -0
  114. package/src/ui/message/md/highlight.ts +626 -0
  115. package/src/ui/message/md/index.ts +14 -0
  116. package/src/ui/message/md/inline.ts +97 -0
  117. package/src/ui/message/md/palette.ts +87 -0
  118. package/src/ui/message/message-list.tsx +69 -0
  119. package/src/ui/message/message-row.tsx +137 -0
  120. package/src/ui/message/message-views.ts +16 -0
  121. package/src/ui/message/renderers.ts +39 -0
  122. package/src/ui/message/tool-diff.ts +188 -0
  123. package/src/ui/message/warmup.ts +41 -0
  124. package/src/ui/message/wave.ts +98 -0
  125. package/src/ui/overlay.ts +22 -0
  126. package/src/ui/panels/approval-panel.tsx +193 -0
  127. package/src/ui/panels/exports.ts +3 -0
  128. package/src/ui/panels/question-model.ts +424 -0
  129. package/src/ui/panels/question-panel.tsx +154 -0
  130. package/src/ui/panels/surface.tsx +106 -0
  131. package/src/ui/panels-builtin.tsx +86 -0
  132. package/src/ui/pointer/builtins.ts +281 -0
  133. package/src/ui/pointer/registry.ts +25 -0
  134. package/src/ui/region.tsx +31 -0
  135. package/src/ui/selection/service.ts +92 -0
  136. package/src/ui/selection-registry.ts +88 -0
  137. package/src/ui/selection.tsx +148 -0
  138. package/src/ui/spinner-tick.tsx +24 -0
  139. package/src/ui/use-available-windows.ts +31 -0
  140. package/src/ui/widgets/actions.tsx +84 -0
  141. package/src/ui/widgets/button.tsx +57 -0
  142. package/src/ui/widgets/checkbox.tsx +58 -0
  143. package/src/ui/widgets/header.tsx +36 -0
  144. package/src/ui/widgets/index.ts +8 -0
  145. package/src/ui/widgets/input.tsx +76 -0
  146. package/src/ui/widgets/registry.ts +32 -0
  147. package/src/ui/widgets/search.tsx +59 -0
  148. package/src/ui/widgets/select.tsx +97 -0
  149. package/src/ui/widgets/static.tsx +37 -0
  150. package/src/ui/widgets/types.ts +1 -0
  151. package/src/ui/window-services-bridge.ts +71 -0
  152. package/src/ui/window-services.ts +20 -0
  153. package/src/ui/windows/models-window.tsx +15 -0
  154. package/src/ui/windows/providers-window.tsx +14 -0
  155. package/src/ui/windows/service-window.tsx +18 -0
  156. package/src/ui/windows/sessions-window.tsx +25 -0
  157. package/src/ui/windows-builtin.tsx +94 -0
  158. package/src/ui/windows.ts +18 -0
  159. package/src/vendor.ts +12 -0
@@ -0,0 +1,147 @@
1
+ import { Box } from 'ink'
2
+ import type { ChatBridge } from '../../chat/bridge.ts'
3
+ import type { TokenStats } from '../../chat/bridge.ts'
4
+ import type { AgentActivity, AgentPhase } from '../../chat/store.ts'
5
+ import type { ActivePanel } from '../../chat/interactions.ts'
6
+ import type { RetryStatus } from '../../chat/retry-status.ts'
7
+ import { COLORS } from '../../theme.ts'
8
+ import { CHROME_MARGIN_X, CHROME_TEXT_X } from '../../core/metrics.ts'
9
+ import { textWidth, truncate } from '../../core/text.ts'
10
+ import { useStatusLineTexts } from '../contributions.ts'
11
+ import { glyphs } from '../../terminal/glyphs.ts'
12
+ import { Region } from '../region.tsx'
13
+ import { SelectableText } from '../selection.tsx'
14
+ import { useSpinnerTick } from '../spinner-tick.tsx'
15
+
16
+ const WORKING_HINT = 'Press esc to interrupt'
17
+ const WORKING_ARMED_HINT = 'Press esc again to interrupt'
18
+ const CHARS_PER_TOKEN = 4
19
+
20
+ const WORKING_PHASE_LABEL: Record<AgentPhase, string> = {
21
+ 'awaiting-request': 'Awaiting request',
22
+ thinking: 'Thinking',
23
+ working: 'Working',
24
+ }
25
+
26
+ interface StatusBarProps {
27
+ bridge: ChatBridge
28
+ columns: number
29
+ top: number
30
+ busy: boolean
31
+ running: boolean
32
+ todoBadge?: { current: number; total: number }
33
+ activity: AgentActivity
34
+ panel: ActivePanel | null
35
+ retryStatus?: RetryStatus
36
+ escArmed: boolean
37
+ streamedChars: number
38
+ }
39
+
40
+ export function StatusBar({
41
+ bridge,
42
+ columns,
43
+ top,
44
+ busy,
45
+ running,
46
+ todoBadge,
47
+ activity,
48
+ panel,
49
+ retryStatus,
50
+ escArmed,
51
+ streamedChars,
52
+ }: StatusBarProps) {
53
+ // Subscribing keeps the retry countdown (Date.now()-derived) fresh per tick.
54
+ useSpinnerTick()
55
+ const badge = running && todoBadge !== undefined ? `[Task ${todoBadge.current}/${todoBadge.total}] ` : ''
56
+ const leftText = busy ? `${badge}${agentStatusLabel(activity, panel, retryStatus)}` : cwdLabel(bridge)
57
+ const extras = useStatusLineTexts(columns).join(` ${glyphs.separator} `)
58
+ const extraCol = CHROME_TEXT_X + textWidth(leftText) + 2
59
+ const hint = busy ? (escArmed ? WORKING_ARMED_HINT : WORKING_HINT) : undefined
60
+ const hintCol = extraCol + (extras === '' ? 0 : textWidth(extras) + 2)
61
+ const leftWidth = textWidth(leftText)
62
+ + (extras === '' ? 0 : 2 + textWidth(extras))
63
+ + (hint === undefined ? 0 : 2 + textWidth(hint))
64
+ const avail = columns - CHROME_MARGIN_X * 2 - CHROME_TEXT_X
65
+ const stats = truncate(statsText(bridge.tokenStats(), streamedChars), Math.max(1, avail - leftWidth - 2))
66
+ const rightCol = Math.max(hintCol + (hint === undefined ? 0 : textWidth(hint)), columns - CHROME_MARGIN_X * 2 - textWidth(stats))
67
+ return (
68
+ <Box position="absolute" top={top} left={0} width={columns} height={1}>
69
+ <Region y={top}>
70
+ <Box position="absolute" top={0} left={CHROME_MARGIN_X} width={columns - CHROME_MARGIN_X}>
71
+ {busy && <SpinnerGlyph />}
72
+ </Box>
73
+ <Box position="absolute" top={0} left={CHROME_TEXT_X} width={columns - CHROME_TEXT_X}>
74
+ <SelectableText
75
+ y={0}
76
+ col={CHROME_TEXT_X}
77
+ text={leftText}
78
+ color={busy ? COLORS.workspaceWriteText : COLORS.cwdText}
79
+ />
80
+ </Box>
81
+ {extras !== '' && (
82
+ <Box position="absolute" top={0} left={extraCol} width={Math.max(1, columns - extraCol)}>
83
+ <SelectableText y={0} col={extraCol} text={extras} color={COLORS.dialogHintText} />
84
+ </Box>
85
+ )}
86
+ {hint !== undefined && (
87
+ <Box position="absolute" top={0} left={hintCol} width={Math.max(1, columns - hintCol)}>
88
+ <SelectableText y={0} col={hintCol} text={hint} color={COLORS.dialogHintText} />
89
+ </Box>
90
+ )}
91
+ <Box position="absolute" top={0} left={rightCol} width={Math.max(1, columns - rightCol)}>
92
+ <SelectableText y={0} col={rightCol} text={stats} color={COLORS.statsText} />
93
+ </Box>
94
+ </Region>
95
+ </Box>
96
+ )
97
+ }
98
+
99
+ function SpinnerGlyph() {
100
+ const tick = useSpinnerTick()
101
+ return (
102
+ <SelectableText
103
+ y={0}
104
+ col={CHROME_MARGIN_X}
105
+ text={`${glyphs.spinnerFrames[tick % glyphs.spinnerFrames.length]} `}
106
+ color={COLORS.workspaceWriteText}
107
+ />
108
+ )
109
+ }
110
+
111
+ function agentStatusLabel(activity: AgentActivity, panel: ActivePanel | null, retryStatus?: RetryStatus): string {
112
+ if (panel?.kind === 'approval') return 'Waiting for permission'
113
+ if (panel?.kind === 'question') return 'Waiting for selection'
114
+ if (activity.compacting) return 'Compacting'
115
+ if (retryStatus !== undefined) {
116
+ const remain = retryStatus.untilTs === 0
117
+ ? undefined
118
+ : Math.max(0, Math.ceil((retryStatus.untilTs - Date.now()) / 1000))
119
+ const progress = `(${retryStatus.attempt}/${retryStatus.maxRetries} ${glyphs.separator} ${retryStatus.code})`
120
+ return remain === undefined ? `Retrying${glyphs.ellipsis} ${progress}` : `Retrying in ${remain}s ${progress}`
121
+ }
122
+ return WORKING_PHASE_LABEL[activity.phase]
123
+ }
124
+
125
+ function cwdLabel(bridge: ChatBridge | undefined): string {
126
+ const home = process.env.HOME ?? ''
127
+ const cwd = bridge?.cwd() ?? process.cwd()
128
+ return home !== '' && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd
129
+ }
130
+
131
+ function statsText(stats: TokenStats, streamedChars = 0): string {
132
+ const estimate = Math.ceil(streamedChars / CHARS_PER_TOKEN)
133
+ const contextPercent = estimate > 0 && stats.projectedTokens !== undefined && stats.contextWindow !== undefined
134
+ ? Math.min(100, Math.round((stats.projectedTokens + estimate) / stats.contextWindow * 100))
135
+ : stats.contextPercent
136
+ const context = `Context ${contextPercent}%`
137
+ const hit = `Hit ${stats.hitPercent}%`
138
+ const tokens = `${formatTokens(stats.input)} ${glyphs.tokenArrow} ${formatTokens(stats.output + estimate)}`
139
+ return `${context} | ${hit} | ${tokens}`
140
+ }
141
+
142
+ function formatTokens(n: number): string {
143
+ if (n < 1_000) return String(n)
144
+ const scaled = (v: number): string => (v >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10))
145
+ if (n < 1_000_000) return `${scaled(n / 1_000)}K`
146
+ return `${scaled(n / 1_000_000)}M`
147
+ }
@@ -0,0 +1,47 @@
1
+ import { useSyncExternalStore } from 'react'
2
+ import { keyedRegistry } from '../kernel/registry.ts'
3
+ import type { OverlayContribution, StatusLineContribution } from '../contract/index.ts'
4
+
5
+ export type { OverlayContribution, StatusLineContribution } from '../contract/index.ts'
6
+
7
+ export const statusLines = keyedRegistry<StatusLineContribution>()
8
+ export const overlays = keyedRegistry<OverlayContribution>()
9
+
10
+ export const registerStatusLine = (contribution: StatusLineContribution): (() => void) =>
11
+ statusLines.register(contribution.id, contribution, { order: contribution.order })
12
+
13
+ export const registerOverlay = (contribution: OverlayContribution): (() => void) =>
14
+ overlays.register(contribution.id, contribution)
15
+
16
+ let statusVersion = 0
17
+ statusLines.subscribe(() => {
18
+ statusVersion += 1
19
+ })
20
+ let statusCacheVersion = -1
21
+ let statusCacheColumns = -1
22
+ let statusCacheTexts: string[] = []
23
+
24
+ export function useStatusLineTexts(columns: number): string[] {
25
+ return useSyncExternalStore(
26
+ statusLines.subscribe,
27
+ () => {
28
+ if (statusCacheVersion !== statusVersion || statusCacheColumns !== columns) {
29
+ statusCacheVersion = statusVersion
30
+ statusCacheColumns = columns
31
+ statusCacheTexts = statusLines.values()
32
+ .map(contribution => contribution.render({ columns }).trim())
33
+ .filter(text => text !== '')
34
+ }
35
+ return statusCacheTexts
36
+ },
37
+ () => [],
38
+ )
39
+ }
40
+
41
+ export function useOverlayContribution(id: string | null): OverlayContribution | undefined {
42
+ return useSyncExternalStore(
43
+ overlays.subscribe,
44
+ () => (id === null ? undefined : overlays.get(id)),
45
+ () => undefined,
46
+ )
47
+ }
@@ -0,0 +1,152 @@
1
+ import { useRef, useState } from 'react'
2
+ import type { Ref } from 'react'
3
+ import { permissionModeInfo } from '../../theme.ts'
4
+ import type { ThemeMode } from '../../theme.ts'
5
+ import { errorLine, loadingLine } from './status-lines.ts'
6
+ import type { PresetSummary } from '../../chat/presets.ts'
7
+ import { useAsyncAction } from '../hooks/use-async-action.ts'
8
+ import { useAsyncList } from '../hooks/use-async-list.ts'
9
+ import { Dialog } from './dialog.tsx'
10
+ import { DIALOG_MAX_HEIGHT, DIALOG_WIDTH_MEDIUM } from './sizes.ts'
11
+ import type { DialogFooterLine, DialogHandle, DialogRow } from './dialog.tsx'
12
+
13
+ export interface DefaultsApi {
14
+ listPresets(): Promise<PresetSummary[]>
15
+ defaultPresetId(): string
16
+ setDefaultPreset(id: string): Promise<void>
17
+ listPermissionPresets(): Promise<string[]>
18
+ defaultPermission(): string
19
+ setDefaultPermission(id: string): Promise<void>
20
+ themeMode(): ThemeMode
21
+ setThemeMode(mode: ThemeMode): Promise<void>
22
+ }
23
+
24
+ export interface DefaultsDialogProps {
25
+ api: DefaultsApi
26
+ onClose: () => void
27
+ ref?: Ref<DialogHandle>
28
+ }
29
+
30
+
31
+ interface DefaultsData {
32
+ presets: PresetSummary[]
33
+ permissions: string[]
34
+ }
35
+
36
+ function useSavedChoice<T>(read: () => T, write: (value: T) => Promise<void>): { value: T; save(value: T): void; error: string | null } {
37
+ const [value, setValue] = useState(read)
38
+ const { error, clearError, run } = useAsyncAction()
39
+ const save = (next: T): void => {
40
+ setValue(next)
41
+ void run(() => write(next)).then(result => {
42
+ if (result.ok) clearError()
43
+ else setValue(read())
44
+ })
45
+ }
46
+ return { value, save, error }
47
+ }
48
+
49
+ export function DefaultsDialog({ api, onClose, ref }: DefaultsDialogProps) {
50
+ const { items, loading, error } = useAsyncList<DefaultsData>(async () => {
51
+ const [presetList, permissionList] = await Promise.all([api.listPresets(), api.listPermissionPresets()])
52
+ return [{ presets: presetList, permissions: permissionList }]
53
+ })
54
+ const presetChoice = useSavedChoice(() => api.defaultPresetId(), api.setDefaultPreset)
55
+ const permissionChoice = useSavedChoice(() => api.defaultPermission(), api.setDefaultPermission)
56
+ const themeChoice = useSavedChoice(() => api.themeMode(), api.setThemeMode)
57
+ const saveError = presetChoice.error ?? permissionChoice.error ?? themeChoice.error
58
+ const presets = items[0]?.presets ?? []
59
+ const permissionIds = items[0]?.permissions ?? []
60
+ const presetIds = presets.map(preset => preset.id)
61
+ const presetNames = presets.map(preset => preset.name)
62
+ const presetValue = presetNames[presetIds.indexOf(presetChoice.value)] ?? presetChoice.value
63
+ const permissionNames = permissionIds.map(id => permissionModeInfo(id).name)
64
+ const permissionValue = permissionNames[permissionIds.indexOf(permissionChoice.value)] ?? permissionChoice.value
65
+ const originalRef = useRef({ preset: api.defaultPresetId(), permission: api.defaultPermission(), theme: api.themeMode() })
66
+ const revertAndClose = async () => {
67
+ const original = originalRef.current
68
+ try {
69
+ if (presetChoice.value !== original.preset) await api.setDefaultPreset(original.preset)
70
+ if (permissionChoice.value !== original.permission) await api.setDefaultPermission(original.permission)
71
+ if (themeChoice.value !== original.theme) await api.setThemeMode(original.theme)
72
+ } catch {
73
+ // A failed revert still closes the dialog; the values were already applied live.
74
+ }
75
+ onClose()
76
+ }
77
+ const rows: DialogRow[] = [
78
+ {
79
+ items: [
80
+ {
81
+ type: 'select',
82
+ label: 'Agent Preset',
83
+ value: presetValue,
84
+ options: presetNames,
85
+ onChange: name => {
86
+ const id = presetIds[presetNames.indexOf(name)]
87
+ if (id !== undefined) presetChoice.save(id)
88
+ },
89
+ spaced: true,
90
+ },
91
+ ],
92
+ },
93
+ {
94
+ items: [
95
+ {
96
+ type: 'select',
97
+ label: 'Permission Mode',
98
+ value: permissionValue,
99
+ options: permissionNames,
100
+ onChange: name => {
101
+ const id = permissionIds[permissionNames.indexOf(name)]
102
+ if (id !== undefined) permissionChoice.save(id)
103
+ },
104
+ spaced: true,
105
+ },
106
+ ],
107
+ },
108
+ {
109
+ items: [
110
+ {
111
+ type: 'select',
112
+ label: 'Theme',
113
+ value: themeChoice.value === 'light' ? 'Light' : 'Dark',
114
+ options: ['Dark', 'Light'],
115
+ onChange: name => {
116
+ const mode: ThemeMode = name === 'Light' ? 'light' : 'dark'
117
+ if (mode !== themeChoice.value) themeChoice.save(mode)
118
+ },
119
+ spaced: true,
120
+ },
121
+ ],
122
+ },
123
+ {
124
+ items: [
125
+ {
126
+ type: 'actions',
127
+ confirmLabel: 'Submit',
128
+ cancelLabel: 'Cancel',
129
+ onConfirm: onClose,
130
+ onCancel: () => void revertAndClose(),
131
+ },
132
+ ],
133
+ },
134
+ ]
135
+ const footer: DialogFooterLine[] = loading ? [loadingLine()] : []
136
+ const errors: DialogFooterLine[] = [
137
+ ...(error !== null ? [errorLine(error)] : []),
138
+ ...(saveError !== null ? [errorLine(saveError)] : []),
139
+ ]
140
+ return (
141
+ <Dialog
142
+ ref={ref}
143
+ width={DIALOG_WIDTH_MEDIUM}
144
+ maxHeight={DIALOG_MAX_HEIGHT}
145
+ title="defaults"
146
+ rows={rows}
147
+ footer={footer}
148
+ errors={errors}
149
+ onClose={onClose}
150
+ />
151
+ )
152
+ }
@@ -0,0 +1,38 @@
1
+ import { Box, Text } from 'ink'
2
+ import type { ReactNode } from 'react'
3
+ import { textWidth } from '../../core/text.ts'
4
+ import { actionPositions, CAROUSEL_BUTTON_WIDTH, selectBlock } from './geometry.ts'
5
+ import { widgetOf } from '../widgets/registry.ts'
6
+ import '../widgets/index.ts'
7
+ import type { DialogRow } from './items.ts'
8
+
9
+ export { actionPositions, CAROUSEL_BUTTON_WIDTH, selectBlock }
10
+
11
+ export function renderRow(
12
+ row: DialogRow,
13
+ focused: boolean,
14
+ contentWidth: number,
15
+ baseY: number,
16
+ left: number,
17
+ pressed: 'left' | 'right' | null = null,
18
+ subCol = 0,
19
+ clip = 0,
20
+ cursor?: number,
21
+ ): ReactNode {
22
+ let col = left
23
+ return (
24
+ <Box flexDirection="row">
25
+ {row.items.map((item, index) => {
26
+ const def = widgetOf(item.type)
27
+ const next = def.render({ item, focused, width: contentWidth, y: baseY, x: col, pressed, subCol, clip, cursor: focused ? cursor : undefined })
28
+ col += def.paintWidth(item) + 1
29
+ return (
30
+ <Box key={index} flexDirection="row">
31
+ {index > 0 && <Text> </Text>}
32
+ {next}
33
+ </Box>
34
+ )
35
+ })}
36
+ </Box>
37
+ )
38
+ }