@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.
- package/bin/dshtui.js +65 -0
- package/cordis.patch.yml +81 -0
- package/lib/contract/index.d.mts +2 -0
- package/lib/contract/index.mjs +5 -0
- package/lib/dialog.d.mts +156 -0
- package/lib/dialog.mjs +3 -0
- package/lib/index-DqnaSXqP.d.mts +823 -0
- package/lib/index.d.mts +17 -0
- package/lib/index.mjs +11404 -0
- package/lib/prod-react-Dzof4qJx.mjs +4 -0
- package/lib/surface-DdxzIgIY.mjs +3126 -0
- package/lib/vendor.d.mts +3 -0
- package/lib/vendor.mjs +4 -0
- package/package.json +128 -0
- package/src/apply-theme.ts +8 -0
- package/src/boot-log.ts +35 -0
- package/src/chat/blocks.ts +41 -0
- package/src/chat/bridge.ts +1076 -0
- package/src/chat/builtin-tool-views.ts +53 -0
- package/src/chat/chat-face.ts +16 -0
- package/src/chat/chat-nodes.ts +53 -0
- package/src/chat/efforts.ts +4 -0
- package/src/chat/interactions.ts +329 -0
- package/src/chat/models.ts +263 -0
- package/src/chat/partial-json.ts +125 -0
- package/src/chat/presets.ts +26 -0
- package/src/chat/question-view.ts +106 -0
- package/src/chat/retry-status.ts +33 -0
- package/src/chat/session-groups.ts +34 -0
- package/src/chat/session-list.ts +311 -0
- package/src/chat/store.ts +636 -0
- package/src/chat/todo-view.ts +66 -0
- package/src/chat/tool-view.ts +311 -0
- package/src/chat/tool-views.ts +114 -0
- package/src/contract/index.ts +704 -0
- package/src/contract/upstream.ts +165 -0
- package/src/core/caret-nonce.ts +19 -0
- package/src/core/composer-layout.ts +65 -0
- package/src/core/edit.ts +152 -0
- package/src/core/field-view.ts +52 -0
- package/src/core/fields.ts +185 -0
- package/src/core/metrics.ts +26 -0
- package/src/core/paste.ts +149 -0
- package/src/core/segments.ts +112 -0
- package/src/core/text.ts +290 -0
- package/src/dialog.ts +11 -0
- package/src/env.ts +38 -0
- package/src/harness-home.ts +22 -0
- package/src/hot-theme.ts +56 -0
- package/src/index.tsx +185 -0
- package/src/kernel/registry.ts +102 -0
- package/src/kernel/surface.ts +33 -0
- package/src/log.ts +249 -0
- package/src/model/message.ts +89 -0
- package/src/model/selection.ts +63 -0
- package/src/performance-guard.ts +28 -0
- package/src/runtime/prod-react.ts +10 -0
- package/src/terminal/background.ts +66 -0
- package/src/terminal/capabilities.ts +54 -0
- package/src/terminal/clipboard-backends.ts +69 -0
- package/src/terminal/clipboard.ts +161 -0
- package/src/terminal/cursor-shape.ts +13 -0
- package/src/terminal/glyphs.ts +44 -0
- package/src/terminal/mouse.ts +154 -0
- package/src/terminal/probe.ts +61 -0
- package/src/terminal/screen.ts +349 -0
- package/src/theme-settings.ts +31 -0
- package/src/theme.ts +319 -0
- package/src/ui/app.tsx +454 -0
- package/src/ui/chrome/caps.tsx +9 -0
- package/src/ui/chrome/hint-service.ts +87 -0
- package/src/ui/chrome/input-status.ts +27 -0
- package/src/ui/chrome/key-gate.tsx +29 -0
- package/src/ui/chrome/status-bar.tsx +147 -0
- package/src/ui/contributions.ts +47 -0
- package/src/ui/dialog/defaults-dialog.tsx +152 -0
- package/src/ui/dialog/dialog-item.tsx +38 -0
- package/src/ui/dialog/dialog.tsx +386 -0
- package/src/ui/dialog/effort-dialog.tsx +45 -0
- package/src/ui/dialog/geometry.ts +131 -0
- package/src/ui/dialog/items.ts +124 -0
- package/src/ui/dialog/list-dialog.tsx +63 -0
- package/src/ui/dialog/models-dialog.tsx +203 -0
- package/src/ui/dialog/presets-dialog.tsx +46 -0
- package/src/ui/dialog/providers-dialog.tsx +313 -0
- package/src/ui/dialog/sessions-dialog.tsx +163 -0
- package/src/ui/dialog/sizes.ts +7 -0
- package/src/ui/dialog/status-lines.ts +11 -0
- package/src/ui/dialog/todo-dialog.tsx +29 -0
- package/src/ui/dialog/use-dialog-input.ts +186 -0
- package/src/ui/extension-point.ts +188 -0
- package/src/ui/home-logo.ts +132 -0
- package/src/ui/hooks/use-async-action.ts +25 -0
- package/src/ui/hooks/use-async-list.ts +71 -0
- package/src/ui/hooks/use-caret.ts +24 -0
- package/src/ui/hooks/use-chat-events.ts +153 -0
- package/src/ui/hooks/use-mouse-selection.ts +352 -0
- package/src/ui/hooks/use-scroll.ts +49 -0
- package/src/ui/hooks/use-terminal-size.ts +20 -0
- package/src/ui/input/commands.ts +118 -0
- package/src/ui/input/composer-bus.ts +14 -0
- package/src/ui/input/composer-fields.ts +111 -0
- package/src/ui/input/composer-keys.ts +21 -0
- package/src/ui/input/composer-paste.ts +20 -0
- package/src/ui/input/input-bar.tsx +279 -0
- package/src/ui/input/use-composer.ts +495 -0
- package/src/ui/key-arbiter.ts +37 -0
- package/src/ui/keymap.ts +15 -0
- package/src/ui/layout-service.ts +77 -0
- package/src/ui/message/layout.ts +763 -0
- package/src/ui/message/md/block.ts +247 -0
- package/src/ui/message/md/engine.ts +430 -0
- package/src/ui/message/md/extensions.ts +58 -0
- package/src/ui/message/md/highlight.ts +626 -0
- package/src/ui/message/md/index.ts +14 -0
- package/src/ui/message/md/inline.ts +97 -0
- package/src/ui/message/md/palette.ts +87 -0
- package/src/ui/message/message-list.tsx +69 -0
- package/src/ui/message/message-row.tsx +137 -0
- package/src/ui/message/message-views.ts +16 -0
- package/src/ui/message/renderers.ts +39 -0
- package/src/ui/message/tool-diff.ts +188 -0
- package/src/ui/message/warmup.ts +41 -0
- package/src/ui/message/wave.ts +98 -0
- package/src/ui/overlay.ts +22 -0
- package/src/ui/panels/approval-panel.tsx +193 -0
- package/src/ui/panels/exports.ts +3 -0
- package/src/ui/panels/question-model.ts +424 -0
- package/src/ui/panels/question-panel.tsx +154 -0
- package/src/ui/panels/surface.tsx +106 -0
- package/src/ui/panels-builtin.tsx +86 -0
- package/src/ui/pointer/builtins.ts +281 -0
- package/src/ui/pointer/registry.ts +25 -0
- package/src/ui/region.tsx +31 -0
- package/src/ui/selection/service.ts +92 -0
- package/src/ui/selection-registry.ts +88 -0
- package/src/ui/selection.tsx +148 -0
- package/src/ui/spinner-tick.tsx +24 -0
- package/src/ui/use-available-windows.ts +31 -0
- package/src/ui/widgets/actions.tsx +84 -0
- package/src/ui/widgets/button.tsx +57 -0
- package/src/ui/widgets/checkbox.tsx +58 -0
- package/src/ui/widgets/header.tsx +36 -0
- package/src/ui/widgets/index.ts +8 -0
- package/src/ui/widgets/input.tsx +76 -0
- package/src/ui/widgets/registry.ts +32 -0
- package/src/ui/widgets/search.tsx +59 -0
- package/src/ui/widgets/select.tsx +97 -0
- package/src/ui/widgets/static.tsx +37 -0
- package/src/ui/widgets/types.ts +1 -0
- package/src/ui/window-services-bridge.ts +71 -0
- package/src/ui/window-services.ts +20 -0
- package/src/ui/windows/models-window.tsx +15 -0
- package/src/ui/windows/providers-window.tsx +14 -0
- package/src/ui/windows/service-window.tsx +18 -0
- package/src/ui/windows/sessions-window.tsx +25 -0
- package/src/ui/windows-builtin.tsx +94 -0
- package/src/ui/windows.ts +18 -0
- 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
|
+
}
|