@brimveyn/aimux 1.7.4 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -15
- package/package.json +2 -2
- package/src/app-runtime/auto-commit-driver.ts +286 -0
- package/src/app-runtime/auto-commit-ref.ts +20 -0
- package/src/app-runtime/backend-attach-runtime.ts +3 -1
- package/src/app-runtime/session-actions.ts +7 -2
- package/src/app-runtime/side-effects.ts +99 -1
- package/src/app-runtime/use-auto-commit-driver.ts +133 -0
- package/src/app-runtime/use-terminal-resize.ts +20 -12
- package/src/app.tsx +59 -22
- package/src/auto-commit/default-auto-commit-prompt.md +46 -0
- package/src/auto-commit/headless-commands.ts +40 -0
- package/src/auto-commit/output-parser.ts +21 -0
- package/src/auto-commit/prompt-loader.ts +33 -0
- package/src/auto-commit/staging-mode.ts +5 -0
- package/src/auto-commit/strip-ansi.ts +13 -0
- package/src/auto-commit/suggestion-runner.ts +55 -0
- package/src/auto-commit/working-tree-hash.ts +24 -0
- package/src/config.ts +24 -0
- package/src/daemon/session-registry.ts +1 -0
- package/src/index.tsx +1 -1
- package/src/input/keymap/help-entries.ts +4 -4
- package/src/input/modes/bridge.ts +6 -0
- package/src/input/modes/transitions.ts +3 -1
- package/src/input/modes/types.ts +4 -0
- package/src/ipc/manager-protocol.ts +2 -2
- package/src/ipc/protocol.ts +2 -8
- package/src/pty/assistant-status-detector.ts +1 -1
- package/src/pty/terminal-snapshot.ts +38 -4
- package/src/services/ai-usage/adapters/claude.ts +139 -0
- package/src/services/ai-usage/adapters/codex.ts +191 -0
- package/src/services/ai-usage/provider.ts +84 -0
- package/src/services/ai-usage/spawn.ts +49 -0
- package/src/services/ai-usage/types.ts +20 -0
- package/src/session-backend/local-session-backend.ts +10 -2
- package/src/state/ai-usage-store.ts +29 -0
- package/src/state/reducers/auto-commit-state.ts +59 -0
- package/src/state/reducers/modal-state.ts +106 -2
- package/src/state/reducers/session-state.ts +26 -14
- package/src/state/session-persistence.ts +14 -5
- package/src/state/store.ts +24 -14
- package/src/state/types.ts +55 -2
- package/src/state/workspace-save.ts +4 -0
- package/src/ui/ai-usage/controller.ts +35 -0
- package/src/ui/components/ai-usage-indicator.tsx +131 -0
- package/src/ui/components/ai-usage-popover.tsx +152 -0
- package/src/ui/components/create-session-modal.tsx +2 -2
- package/src/ui/components/git-commit-modal.tsx +167 -18
- package/src/ui/components/session-bar.tsx +2 -2
- package/src/ui/components/session-picker-modal.tsx +4 -4
- package/src/ui/components/sidebar.tsx +3 -1
- package/src/ui/components/status-bar.tsx +2 -0
- package/src/ui/components/terminal-pane.tsx +18 -3
- package/src/ui/root.tsx +13 -2
- package/src/ui/status-bar-model.ts +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { AIUsageTool } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
3
|
+
import { useAIUsageStore } from '../../state/ai-usage-store'
|
|
4
|
+
import { toggleAIUsagePopover } from '../ai-usage/controller'
|
|
5
|
+
import { useTokens } from '../theme'
|
|
6
|
+
|
|
7
|
+
const TOOL_ICON: Record<AIUsageTool, string> = {
|
|
8
|
+
claude: 'CC',
|
|
9
|
+
codex: 'CO',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const BAR_SEGMENTS = 4
|
|
13
|
+
const BAR_FILLED_CHAR = '\u{2501}'
|
|
14
|
+
const BAR_EMPTY_CHAR = '\u{2500}'
|
|
15
|
+
|
|
16
|
+
function formatTokens(total: number): string {
|
|
17
|
+
if (total >= 1_000_000) return `${(total / 1_000_000).toFixed(1)}M`
|
|
18
|
+
if (total >= 1_000) return `${(total / 1_000).toFixed(1)}k`
|
|
19
|
+
return String(total)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildBar(percent: number): { filled: string; empty: string } {
|
|
23
|
+
let filledCount = 0
|
|
24
|
+
for (let i = 0; i < BAR_SEGMENTS; i++) {
|
|
25
|
+
if (percent > i * (100 / BAR_SEGMENTS)) filledCount++
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
empty: BAR_EMPTY_CHAR.repeat(BAR_SEGMENTS - filledCount),
|
|
29
|
+
filled: BAR_FILLED_CHAR.repeat(filledCount),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatResetIn(snap: {
|
|
34
|
+
resetAt: string | null
|
|
35
|
+
timeRemaining: string | null
|
|
36
|
+
}): string | null {
|
|
37
|
+
if (snap.resetAt) {
|
|
38
|
+
const diffMs = new Date(snap.resetAt).getTime() - Date.now()
|
|
39
|
+
if (diffMs > 0) {
|
|
40
|
+
const totalMin = Math.round(diffMs / 60_000)
|
|
41
|
+
const h = Math.floor(totalMin / 60)
|
|
42
|
+
const m = totalMin % 60
|
|
43
|
+
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}h`
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return snap.timeRemaining
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function AIUsageIndicator() {
|
|
50
|
+
const t = useTokens()
|
|
51
|
+
const enabled = useAIUsageStore((s) => s.enabled)
|
|
52
|
+
const snapshots = useAIUsageStore((s) => s.snapshots)
|
|
53
|
+
|
|
54
|
+
if (!enabled) return null
|
|
55
|
+
|
|
56
|
+
const ordered: AIUsageTool[] = ['claude', 'codex']
|
|
57
|
+
const entries = ordered
|
|
58
|
+
.map((tool) => ({ snap: snapshots[tool], tool }))
|
|
59
|
+
.filter((entry) => entry.snap !== undefined)
|
|
60
|
+
|
|
61
|
+
if (entries.length === 0) {
|
|
62
|
+
return (
|
|
63
|
+
<box flexDirection="row" gap={1}>
|
|
64
|
+
<text fg={t.muted}>…</text>
|
|
65
|
+
</box>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<box
|
|
71
|
+
flexDirection="row"
|
|
72
|
+
gap={2}
|
|
73
|
+
onMouseDown={(e) => {
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
e.stopPropagation()
|
|
76
|
+
if (e.button !== 0) return
|
|
77
|
+
toggleAIUsagePopover(e.x, e.y)
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{entries.map(({ snap, tool }) => {
|
|
81
|
+
if (!snap) return null
|
|
82
|
+
const icon = TOOL_ICON[tool]
|
|
83
|
+
if (snap.error) {
|
|
84
|
+
return (
|
|
85
|
+
<text key={tool} fg={t.palette.error} selectable={false}>
|
|
86
|
+
{icon} —
|
|
87
|
+
</text>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
if (snap.percent !== null) {
|
|
91
|
+
const p = Math.round(snap.percent)
|
|
92
|
+
let color = t.palette.success
|
|
93
|
+
if (p >= 85) {
|
|
94
|
+
color = t.palette.error
|
|
95
|
+
} else if (p >= 60) {
|
|
96
|
+
color = t.palette.warning
|
|
97
|
+
}
|
|
98
|
+
const { empty, filled } = buildBar(snap.percent)
|
|
99
|
+
const reset = formatResetIn(snap)
|
|
100
|
+
const pctText = `${String(p).padStart(2, ' ')}%`
|
|
101
|
+
return (
|
|
102
|
+
<box key={tool} flexDirection="row">
|
|
103
|
+
<text fg={color} selectable={false}>
|
|
104
|
+
{icon}{' '}
|
|
105
|
+
</text>
|
|
106
|
+
<text fg={color} selectable={false}>
|
|
107
|
+
{filled}
|
|
108
|
+
</text>
|
|
109
|
+
<text fg={t.muted} selectable={false}>
|
|
110
|
+
{empty}
|
|
111
|
+
</text>
|
|
112
|
+
<text fg={t.palette.ink} selectable={false}>
|
|
113
|
+
{` ${pctText}`}
|
|
114
|
+
</text>
|
|
115
|
+
{reset ? (
|
|
116
|
+
<text fg={t.muted} selectable={false}>
|
|
117
|
+
{` · ${reset}`}
|
|
118
|
+
</text>
|
|
119
|
+
) : null}
|
|
120
|
+
</box>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
return (
|
|
124
|
+
<text key={tool} fg={t.muted} selectable={false}>
|
|
125
|
+
{icon} {formatTokens(snap.tokens.total)}
|
|
126
|
+
</text>
|
|
127
|
+
)
|
|
128
|
+
})}
|
|
129
|
+
</box>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { AIUsageTool } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
3
|
+
import { useKeyboard } from '@opentui/react'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
import type { UsageSnapshot } from '../../services/ai-usage/types'
|
|
7
|
+
|
|
8
|
+
import { useAIUsageStore } from '../../state/ai-usage-store'
|
|
9
|
+
import { useAppStore } from '../../state/app-store'
|
|
10
|
+
import {
|
|
11
|
+
type AIUsagePopoverState,
|
|
12
|
+
closeAIUsagePopover,
|
|
13
|
+
subscribeAIUsagePopover,
|
|
14
|
+
} from '../ai-usage/controller'
|
|
15
|
+
import { useTokens } from '../theme'
|
|
16
|
+
|
|
17
|
+
const TOOL_TITLE: Record<AIUsageTool, string> = {
|
|
18
|
+
claude: 'Claude Code',
|
|
19
|
+
codex: 'Codex',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const POPOVER_WIDTH = 38
|
|
23
|
+
|
|
24
|
+
function fmt(n: number): string {
|
|
25
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`
|
|
26
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`
|
|
27
|
+
return String(n)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildLines(snap: UsageSnapshot): string[] {
|
|
31
|
+
if (snap.error) {
|
|
32
|
+
return [`error: ${snap.error.slice(0, 30)}`]
|
|
33
|
+
}
|
|
34
|
+
const lines: string[] = []
|
|
35
|
+
if (snap.percent !== null) {
|
|
36
|
+
lines.push(`usage ${snap.percent.toFixed(1)}%`)
|
|
37
|
+
}
|
|
38
|
+
lines.push(`tokens in ${fmt(snap.tokens.input)} / out ${fmt(snap.tokens.output)}`)
|
|
39
|
+
if (snap.tokens.cache > 0) {
|
|
40
|
+
lines.push(`cache ${fmt(snap.tokens.cache)}`)
|
|
41
|
+
}
|
|
42
|
+
lines.push(`total ${fmt(snap.tokens.total)}`)
|
|
43
|
+
if (snap.costUSD !== null) {
|
|
44
|
+
lines.push(`cost $${snap.costUSD.toFixed(2)}`)
|
|
45
|
+
}
|
|
46
|
+
if (snap.burnRatePerHour !== null) {
|
|
47
|
+
lines.push(`burn ${fmt(Math.round(snap.burnRatePerHour))}/h`)
|
|
48
|
+
}
|
|
49
|
+
if (snap.timeRemaining) {
|
|
50
|
+
lines.push(`resets ${snap.timeRemaining}`)
|
|
51
|
+
} else if (snap.resetAt) {
|
|
52
|
+
lines.push(`resets ${new Date(snap.resetAt).toLocaleTimeString()}`)
|
|
53
|
+
}
|
|
54
|
+
return lines
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function AIUsagePopover() {
|
|
58
|
+
const [popover, setPopover] = useState<AIUsagePopoverState | null>(null)
|
|
59
|
+
const t = useTokens()
|
|
60
|
+
const enabled = useAIUsageStore((s) => s.enabled)
|
|
61
|
+
const snapshots = useAIUsageStore((s) => s.snapshots)
|
|
62
|
+
const terminalCols = useAppStore((s) => s.layout.terminalCols)
|
|
63
|
+
const terminalRows = useAppStore((s) => s.layout.terminalRows)
|
|
64
|
+
|
|
65
|
+
useEffect(() => subscribeAIUsagePopover(setPopover), [])
|
|
66
|
+
|
|
67
|
+
useKeyboard((key) => {
|
|
68
|
+
if (!popover) return
|
|
69
|
+
if (key.name === 'escape') {
|
|
70
|
+
key.preventDefault()
|
|
71
|
+
closeAIUsagePopover()
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
if (!enabled || !popover) return null
|
|
76
|
+
|
|
77
|
+
const tools: AIUsageTool[] = ['claude', 'codex']
|
|
78
|
+
const sections = tools
|
|
79
|
+
.map((tool) => ({ snap: snapshots[tool], tool }))
|
|
80
|
+
.filter((s) => s.snap !== undefined)
|
|
81
|
+
|
|
82
|
+
let bodyLines = 0
|
|
83
|
+
if (sections.length === 0) {
|
|
84
|
+
bodyLines = 1
|
|
85
|
+
} else {
|
|
86
|
+
for (const s of sections) {
|
|
87
|
+
if (!s.snap) continue
|
|
88
|
+
bodyLines += buildLines(s.snap).length + 2
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const height = Math.min(terminalRows - 2, bodyLines + 2)
|
|
92
|
+
const width = Math.min(terminalCols - 2, POPOVER_WIDTH)
|
|
93
|
+
|
|
94
|
+
const left = Math.max(0, Math.min(popover.anchorX - width + 2, terminalCols - width))
|
|
95
|
+
const top = Math.max(0, popover.anchorY - height)
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<box position="absolute" top={0} left={0} width="100%" height="100%">
|
|
99
|
+
<box
|
|
100
|
+
position="absolute"
|
|
101
|
+
top={0}
|
|
102
|
+
left={0}
|
|
103
|
+
width="100%"
|
|
104
|
+
height="100%"
|
|
105
|
+
onMouseDown={(e) => {
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
e.stopPropagation()
|
|
108
|
+
closeAIUsagePopover()
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
<box
|
|
112
|
+
position="absolute"
|
|
113
|
+
top={top}
|
|
114
|
+
left={left}
|
|
115
|
+
width={width}
|
|
116
|
+
flexDirection="column"
|
|
117
|
+
border
|
|
118
|
+
borderColor={t.palette.primary}
|
|
119
|
+
backgroundColor={t.elevated}
|
|
120
|
+
onMouseDown={(e) => {
|
|
121
|
+
e.stopPropagation()
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{sections.length === 0 ? (
|
|
125
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
126
|
+
<text fg={t.muted} selectable={false}>
|
|
127
|
+
no data yet — collecting…
|
|
128
|
+
</text>
|
|
129
|
+
</box>
|
|
130
|
+
) : (
|
|
131
|
+
sections.map(({ snap, tool }, idx) => {
|
|
132
|
+
if (!snap) return null
|
|
133
|
+
const lines = buildLines(snap)
|
|
134
|
+
return (
|
|
135
|
+
<box key={tool} flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
136
|
+
{idx > 0 ? <text fg={t.muted}> </text> : null}
|
|
137
|
+
<text fg={t.accent} selectable={false}>
|
|
138
|
+
{TOOL_TITLE[tool]}
|
|
139
|
+
</text>
|
|
140
|
+
{lines.map((line, i) => (
|
|
141
|
+
<text key={`${tool}-${i}`} fg={t.palette.ink} selectable={false}>
|
|
142
|
+
{line}
|
|
143
|
+
</text>
|
|
144
|
+
))}
|
|
145
|
+
</box>
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
)}
|
|
149
|
+
</box>
|
|
150
|
+
</box>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
@@ -48,7 +48,7 @@ export function CreateSessionModal({
|
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
50
|
<ModalShell
|
|
51
|
-
title="Create
|
|
51
|
+
title="Create workspace"
|
|
52
52
|
keybindsModeId="modal.create-session"
|
|
53
53
|
width={uiTokens.modalWidth.xl}
|
|
54
54
|
>
|
|
@@ -88,7 +88,7 @@ export function CreateSessionModal({
|
|
|
88
88
|
</box>
|
|
89
89
|
|
|
90
90
|
<box flexDirection="column">
|
|
91
|
-
<text fg={nameActive ? t.palette.ink : t.muted}>
|
|
91
|
+
<text fg={nameActive ? t.palette.ink : t.muted}>Workspace name</text>
|
|
92
92
|
<InputField active={nameActive} value={sessionName} />
|
|
93
93
|
</box>
|
|
94
94
|
</ModalShell>
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { isAutoCommitEnabled } from '@brimveyn/aimux-config'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import type { ModeId } from '../../input/modes/types'
|
|
5
|
+
|
|
6
|
+
import { useAppStore } from '../../state/app-store'
|
|
7
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
1
8
|
import { useTokens } from '../theme'
|
|
2
9
|
import { uiTokens } from '../ui-tokens'
|
|
3
10
|
import { InputField } from './input-field'
|
|
@@ -8,32 +15,174 @@ interface GitCommitModalProps {
|
|
|
8
15
|
title: string
|
|
9
16
|
body: string
|
|
10
17
|
cursorPos: number
|
|
18
|
+
stage: 'edit' | 'generating' | 'confirm'
|
|
19
|
+
assistant?: string
|
|
20
|
+
model?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
24
|
+
const SPINNER_INTERVAL_MS = 80
|
|
25
|
+
|
|
26
|
+
function useSpinnerFrame(active: boolean): string {
|
|
27
|
+
const [index, setIndex] = useState(0)
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!active) return
|
|
30
|
+
const id = setInterval(
|
|
31
|
+
() => setIndex((i) => (i + 1) % SPINNER_FRAMES.length),
|
|
32
|
+
SPINNER_INTERVAL_MS
|
|
33
|
+
)
|
|
34
|
+
return () => clearInterval(id)
|
|
35
|
+
}, [active])
|
|
36
|
+
return SPINNER_FRAMES[index] ?? '⠋'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pickModeId(stage: 'edit' | 'generating' | 'confirm'): ModeId {
|
|
40
|
+
if (stage === 'generating') return 'modal.git-commit.generating'
|
|
41
|
+
if (stage === 'confirm') return 'modal.git-commit.confirm'
|
|
42
|
+
return 'modal.git-commit'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pickShellTitle(stage: 'edit' | 'generating' | 'confirm'): string {
|
|
46
|
+
if (stage === 'generating') return 'Auto-commit'
|
|
47
|
+
if (stage === 'confirm') return 'Auto-commit (stage all + commit)'
|
|
48
|
+
return 'Commit'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function GeneratingOverlay({ assistant, model }: { assistant?: string; model?: string }) {
|
|
52
|
+
const t = useTokens()
|
|
53
|
+
const frame = useSpinnerFrame(true)
|
|
54
|
+
const providerLabel = [assistant, model].filter(Boolean).join(' · ') || 'configured provider'
|
|
55
|
+
return (
|
|
56
|
+
<box
|
|
57
|
+
flexDirection="column"
|
|
58
|
+
alignItems="center"
|
|
59
|
+
justifyContent="center"
|
|
60
|
+
paddingTop={2}
|
|
61
|
+
paddingBottom={2}
|
|
62
|
+
>
|
|
63
|
+
<text fg={t.palette.primary}>🪄</text>
|
|
64
|
+
<box marginTop={1} flexDirection="row" gap={1}>
|
|
65
|
+
<text fg={t.palette.primary}>{frame}</text>
|
|
66
|
+
<text fg={t.palette.ink}>Generating commit message</text>
|
|
67
|
+
</box>
|
|
68
|
+
<text fg={t.muted}>via {providerLabel}</text>
|
|
69
|
+
<box marginTop={1}>
|
|
70
|
+
<text fg={t.muted}>Esc to cancel</text>
|
|
71
|
+
</box>
|
|
72
|
+
</box>
|
|
73
|
+
)
|
|
11
74
|
}
|
|
12
75
|
|
|
13
|
-
export function GitCommitModal({
|
|
76
|
+
export function GitCommitModal({
|
|
77
|
+
activeField,
|
|
78
|
+
assistant,
|
|
79
|
+
body,
|
|
80
|
+
cursorPos,
|
|
81
|
+
model,
|
|
82
|
+
stage,
|
|
83
|
+
title,
|
|
84
|
+
}: GitCommitModalProps) {
|
|
14
85
|
const t = useTokens()
|
|
15
86
|
const titleActive = activeField === 'title'
|
|
16
87
|
const bodyActive = activeField === 'body'
|
|
88
|
+
const isConfirm = stage === 'confirm'
|
|
89
|
+
const isGenerating = stage === 'generating'
|
|
90
|
+
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
91
|
+
const bgKind = useAppStore((s) => {
|
|
92
|
+
const id = s.currentSessionId
|
|
93
|
+
return id ? s.autoCommit.bySession[id]?.kind : undefined
|
|
94
|
+
})
|
|
95
|
+
const isBgGenerating = bgKind === 'generating'
|
|
96
|
+
const showBgSpinner = isBgGenerating && !isConfirm && !isGenerating
|
|
97
|
+
const bgSpinnerFrame = useSpinnerFrame(showBgSpinner)
|
|
98
|
+
const stagedCount = useAppStore(
|
|
99
|
+
(s) => s.gitPanel.files.filter((f) => f.section === 'staged').length
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const onAutoCommitClick = (): void => {
|
|
103
|
+
const hasTitle = title.trim().length > 0
|
|
104
|
+
if (hasTitle || !currentSessionId) {
|
|
105
|
+
dispatchGlobal({ type: 'git-commit-enter-confirm' })
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (bgKind === 'ready') {
|
|
109
|
+
dispatchGlobal({ sessionId: currentSessionId, type: 'git-commit-use-background-suggestion' })
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
dispatchGlobal({ sessionId: currentSessionId, type: 'git-commit-enter-generating' })
|
|
113
|
+
if (bgKind !== 'generating') {
|
|
114
|
+
runSideEffectGlobal({ sessionId: currentSessionId, type: 'generate-auto-commit-now' })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const modeId = pickModeId(stage)
|
|
119
|
+
const shellTitle = pickShellTitle(stage)
|
|
17
120
|
|
|
18
121
|
return (
|
|
19
|
-
<ModalShell title=
|
|
20
|
-
<
|
|
21
|
-
<text fg={titleActive ? t.palette.ink : t.muted}>Title</text>
|
|
22
|
-
<InputField
|
|
23
|
-
active={titleActive}
|
|
24
|
-
cursorPos={titleActive ? cursorPos : undefined}
|
|
25
|
-
value={title}
|
|
26
|
-
/>
|
|
27
|
-
</box>
|
|
122
|
+
<ModalShell title={shellTitle} keybindsModeId={modeId} width={uiTokens.modalWidth.xl}>
|
|
123
|
+
{isGenerating ? <GeneratingOverlay assistant={assistant} model={model} /> : null}
|
|
28
124
|
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
125
|
+
{isConfirm ? (
|
|
126
|
+
<box flexDirection="column">
|
|
127
|
+
{stagedCount > 0 ? (
|
|
128
|
+
<text fg={t.palette.warning}>
|
|
129
|
+
Commit will include the <strong>{stagedCount} staged file(s)</strong> only.
|
|
130
|
+
</text>
|
|
131
|
+
) : (
|
|
132
|
+
<text fg={t.palette.warning}>
|
|
133
|
+
<strong>git add -A</strong> will stage every change before committing.
|
|
134
|
+
</text>
|
|
135
|
+
)}
|
|
136
|
+
<text fg={t.muted}>Enter to confirm · Esc to cancel · edits below still apply.</text>
|
|
137
|
+
</box>
|
|
138
|
+
) : null}
|
|
139
|
+
|
|
140
|
+
{isGenerating ? null : (
|
|
141
|
+
<>
|
|
142
|
+
<box flexDirection="column">
|
|
143
|
+
<text fg={titleActive ? t.palette.ink : t.muted}>Title</text>
|
|
144
|
+
<InputField
|
|
145
|
+
active={titleActive}
|
|
146
|
+
cursorPos={titleActive ? cursorPos : undefined}
|
|
147
|
+
value={title}
|
|
148
|
+
/>
|
|
149
|
+
</box>
|
|
150
|
+
|
|
151
|
+
<box flexDirection="column">
|
|
152
|
+
<text fg={bodyActive ? t.palette.ink : t.muted}>Body (optional)</text>
|
|
153
|
+
<InputField
|
|
154
|
+
active={bodyActive}
|
|
155
|
+
cursorPos={bodyActive ? cursorPos : undefined}
|
|
156
|
+
value={body}
|
|
157
|
+
/>
|
|
158
|
+
</box>
|
|
159
|
+
</>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{isConfirm || isGenerating || !isAutoCommitEnabled() ? null : (
|
|
163
|
+
<box flexDirection="row" gap={1} marginTop={1} alignItems="center">
|
|
164
|
+
<box
|
|
165
|
+
border
|
|
166
|
+
borderColor={t.palette.primary}
|
|
167
|
+
paddingLeft={1}
|
|
168
|
+
paddingRight={1}
|
|
169
|
+
flexDirection="row"
|
|
170
|
+
gap={1}
|
|
171
|
+
alignItems="center"
|
|
172
|
+
onMouseDown={(event) => {
|
|
173
|
+
event.preventDefault()
|
|
174
|
+
event.stopPropagation()
|
|
175
|
+
onAutoCommitClick()
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
{showBgSpinner ? <text fg={t.palette.primary}>{bgSpinnerFrame}</text> : null}
|
|
179
|
+
<text fg={t.palette.primary}>
|
|
180
|
+
<strong>Auto-commit</strong>
|
|
181
|
+
</text>
|
|
182
|
+
</box>
|
|
183
|
+
<text fg={t.muted}>C-a · stages all changes (AI-suggests message if empty)</text>
|
|
184
|
+
</box>
|
|
185
|
+
)}
|
|
37
186
|
</ModalShell>
|
|
38
187
|
)
|
|
39
188
|
}
|
|
@@ -124,7 +124,7 @@ export function SessionBar() {
|
|
|
124
124
|
onMouseDragEnd={cancelDrag}
|
|
125
125
|
rightClickMenu={[
|
|
126
126
|
[
|
|
127
|
-
'Rename',
|
|
127
|
+
'Rename workspace',
|
|
128
128
|
() =>
|
|
129
129
|
dispatchGlobal({
|
|
130
130
|
initialName: session.name,
|
|
@@ -134,7 +134,7 @@ export function SessionBar() {
|
|
|
134
134
|
}),
|
|
135
135
|
],
|
|
136
136
|
[
|
|
137
|
-
'
|
|
137
|
+
'Delete workspace',
|
|
138
138
|
() => runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' }),
|
|
139
139
|
],
|
|
140
140
|
]}
|
|
@@ -31,8 +31,8 @@ function formatSessionLine(
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function getEmptyStateMessage(hasFilter: boolean): string {
|
|
34
|
-
if (hasFilter) return 'No matching
|
|
35
|
-
return 'No
|
|
34
|
+
if (hasFilter) return 'No matching workspaces.'
|
|
35
|
+
return 'No workspaces yet. Press Enter or n to create your first workspace.'
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function SessionPickerModal({
|
|
@@ -72,7 +72,7 @@ export function SessionPickerModal({
|
|
|
72
72
|
onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
|
|
73
73
|
title: (
|
|
74
74
|
<text fg={selectedIndex === filtered.length ? t.palette.ink : t.muted}>
|
|
75
|
-
Create new
|
|
75
|
+
Create new workspace
|
|
76
76
|
</text>
|
|
77
77
|
),
|
|
78
78
|
}
|
|
@@ -81,7 +81,7 @@ export function SessionPickerModal({
|
|
|
81
81
|
|
|
82
82
|
return (
|
|
83
83
|
<Picker
|
|
84
|
-
title="
|
|
84
|
+
title="Workspaces"
|
|
85
85
|
keybindsModeId="modal.session-picker.filtering"
|
|
86
86
|
width={uiTokens.modalWidth.lg}
|
|
87
87
|
gap={1}
|
|
@@ -63,7 +63,9 @@ const SidebarTop = memo(function SidebarTop({ contentWidth }: { contentWidth: nu
|
|
|
63
63
|
<text fg={tokens.palette.primary}>
|
|
64
64
|
<strong>aimux</strong>
|
|
65
65
|
</text>
|
|
66
|
-
<text fg={tokens.accent}>
|
|
66
|
+
<text fg={tokens.accent}>
|
|
67
|
+
{currentSession ? currentSession.name : 'No workspace selected'}
|
|
68
|
+
</text>
|
|
67
69
|
{branch ? (
|
|
68
70
|
<box flexDirection="row">
|
|
69
71
|
<text fg={tokens.palette.primary}>{'\u{e702}'} </text>
|
|
@@ -5,6 +5,7 @@ import { useAppStore } from '../../state/app-store'
|
|
|
5
5
|
import { useKeymap } from '../keymap-context'
|
|
6
6
|
import { getStatusBarModel } from '../status-bar-model'
|
|
7
7
|
import { getCurrentTokens, useBg, useTokens } from '../theme'
|
|
8
|
+
import { AIUsageIndicator } from './ai-usage-indicator'
|
|
8
9
|
|
|
9
10
|
function getModeColor(focusMode: AppState['focusMode']): string {
|
|
10
11
|
const t = getCurrentTokens()
|
|
@@ -65,6 +66,7 @@ export function StatusBar() {
|
|
|
65
66
|
<text fg={t.muted}>{model.right}</text>
|
|
66
67
|
<box flexDirection="row" gap={2}>
|
|
67
68
|
{model.help ? <text fg={t.muted}>{model.help}</text> : null}
|
|
69
|
+
<AIUsageIndicator />
|
|
68
70
|
<text fg={t.hover}>v{APP_VERSION}</text>
|
|
69
71
|
</box>
|
|
70
72
|
</box>
|
|
@@ -33,7 +33,7 @@ function getTitle(
|
|
|
33
33
|
focusMode: TerminalPaneProps['focusMode']
|
|
34
34
|
): string {
|
|
35
35
|
if (!tab) {
|
|
36
|
-
return 'No active
|
|
36
|
+
return 'No active workspace'
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if (isActive && focusMode === 'terminal-input') {
|
|
@@ -107,7 +107,7 @@ const TerminalViewport = memo(function TerminalViewport({
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
return (
|
|
110
|
-
<text fg={t.palette.ink}>{buffer.length > 0 ? buffer : 'Waiting for
|
|
110
|
+
<text fg={t.palette.ink}>{buffer.length > 0 ? buffer : 'Waiting for workspace output...'}</text>
|
|
111
111
|
)
|
|
112
112
|
})
|
|
113
113
|
|
|
@@ -239,6 +239,19 @@ export function TerminalPane({
|
|
|
239
239
|
<text fg={t.palette.primary}>Ctrl+n</text>
|
|
240
240
|
<text fg={t.muted}> to launch an assistant</text>
|
|
241
241
|
</box>
|
|
242
|
+
<box
|
|
243
|
+
flexDirection="row"
|
|
244
|
+
justifyContent="center"
|
|
245
|
+
marginTop={1}
|
|
246
|
+
paddingX={2}
|
|
247
|
+
backgroundColor={t.selected}
|
|
248
|
+
onMouseDown={(event) => {
|
|
249
|
+
event.stopPropagation()
|
|
250
|
+
dispatchGlobal({ type: 'open-new-tab-modal' })
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<text fg={t.palette.ink}>New assistant</text>
|
|
254
|
+
</box>
|
|
242
255
|
</box>
|
|
243
256
|
) : (
|
|
244
257
|
<box
|
|
@@ -258,7 +271,9 @@ export function TerminalPane({
|
|
|
258
271
|
)}
|
|
259
272
|
</ContextMenuBox>
|
|
260
273
|
{tab?.status === 'disconnected' ? (
|
|
261
|
-
<text fg={t.palette.warning}>
|
|
274
|
+
<text fg={t.palette.warning}>
|
|
275
|
+
Restored snapshot. Press Ctrl+r to restart this workspace.
|
|
276
|
+
</text>
|
|
262
277
|
) : null}
|
|
263
278
|
{tab?.errorMessage ? <text fg={t.palette.error}>{tab.errorMessage}</text> : null}
|
|
264
279
|
</box>
|