@brimveyn/aimux 1.21.0 → 1.22.2
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.md +27 -25
- package/package.json +2 -2
- package/src/app-runtime/auto-commit-driver.ts +22 -27
- package/src/app-runtime/backend-attach-runtime.ts +43 -36
- package/src/app-runtime/backend-runtime-events.ts +107 -88
- package/src/app-runtime/editor-actions.ts +201 -0
- package/src/app-runtime/git-actions.ts +197 -0
- package/src/app-runtime/measure-ref.ts +19 -0
- package/src/app-runtime/navigation-actions.ts +213 -0
- package/src/app-runtime/project-actions.ts +188 -0
- package/src/app-runtime/prompt-injection.ts +123 -0
- package/src/app-runtime/selection.ts +29 -0
- package/src/app-runtime/settings-actions.ts +156 -0
- package/src/app-runtime/setup-actions.ts +237 -0
- package/src/app-runtime/side-effect-context.ts +33 -0
- package/src/app-runtime/side-effects.ts +302 -1541
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/tab-actions.ts +218 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
- package/src/app-runtime/use-auto-commit-driver.ts +14 -13
- package/src/app-runtime/use-backend-runtime.ts +12 -11
- package/src/app-runtime/use-directory-search.ts +5 -4
- package/src/app-runtime/use-mouse-handlers.ts +2 -1
- package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app-runtime/use-setup-runner.ts +96 -0
- package/src/app-runtime/use-terminal-resize.ts +18 -14
- package/src/app-runtime/workspace-actions.ts +366 -0
- package/src/app-runtime/workspace-activity.ts +138 -0
- package/src/app-runtime/workspace-naming.ts +90 -0
- package/src/app.tsx +93 -48
- package/src/assets/sounds/bell.wav +0 -0
- package/src/assets/sounds/ding.wav +0 -0
- package/src/assets/sounds/plane.wav +0 -0
- package/src/cli/client/daemon-client.ts +2 -2
- package/src/cli/client/project-resolver.ts +112 -0
- package/src/cli/commands/project/close.ts +32 -0
- package/src/cli/commands/project/create.ts +93 -0
- package/src/cli/commands/project/list.ts +25 -0
- package/src/cli/commands/project/show.ts +32 -0
- package/src/cli/commands/{workspace → project}/switch.ts +19 -19
- package/src/cli/commands/tab/await.ts +2 -2
- package/src/cli/commands/tab/close.ts +3 -3
- package/src/cli/commands/tab/create.ts +74 -73
- package/src/cli/commands/tab/focus.ts +3 -3
- package/src/cli/commands/tab/list.ts +6 -6
- package/src/cli/commands/tab/run.ts +2 -2
- package/src/cli/commands/tab/send.ts +2 -2
- package/src/cli/commands/tab/snapshot.ts +2 -2
- package/src/cli/commands/tab/tail.ts +2 -2
- package/src/cli/commands/tab/wait.ts +2 -2
- package/src/cli/commands/worker/await.ts +3 -3
- package/src/cli/commands/worker/doctor.ts +30 -30
- package/src/cli/commands/worker/list.ts +15 -15
- package/src/cli/commands/worker/prompt.ts +3 -3
- package/src/cli/commands/worker/run.ts +23 -23
- package/src/cli/commands/worker/shared.ts +53 -59
- package/src/cli/commands/worker/stop.ts +29 -29
- package/src/cli/commands/worker/submit.ts +3 -3
- package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
- package/src/cli/commands/workspace/create.ts +30 -68
- package/src/cli/commands/workspace/list.ts +52 -9
- package/src/cli/commands/workspace/remove.ts +81 -0
- package/src/cli/completion/plan.ts +2 -2
- package/src/cli/completion/sources.ts +18 -18
- package/src/cli/context.ts +13 -13
- package/src/cli/flags.ts +30 -10
- package/src/cli/index.ts +26 -16
- package/src/cli/output.ts +1 -1
- package/src/cli/registry.ts +12 -12
- package/src/config/loader.ts +16 -5
- package/src/config.ts +70 -112
- package/src/daemon/catalog-writer.ts +67 -67
- package/src/daemon/daemon.ts +205 -201
- package/src/daemon/session-manager.ts +39 -39
- package/src/daemon/session-registry.ts +14 -14
- package/src/git/divergence.ts +33 -2
- package/src/git/git-poller.ts +1 -1
- package/src/git/git-status.ts +2 -2
- package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
- package/src/git/pr-status.ts +1 -1
- package/src/git/repo-discovery.ts +1 -1
- package/src/git/use-repo-discovery.ts +1 -1
- package/src/git/workspace-branch-poller.ts +54 -0
- package/src/git/workspace-divergence-poller.ts +65 -0
- package/src/git/worktree.ts +29 -0
- package/src/index.tsx +4 -4
- package/src/input/keymap/help-entries.ts +7 -5
- package/src/input/modes/bridge.ts +13 -12
- package/src/input/modes/handlers/shared.ts +1 -1
- package/src/input/modes/transitions.ts +34 -21
- package/src/input/modes/types.ts +48 -31
- package/src/ipc/manager-protocol.ts +44 -44
- package/src/ipc/protocol.ts +192 -166
- package/src/platform/play-sound.ts +184 -0
- package/src/platform/project-search.ts +8 -8
- package/src/platform/worktree-paths.ts +8 -6
- package/src/pty/assistant-question-extractor.ts +1 -1
- package/src/pty/assistant-status-detection-loop.ts +79 -55
- package/src/pty/assistant-status-detector.ts +40 -15
- package/src/pty/command-registry.ts +81 -1
- package/src/restart-terminal-manager.ts +1 -1
- package/src/services/ai-usage/provider.ts +9 -2
- package/src/services/ai-usage/spawn.ts +19 -6
- package/src/session-backend/bootstrap.ts +4 -4
- package/src/session-backend/local-session-backend.ts +78 -75
- package/src/session-backend/remote-session-backend.ts +64 -52
- package/src/session-backend/types.ts +43 -32
- package/src/settings/live.ts +90 -0
- package/src/settings/search.ts +43 -0
- package/src/settings/sections/about.ts +49 -0
- package/src/settings/sections/appearance.ts +54 -0
- package/src/settings/sections/automation.ts +105 -0
- package/src/settings/sections/commands.ts +72 -0
- package/src/settings/sections/editor.ts +56 -0
- package/src/settings/sections/experimental.ts +57 -0
- package/src/settings/sections/git.ts +112 -0
- package/src/settings/sections/index.ts +95 -0
- package/src/settings/sections/integrations.ts +23 -0
- package/src/settings/sections/layout.ts +72 -0
- package/src/settings/sections/notifications.ts +99 -0
- package/src/settings/sections/setup.ts +54 -0
- package/src/settings/sections/status-bar.ts +63 -0
- package/src/settings/settings-store.ts +207 -0
- package/src/settings/types.ts +108 -0
- package/src/snippets/run-shell-var.ts +48 -22
- package/src/state/actions.ts +342 -0
- package/src/state/app-store.ts +2 -1
- package/src/state/bars.ts +1 -1
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/layout-resize.ts +2 -2
- package/src/state/project-catalog.ts +275 -0
- package/src/state/project-data.ts +143 -0
- package/src/state/{session-persistence.ts → project-persistence.ts} +74 -68
- package/src/state/project-save.ts +46 -0
- package/src/state/project-workspaces.ts +392 -0
- package/src/state/reducers/auto-commit-state.ts +11 -10
- package/src/state/reducers/git-commit-modal-state.ts +122 -0
- package/src/state/reducers/git-mode-state.ts +1 -1
- package/src/state/reducers/git-panel-state.ts +10 -6
- package/src/state/reducers/modal-state.ts +290 -681
- package/src/state/reducers/multi-repo-state.ts +3 -6
- package/src/state/reducers/project-state.ts +312 -0
- package/src/state/reducers/settings-state.ts +80 -0
- package/src/state/reducers/tab-state.ts +107 -60
- package/src/state/reducers/ui-state.ts +4 -3
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +57 -24
- package/src/state/types.ts +244 -405
- package/src/state/validation.ts +14 -12
- package/src/terminal-manager/manager-client.ts +32 -32
- package/src/terminal-manager/terminal-manager.ts +24 -24
- package/src/ui/components/git/git-panel.tsx +2 -2
- package/src/ui/components/git/git-view.tsx +12 -12
- package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
- package/src/ui/components/git/pane/git-pane-widget.tsx +7 -7
- package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
- package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
- package/src/ui/components/layout/status-bar.tsx +6 -2
- package/src/ui/components/layout/terminal-pane.tsx +34 -29
- package/src/ui/components/layout/top-tab-bar.tsx +46 -36
- package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
- package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
- package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
- package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
- package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
- package/src/ui/components/modals/shared/form.tsx +34 -22
- package/src/ui/components/modals/shared/picker.tsx +10 -10
- package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
- package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
- package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
- package/src/ui/components/primitives/input-field.tsx +7 -3
- package/src/ui/components/primitives/surface.tsx +3 -0
- package/src/ui/components/settings/row-value.tsx +74 -0
- package/src/ui/components/settings/settings-row.tsx +78 -0
- package/src/ui/components/settings/settings-view.tsx +169 -0
- package/src/ui/components/setup/setup-widget.tsx +152 -0
- package/src/ui/flash/build-labels.ts +27 -27
- package/src/ui/hooks/use-activity-sprite.ts +73 -0
- package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
- package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
- package/src/ui/root.tsx +139 -121
- package/src/ui/status-bar-model.ts +48 -34
- package/src/ui/terminal-graphics/kitty.ts +28 -2
- package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
- package/src/ui/terminal-graphics/sprites.ts +246 -0
- package/src/ui/truncate.ts +7 -0
- package/src/ui/widgets/registry.tsx +6 -3
- package/src/ui/widgets/widget-context-menu.ts +26 -10
- package/src/update.ts +2 -2
- package/src/app-runtime/session-actions.ts +0 -187
- package/src/cli/client/workspace-resolver.ts +0 -110
- package/src/cli/commands/workspace/close.ts +0 -32
- package/src/cli/commands/workspace/show.ts +0 -32
- package/src/cli/commands/worktree/create.ts +0 -55
- package/src/cli/commands/worktree/list.ts +0 -68
- package/src/cli/commands/worktree/remove.ts +0 -81
- package/src/git/worktree-branch-poller.ts +0 -54
- package/src/git/worktree-divergence-poller.ts +0 -59
- package/src/state/reducers/session-state.ts +0 -262
- package/src/state/session-catalog.ts +0 -143
- package/src/state/session-worktrees.ts +0 -260
- package/src/state/workspace-save.ts +0 -43
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
- package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
- package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
- /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
|
@@ -6,15 +6,19 @@ interface InputFieldProps {
|
|
|
6
6
|
value: string
|
|
7
7
|
cursorPos?: number
|
|
8
8
|
placeholder?: string
|
|
9
|
+
/** Rows the box occupies even when empty, so a long answer looks expected. */
|
|
10
|
+
minLines?: number
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export function InputField({ active, cursorPos, placeholder, value }: InputFieldProps) {
|
|
13
|
+
export function InputField({ active, cursorPos, minLines, placeholder, value }: InputFieldProps) {
|
|
12
14
|
const t = useTheme()
|
|
13
15
|
const fg = active ? t.text : t.textMuted
|
|
16
|
+
// The surface pads one row top and bottom.
|
|
17
|
+
const minHeight = minLines != null ? minLines + 2 : undefined
|
|
14
18
|
if (!active) {
|
|
15
19
|
const showPlaceholder = !value && !!(placeholder != null && placeholder !== '')
|
|
16
20
|
return (
|
|
17
|
-
<Surface tone="input" padding={1}>
|
|
21
|
+
<Surface tone="input" padding={1} minHeight={minHeight}>
|
|
18
22
|
<text fg={showPlaceholder ? t.textMuted : fg}>{showPlaceholder ? placeholder : value}</text>
|
|
19
23
|
</Surface>
|
|
20
24
|
)
|
|
@@ -29,7 +33,7 @@ export function InputField({ active, cursorPos, placeholder, value }: InputField
|
|
|
29
33
|
const trailing = cursorOnLineEnd ? value.slice(safePos) : value.slice(safePos + 1)
|
|
30
34
|
|
|
31
35
|
return (
|
|
32
|
-
<Surface tone="inputActive" padding={1}>
|
|
36
|
+
<Surface tone="inputActive" padding={1} minHeight={minHeight}>
|
|
33
37
|
<text fg={fg}>
|
|
34
38
|
{before}
|
|
35
39
|
<span bg={t.text} fg={t.background}>
|
|
@@ -26,6 +26,7 @@ interface SurfaceProps {
|
|
|
26
26
|
children: ReactNode
|
|
27
27
|
flexDirection?: 'row' | 'column'
|
|
28
28
|
gap?: number
|
|
29
|
+
minHeight?: number
|
|
29
30
|
padding?: number
|
|
30
31
|
paddingLeft?: number
|
|
31
32
|
paddingRight?: number
|
|
@@ -39,6 +40,7 @@ export function Surface({
|
|
|
39
40
|
children,
|
|
40
41
|
flexDirection = 'column',
|
|
41
42
|
gap = 0,
|
|
43
|
+
minHeight,
|
|
42
44
|
padding,
|
|
43
45
|
paddingBottom,
|
|
44
46
|
paddingLeft,
|
|
@@ -53,6 +55,7 @@ export function Surface({
|
|
|
53
55
|
backgroundColor={t[toneToToken(tone)]}
|
|
54
56
|
flexDirection={flexDirection}
|
|
55
57
|
gap={gap}
|
|
58
|
+
minHeight={minHeight}
|
|
56
59
|
padding={padding}
|
|
57
60
|
paddingBottom={paddingBottom}
|
|
58
61
|
paddingLeft={paddingLeft}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { memo } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { SettingRow, SettingValue } from '../../../settings/types'
|
|
4
|
+
|
|
5
|
+
import { readRow, useSettingsStore } from '../../../settings/settings-store'
|
|
6
|
+
import { useAppStore } from '../../../state/app-store'
|
|
7
|
+
import { type ResolvedTuiTheme, useTheme } from '../../theme'
|
|
8
|
+
|
|
9
|
+
/** Filled means on. Both are one cell wide in a Latin-width terminal. */
|
|
10
|
+
const ON = '●'
|
|
11
|
+
const OFF = '○'
|
|
12
|
+
|
|
13
|
+
/** What a row shows on its right edge, per kind. */
|
|
14
|
+
export function formatValue(row: SettingRow, value: SettingValue): string {
|
|
15
|
+
switch (row.kind) {
|
|
16
|
+
case 'toggle':
|
|
17
|
+
return value === true ? ON : OFF
|
|
18
|
+
case 'select': {
|
|
19
|
+
const option = row.options.find((entry) => entry.value === value)
|
|
20
|
+
return `${option?.label ?? String(value)} ›`
|
|
21
|
+
}
|
|
22
|
+
case 'number':
|
|
23
|
+
return String(value)
|
|
24
|
+
case 'text': {
|
|
25
|
+
const text = String(value)
|
|
26
|
+
return text === '' ? (row.placeholder ?? 'not set') : text
|
|
27
|
+
}
|
|
28
|
+
case 'info':
|
|
29
|
+
case 'action':
|
|
30
|
+
return String(value)
|
|
31
|
+
default:
|
|
32
|
+
row satisfies never
|
|
33
|
+
return ''
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The same value in prose, for the line under the label. The column wants a glyph
|
|
39
|
+
* you can scan; a sentence wants a word — `default: ●` is not a sentence.
|
|
40
|
+
*/
|
|
41
|
+
export function describeValue(row: SettingRow, value: SettingValue): string {
|
|
42
|
+
switch (row.kind) {
|
|
43
|
+
case 'toggle':
|
|
44
|
+
return value === true ? 'on' : 'off'
|
|
45
|
+
case 'select':
|
|
46
|
+
return row.options.find((entry) => entry.value === value)?.label ?? String(value)
|
|
47
|
+
case 'text':
|
|
48
|
+
return String(value) === '' ? 'empty' : String(value)
|
|
49
|
+
default:
|
|
50
|
+
return String(value)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function valueColor(row: SettingRow, value: SettingValue, t: ResolvedTuiTheme): string {
|
|
55
|
+
// A toggle is read at a glance or not at all, so its state is the colour as much
|
|
56
|
+
// as the glyph: lit when on, as quiet as the rest of the row when off.
|
|
57
|
+
if (row.kind === 'toggle') return value === true ? t.success : t.textMuted
|
|
58
|
+
return row.kind === 'info' ? t.textMuted : t.primary
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A row's current value, read by the row itself.
|
|
63
|
+
*
|
|
64
|
+
* Per row rather than per list on purpose: a value is a primitive, so a PTY frame
|
|
65
|
+
* that rewrites `tabs` re-runs this selector, gets the same value back, and
|
|
66
|
+
* renders nothing. A parent subscribing to the whole store for every row in the
|
|
67
|
+
* list would repaint it at the rate the terminals print.
|
|
68
|
+
*/
|
|
69
|
+
export const RowValue = memo(function RowValue({ row }: { row: SettingRow }) {
|
|
70
|
+
const t = useTheme()
|
|
71
|
+
const values = useSettingsStore((s) => s.values)
|
|
72
|
+
const value = useAppStore((s) => readRow(row, { state: s, values }))
|
|
73
|
+
return <text fg={valueColor(row, value, t)}>{formatValue(row, value)}</text>
|
|
74
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { memo } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { SettingRow } from '../../../settings/types'
|
|
4
|
+
|
|
5
|
+
import { storedRow, useSettingsStore } from '../../../settings/settings-store'
|
|
6
|
+
import { useTheme } from '../../theme'
|
|
7
|
+
import { ListItem } from '../primitives/list-item'
|
|
8
|
+
import { describeValue, RowValue } from './row-value'
|
|
9
|
+
|
|
10
|
+
/** Where the value comes from, on the left, out of the value column's way. */
|
|
11
|
+
const CONFIG_FILE_MARK = '*'
|
|
12
|
+
const TOUCHED_MARK = '~'
|
|
13
|
+
|
|
14
|
+
interface SettingsRowProps {
|
|
15
|
+
row: SettingRow
|
|
16
|
+
active: boolean
|
|
17
|
+
index: number
|
|
18
|
+
onSelect: (index: number) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const SettingsRow = memo(function SettingsRow({
|
|
22
|
+
active,
|
|
23
|
+
index,
|
|
24
|
+
onSelect,
|
|
25
|
+
row,
|
|
26
|
+
}: SettingsRowProps) {
|
|
27
|
+
const t = useTheme()
|
|
28
|
+
const fromConfigFile = useSettingsStore((s) => s.fromConfigFile.has(row.id))
|
|
29
|
+
const touched = useSettingsStore((s) => s.touched.has(row.id))
|
|
30
|
+
const defaultValue = useSettingsStore((s) => s.defaults[row.id])
|
|
31
|
+
const editable = row.kind !== 'info'
|
|
32
|
+
// A blank when neither applies, never nothing: a marker that comes and goes
|
|
33
|
+
// would shift the label of every row it is missing from.
|
|
34
|
+
let mark = ' '
|
|
35
|
+
if (fromConfigFile) mark = CONFIG_FILE_MARK
|
|
36
|
+
else if (touched) mark = TOUCHED_MARK
|
|
37
|
+
|
|
38
|
+
// Always, never only on the focused row: a row that grows a line when you land
|
|
39
|
+
// on it shifts everything under it, so the list moves under the cursor the
|
|
40
|
+
// whole way down. A taller list that holds still reads better than a compact
|
|
41
|
+
// one that does not.
|
|
42
|
+
const notes: string[] = []
|
|
43
|
+
if (row.description != null && row.description !== '') notes.push(row.description)
|
|
44
|
+
if (fromConfigFile) notes.push('set in aimux.config.ts — comes back on restart')
|
|
45
|
+
else if (storedRow(row)?.restart === true) notes.push('applies on restart')
|
|
46
|
+
// Right-aligned on the same line rather than tacked onto the end of the
|
|
47
|
+
// description: it is about the row, not about what the setting does, and the
|
|
48
|
+
// value column above it is on that edge too. Only once the row has been
|
|
49
|
+
// changed — `r` is otherwise a key you press to find out what it does.
|
|
50
|
+
const resetHint =
|
|
51
|
+
touched && defaultValue !== undefined ? `r resets to ${describeValue(row, defaultValue)}` : null
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ListItem
|
|
55
|
+
id={`setting-row-${row.id}`}
|
|
56
|
+
active={active}
|
|
57
|
+
index={index}
|
|
58
|
+
onClickIndex={onSelect}
|
|
59
|
+
leading={<text fg={fromConfigFile ? t.warning : t.secondary}>{mark}</text>}
|
|
60
|
+
title={<text fg={editable ? t.text : t.textMuted}>{row.label}</text>}
|
|
61
|
+
subtitle={
|
|
62
|
+
notes.length > 0 || resetHint != null ? (
|
|
63
|
+
<box flexDirection="row">
|
|
64
|
+
<box flexGrow={1} flexShrink={1}>
|
|
65
|
+
<text fg={t.textMuted}>{notes.join(' · ')}</text>
|
|
66
|
+
</box>
|
|
67
|
+
{resetHint != null ? (
|
|
68
|
+
<text fg={t.textMuted} wrapMode="none">
|
|
69
|
+
{resetHint}
|
|
70
|
+
</text>
|
|
71
|
+
) : null}
|
|
72
|
+
</box>
|
|
73
|
+
) : undefined
|
|
74
|
+
}
|
|
75
|
+
trailing={<RowValue row={row} />}
|
|
76
|
+
/>
|
|
77
|
+
)
|
|
78
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
|
+
|
|
3
|
+
import { useTerminalDimensions } from '@opentui/react'
|
|
4
|
+
import { memo, useCallback, useMemo, useRef } from 'react'
|
|
5
|
+
|
|
6
|
+
import { getSectionRows, SETTING_SECTIONS } from '../../../settings/sections'
|
|
7
|
+
import { useSettingsStore } from '../../../settings/settings-store'
|
|
8
|
+
import { useAppStore } from '../../../state/app-store'
|
|
9
|
+
import { clampBarWidth } from '../../../state/bars'
|
|
10
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
11
|
+
import { useScrollActiveIntoView } from '../../hooks/use-scroll-active-into-view'
|
|
12
|
+
import { useTheme } from '../../theme'
|
|
13
|
+
import { ListItem } from '../primitives/list-item'
|
|
14
|
+
import { SettingsRow } from './settings-row'
|
|
15
|
+
|
|
16
|
+
const COLUMN_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 0 }
|
|
17
|
+
/**
|
|
18
|
+
* Share of the window the settings themselves are allowed. Without a cap the
|
|
19
|
+
* value sits on the far edge of a wide terminal, a hundred columns from the label
|
|
20
|
+
* it belongs to, and the pair stops reading as a pair.
|
|
21
|
+
*
|
|
22
|
+
* A share of the window rather than a fixed column count, so it holds whatever
|
|
23
|
+
* the window is; the floor is there for the narrow ones, where the pane is
|
|
24
|
+
* smaller than the cap anyway and this stops binding.
|
|
25
|
+
*/
|
|
26
|
+
const CONTENT_WIDTH_RATIO = 0.45
|
|
27
|
+
const CONTENT_MIN_WIDTH = 56
|
|
28
|
+
|
|
29
|
+
export const SettingsView = memo(function SettingsView() {
|
|
30
|
+
const t = useTheme()
|
|
31
|
+
// The cursor is all this component needs from the app state. Rows read their
|
|
32
|
+
// own values, so nothing here re-renders at the rate the terminals print.
|
|
33
|
+
const settings = useAppStore((s) => s.settings)
|
|
34
|
+
// The section list stands exactly where the left bar stands, so opening the
|
|
35
|
+
// screen does not shift the column under the cursor. Its configured width, not
|
|
36
|
+
// `getBarWidth`: a hidden bar still says how wide the sidebar is.
|
|
37
|
+
const navWidth = useAppStore((s) => clampBarWidth(s.bars.left.width))
|
|
38
|
+
// Which rows exist can depend on the projects — Setup has one per project — and
|
|
39
|
+
// building a Setup row reads its script off disk. So the list is rebuilt when
|
|
40
|
+
// the section changes, when the projects change, and after any write; not on
|
|
41
|
+
// every render, which at the rate this store changes would mean reading files
|
|
42
|
+
// at the rate the terminals print.
|
|
43
|
+
const projects = useAppStore((s) => s.projects)
|
|
44
|
+
const revision = useSettingsStore((s) => s.revision)
|
|
45
|
+
const rows = useMemo(
|
|
46
|
+
() => getSectionRows(settings.sectionId, projects),
|
|
47
|
+
// `revision` is an invalidation key, not an input: it says a value the builder
|
|
48
|
+
// read from disk may have changed under it.
|
|
49
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
50
|
+
[settings.sectionId, projects, revision]
|
|
51
|
+
)
|
|
52
|
+
const dimensions = useTerminalDimensions()
|
|
53
|
+
const contentMaxWidth = Math.max(
|
|
54
|
+
CONTENT_MIN_WIDTH,
|
|
55
|
+
Math.round(dimensions.width * CONTENT_WIDTH_RATIO)
|
|
56
|
+
)
|
|
57
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
58
|
+
|
|
59
|
+
useScrollActiveIntoView({
|
|
60
|
+
activeId: settings.pane === 'rows' ? (rows[settings.rowIndex]?.id ?? null) : null,
|
|
61
|
+
idPrefix: 'setting-row-',
|
|
62
|
+
scrollRef,
|
|
63
|
+
visible: true,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const handleSectionClick = useCallback((index: number) => {
|
|
67
|
+
const sectionId = SETTING_SECTIONS[index]?.id
|
|
68
|
+
if (sectionId == null) return
|
|
69
|
+
dispatchGlobal({ sectionId, type: 'settings-select-section' })
|
|
70
|
+
}, [])
|
|
71
|
+
|
|
72
|
+
// One click selects the row and changes it — a checkbox you have to select
|
|
73
|
+
// first and then click again is not a checkbox.
|
|
74
|
+
const handleRowClick = useCallback((rowIndex: number) => {
|
|
75
|
+
dispatchGlobal({ rowIndex, type: 'settings-select-row' })
|
|
76
|
+
runSideEffectGlobal({ type: 'activate-settings-row' })
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
const handleClose = useCallback(() => {
|
|
80
|
+
dispatchGlobal({ type: 'exit-settings' })
|
|
81
|
+
}, [])
|
|
82
|
+
|
|
83
|
+
const section = SETTING_SECTIONS.find((s) => s.id === settings.sectionId)
|
|
84
|
+
const sectionLabel = section?.label ?? 'Settings'
|
|
85
|
+
const sectionNote = section?.description
|
|
86
|
+
// Same 1-cell seam the bar draws between itself and the terminal, so the two
|
|
87
|
+
// views line up to the column.
|
|
88
|
+
const navContentWidth = Math.max(1, navWidth - 1)
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<box flexDirection="row" flexGrow={1} overflow="hidden">
|
|
92
|
+
<box width={navWidth} flexDirection="row" overflow="hidden" backgroundColor={t.background}>
|
|
93
|
+
<box width={navContentWidth} flexDirection="column" overflow="hidden">
|
|
94
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
95
|
+
<text fg={t.textMuted}>Settings</text>
|
|
96
|
+
</box>
|
|
97
|
+
<box flexGrow={1} flexShrink={1} flexDirection="column" overflow="hidden">
|
|
98
|
+
{SETTING_SECTIONS.map((section, index) => (
|
|
99
|
+
<ListItem
|
|
100
|
+
key={section.id}
|
|
101
|
+
active={section.id === settings.sectionId}
|
|
102
|
+
index={index}
|
|
103
|
+
onClickIndex={handleSectionClick}
|
|
104
|
+
title={
|
|
105
|
+
<text fg={section.id === settings.sectionId ? t.text : t.textMuted}>
|
|
106
|
+
{section.label}
|
|
107
|
+
</text>
|
|
108
|
+
}
|
|
109
|
+
trailing={
|
|
110
|
+
section.id === settings.sectionId && settings.pane === 'nav' ? (
|
|
111
|
+
<text fg={t.primary}>›</text>
|
|
112
|
+
) : undefined
|
|
113
|
+
}
|
|
114
|
+
/>
|
|
115
|
+
))}
|
|
116
|
+
</box>
|
|
117
|
+
<box flexDirection="row" flexShrink={0} paddingLeft={1} paddingRight={1}>
|
|
118
|
+
<text fg={t.textMuted} onMouseDown={handleClose}>
|
|
119
|
+
‹ Close
|
|
120
|
+
</text>
|
|
121
|
+
</box>
|
|
122
|
+
</box>
|
|
123
|
+
<box width={1} flexShrink={0} backgroundColor={t.border} />
|
|
124
|
+
</box>
|
|
125
|
+
{/* The pane the terminal would be in, with the same border — the content
|
|
126
|
+
keeps the inset it had, instead of jumping to the edge of the screen. */}
|
|
127
|
+
<box
|
|
128
|
+
border
|
|
129
|
+
borderColor={settings.pane === 'rows' ? t.borderActive : t.border}
|
|
130
|
+
title={sectionLabel}
|
|
131
|
+
padding={0}
|
|
132
|
+
flexDirection="column"
|
|
133
|
+
flexGrow={1}
|
|
134
|
+
backgroundColor={t.background}
|
|
135
|
+
>
|
|
136
|
+
<box flexDirection="column" flexGrow={1} flexShrink={1} maxWidth={contentMaxWidth}>
|
|
137
|
+
{sectionNote != null && sectionNote !== '' ? (
|
|
138
|
+
<box flexShrink={0} paddingLeft={1} paddingRight={1}>
|
|
139
|
+
<text fg={t.textMuted}>{sectionNote}</text>
|
|
140
|
+
</box>
|
|
141
|
+
) : null}
|
|
142
|
+
<scrollbox
|
|
143
|
+
ref={scrollRef}
|
|
144
|
+
scrollY
|
|
145
|
+
flexGrow={1}
|
|
146
|
+
flexShrink={1}
|
|
147
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
148
|
+
>
|
|
149
|
+
{rows.length === 0 ? (
|
|
150
|
+
<box paddingLeft={1}>
|
|
151
|
+
<text fg={t.textMuted}>Nothing to configure here yet.</text>
|
|
152
|
+
</box>
|
|
153
|
+
) : (
|
|
154
|
+
rows.map((row, index) => (
|
|
155
|
+
<SettingsRow
|
|
156
|
+
key={row.id}
|
|
157
|
+
row={row}
|
|
158
|
+
index={index}
|
|
159
|
+
active={settings.pane === 'rows' && index === settings.rowIndex}
|
|
160
|
+
onSelect={handleRowClick}
|
|
161
|
+
/>
|
|
162
|
+
))
|
|
163
|
+
)}
|
|
164
|
+
</scrollbox>
|
|
165
|
+
</box>
|
|
166
|
+
</box>
|
|
167
|
+
</box>
|
|
168
|
+
)
|
|
169
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { memo, useCallback, useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { SideEffect } from '../../../input/modes/types'
|
|
4
|
+
|
|
5
|
+
import { measureGlobal } from '../../../app-runtime/measure-ref'
|
|
6
|
+
import { findSetupTab } from '../../../app-runtime/setup-actions'
|
|
7
|
+
import { usePaneSizeReport } from '../../../app-runtime/use-pane-size-report'
|
|
8
|
+
import { useAppStore } from '../../../state/app-store'
|
|
9
|
+
import { runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
10
|
+
import { hasSetupScript } from '../../../state/project-data'
|
|
11
|
+
import { getActiveWorkspace, getCurrentProject } from '../../../state/project-workspaces'
|
|
12
|
+
import { useTheme, useTransparent } from '../../theme'
|
|
13
|
+
import { TerminalViewport } from '../layout/terminal-pane'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How often the widget re-checks whether a setup script exists. Needed because
|
|
17
|
+
* the file can appear with no state change behind it — an agent writing it, or
|
|
18
|
+
* the user saving in their editor. Cheap `existsSync`; checking on every render
|
|
19
|
+
* would fire a syscall per PTY frame.
|
|
20
|
+
*/
|
|
21
|
+
const SCRIPT_POLL_MS = 2_000
|
|
22
|
+
|
|
23
|
+
// Hoisted so they are stable prop identities across renders.
|
|
24
|
+
const RUN: SideEffect = { type: 'run-setup' }
|
|
25
|
+
const STOP: SideEffect = { type: 'stop-setup' }
|
|
26
|
+
const CONFIGURE: SideEffect = { type: 'configure-setup-script' }
|
|
27
|
+
const ASK_AGENT: SideEffect = { type: 'ask-agent-for-setup-script' }
|
|
28
|
+
const PROMOTE: SideEffect = { type: 'promote-setup-tab' }
|
|
29
|
+
|
|
30
|
+
function Button({ effect, label }: { effect: SideEffect; label: string }) {
|
|
31
|
+
const t = useTheme()
|
|
32
|
+
const transparent = useTransparent()
|
|
33
|
+
const onMouseDown = useCallback(() => runSideEffectGlobal(effect), [effect])
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<box
|
|
37
|
+
paddingLeft={1}
|
|
38
|
+
paddingRight={1}
|
|
39
|
+
flexShrink={0}
|
|
40
|
+
backgroundColor={transparent ? undefined : t.backgroundElement}
|
|
41
|
+
onMouseDown={onMouseDown}
|
|
42
|
+
>
|
|
43
|
+
<text selectable={false} fg={t.text} wrapMode="none">
|
|
44
|
+
{label}
|
|
45
|
+
</text>
|
|
46
|
+
</box>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const SetupWidget = memo(function SetupWidget() {
|
|
51
|
+
const t = useTheme()
|
|
52
|
+
const currentProjectId = useAppStore((s) => s.currentProjectId)
|
|
53
|
+
const projects = useAppStore((s) => s.projects)
|
|
54
|
+
|
|
55
|
+
const project = getCurrentProject({ currentProjectId, projects })
|
|
56
|
+
const workspace = getActiveWorkspace(project)
|
|
57
|
+
|
|
58
|
+
// Selecting the tab rather than the whole `tabs` array: that array gets a new
|
|
59
|
+
// identity on every viewport frame of every terminal, so subscribing to it
|
|
60
|
+
// re-renders this widget at the rate the other PTYs print. The found tab keeps
|
|
61
|
+
// its identity until it changes, which is exactly when a redraw is due.
|
|
62
|
+
const setupTab = useAppStore((s) => findSetupTab(s.tabs, workspace?.id))
|
|
63
|
+
|
|
64
|
+
const [scriptExists, setScriptExists] = useState(false)
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (project === undefined) {
|
|
67
|
+
setScriptExists(false)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const projectId = project.id
|
|
71
|
+
const check = () => setScriptExists(hasSetupScript(projectId))
|
|
72
|
+
check()
|
|
73
|
+
const timer = setInterval(check, SCRIPT_POLL_MS)
|
|
74
|
+
return () => clearInterval(timer)
|
|
75
|
+
}, [project])
|
|
76
|
+
|
|
77
|
+
// The PTY is sized from this box, not from the main terminal area — the resize
|
|
78
|
+
// cascade skips hidden tabs precisely so this is the only authority.
|
|
79
|
+
const setContentBox = usePaneSizeReport(setupTab?.id, setupTab !== undefined, measureGlobal)
|
|
80
|
+
|
|
81
|
+
if (!project || !workspace) {
|
|
82
|
+
return (
|
|
83
|
+
<box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} overflow="hidden">
|
|
84
|
+
<text selectable={false} fg={t.textMuted} wrapMode="none">
|
|
85
|
+
No workspace
|
|
86
|
+
</text>
|
|
87
|
+
</box>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ranAt = workspace.setupRanAt
|
|
92
|
+
const exitCode = workspace.setupExitCode
|
|
93
|
+
const finished = ranAt != null && ranAt !== ''
|
|
94
|
+
const running = setupTab !== undefined && !finished
|
|
95
|
+
|
|
96
|
+
let statusText = 'not run here'
|
|
97
|
+
let statusFg = t.textMuted
|
|
98
|
+
if (!scriptExists) {
|
|
99
|
+
statusText = 'no setup script'
|
|
100
|
+
} else if (running) {
|
|
101
|
+
statusText = 'running…'
|
|
102
|
+
statusFg = t.primary
|
|
103
|
+
} else if (finished) {
|
|
104
|
+
statusText = exitCode === 0 ? '✓ setup ok' : `✗ exit ${exitCode ?? '?'}`
|
|
105
|
+
statusFg = exitCode === 0 ? t.success : t.error
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let runLabel = 'Run'
|
|
109
|
+
if (running) runLabel = 'Stop'
|
|
110
|
+
else if (finished) runLabel = 'Re-run'
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} overflow="hidden">
|
|
114
|
+
<box flexDirection="row" flexShrink={0} justifyContent="space-between" gap={1}>
|
|
115
|
+
<text selectable={false} fg={statusFg} wrapMode="none">
|
|
116
|
+
{statusText}
|
|
117
|
+
</text>
|
|
118
|
+
<text selectable={false} fg={t.textMuted} wrapMode="none">
|
|
119
|
+
{workspace.name}
|
|
120
|
+
</text>
|
|
121
|
+
</box>
|
|
122
|
+
|
|
123
|
+
{/* Only Run depends on a script existing. Edit and Agent must not vanish
|
|
124
|
+
the moment one does, or writing a stub and closing the editor leaves no
|
|
125
|
+
way to ask an agent to fill it in — and no way out. */}
|
|
126
|
+
<box flexDirection="row" flexShrink={0} gap={1} paddingTop={1} paddingBottom={1}>
|
|
127
|
+
{scriptExists ? <Button effect={running ? STOP : RUN} label={runLabel} /> : null}
|
|
128
|
+
<Button effect={CONFIGURE} label={scriptExists ? 'Edit' : 'Create'} />
|
|
129
|
+
<Button effect={ASK_AGENT} label="Agent" />
|
|
130
|
+
{/* A failing setup means reading a stack trace, which a bar this narrow
|
|
131
|
+
cannot do. This is the way out. */}
|
|
132
|
+
{setupTab ? <Button effect={PROMOTE} label="↗" /> : null}
|
|
133
|
+
</box>
|
|
134
|
+
|
|
135
|
+
{setupTab ? (
|
|
136
|
+
<box ref={setContentBox} flexDirection="column" flexGrow={1} width="100%" overflow="hidden">
|
|
137
|
+
<TerminalViewport
|
|
138
|
+
buffer={setupTab.buffer}
|
|
139
|
+
softCursor={false}
|
|
140
|
+
viewport={setupTab.viewport}
|
|
141
|
+
/>
|
|
142
|
+
</box>
|
|
143
|
+
) : (
|
|
144
|
+
<text selectable={false} fg={t.textMuted} wrapMode="word">
|
|
145
|
+
{scriptExists
|
|
146
|
+
? 'Runs automatically in each new workspace.'
|
|
147
|
+
: 'A setup script installs what a fresh worktree lacks: dependencies, .env, caches.'}
|
|
148
|
+
</text>
|
|
149
|
+
)}
|
|
150
|
+
</box>
|
|
151
|
+
)
|
|
152
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AppState, FlashJumpTarget, FlashLabel } from '../../state/types'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { filterTabsForActiveWorkspace, getPrimaryWorkspace } from '../../state/project-workspaces'
|
|
4
|
+
import { orderProjectsForDisplay } from '../project-ordering'
|
|
5
5
|
import { assignFlashLabels, type FlashTarget } from './assign-labels'
|
|
6
6
|
|
|
7
7
|
interface PendingTarget {
|
|
@@ -12,50 +12,50 @@ interface PendingTarget {
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Collect every visible flash-jump target in the same order the UI renders:
|
|
15
|
-
*
|
|
16
|
-
* tabs of the active
|
|
15
|
+
* project rows first (each followed by its non-primary workspaces), then the
|
|
16
|
+
* tabs of the active workspace. Stable ordering keeps the assigned labels
|
|
17
17
|
* predictable across re-opens when nothing changed.
|
|
18
18
|
*/
|
|
19
19
|
function collectTargets(state: AppState): PendingTarget[] {
|
|
20
20
|
const out: PendingTarget[] = []
|
|
21
|
-
const ordered =
|
|
22
|
-
for (const [index,
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const primary =
|
|
21
|
+
const ordered = orderProjectsForDisplay(state.projects)
|
|
22
|
+
for (const [index, project] of ordered.entries()) {
|
|
23
|
+
const projectIndex = index + 1
|
|
24
|
+
const workspaces = project.workspaces ?? []
|
|
25
|
+
const primary = getPrimaryWorkspace(workspaces)
|
|
26
26
|
out.push({
|
|
27
|
-
key: `ws:${
|
|
28
|
-
name:
|
|
27
|
+
key: `ws:${project.id}`,
|
|
28
|
+
name: project.name,
|
|
29
29
|
target: {
|
|
30
|
-
kind: '
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
kind: 'project',
|
|
31
|
+
projectId: project.id,
|
|
32
|
+
projectIndex,
|
|
33
|
+
workspaceId: primary?.id,
|
|
34
34
|
},
|
|
35
35
|
})
|
|
36
|
-
for (const
|
|
37
|
-
if (
|
|
36
|
+
for (const workspace of workspaces) {
|
|
37
|
+
if (workspace.id === primary?.id) continue
|
|
38
38
|
out.push({
|
|
39
|
-
key: `wt:${
|
|
40
|
-
name:
|
|
39
|
+
key: `wt:${workspace.id}`,
|
|
40
|
+
name: workspace.name,
|
|
41
41
|
target: {
|
|
42
|
-
kind: '
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
kind: 'workspace',
|
|
43
|
+
projectId: project.id,
|
|
44
|
+
projectIndex,
|
|
45
|
+
workspaceId: workspace.id,
|
|
46
46
|
},
|
|
47
47
|
})
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
const tabs =
|
|
51
|
+
const currentProject = state.projects.find((s) => s.id === state.currentProjectId)
|
|
52
|
+
if (currentProject) {
|
|
53
|
+
const tabs = filterTabsForActiveWorkspace(state.tabs, currentProject)
|
|
54
54
|
for (const tab of tabs) {
|
|
55
55
|
out.push({
|
|
56
56
|
key: `tab:${tab.id}`,
|
|
57
57
|
name: tab.title,
|
|
58
|
-
target: { kind: 'tab',
|
|
58
|
+
target: { kind: 'tab', projectId: currentProject.id, projectIndex: 0, tabId: tab.id },
|
|
59
59
|
})
|
|
60
60
|
}
|
|
61
61
|
}
|