@brimveyn/aimux 1.7.3 → 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.
Files changed (62) hide show
  1. package/README.md +27 -15
  2. package/package.json +2 -2
  3. package/src/app-runtime/auto-commit-driver.ts +286 -0
  4. package/src/app-runtime/auto-commit-ref.ts +20 -0
  5. package/src/app-runtime/backend-attach-runtime.ts +3 -1
  6. package/src/app-runtime/session-actions.ts +7 -2
  7. package/src/app-runtime/side-effects.ts +111 -4
  8. package/src/app-runtime/split-drag-controller.ts +31 -1
  9. package/src/app-runtime/use-auto-commit-driver.ts +133 -0
  10. package/src/app-runtime/use-mouse-handlers.ts +93 -5
  11. package/src/app-runtime/use-terminal-resize.ts +24 -16
  12. package/src/app.tsx +71 -19
  13. package/src/auto-commit/default-auto-commit-prompt.md +46 -0
  14. package/src/auto-commit/headless-commands.ts +40 -0
  15. package/src/auto-commit/output-parser.ts +21 -0
  16. package/src/auto-commit/prompt-loader.ts +33 -0
  17. package/src/auto-commit/staging-mode.ts +5 -0
  18. package/src/auto-commit/strip-ansi.ts +13 -0
  19. package/src/auto-commit/suggestion-runner.ts +55 -0
  20. package/src/auto-commit/working-tree-hash.ts +24 -0
  21. package/src/config.ts +45 -2
  22. package/src/daemon/session-registry.ts +1 -0
  23. package/src/index.tsx +1 -1
  24. package/src/input/keymap/help-entries.ts +4 -4
  25. package/src/input/modes/bridge.ts +6 -0
  26. package/src/input/modes/transitions.ts +3 -1
  27. package/src/input/modes/types.ts +4 -0
  28. package/src/ipc/manager-protocol.ts +2 -2
  29. package/src/ipc/protocol.ts +2 -8
  30. package/src/pty/assistant-status-detector.ts +1 -1
  31. package/src/pty/terminal-snapshot.ts +38 -4
  32. package/src/services/ai-usage/adapters/claude.ts +139 -0
  33. package/src/services/ai-usage/adapters/codex.ts +191 -0
  34. package/src/services/ai-usage/provider.ts +84 -0
  35. package/src/services/ai-usage/spawn.ts +49 -0
  36. package/src/services/ai-usage/types.ts +20 -0
  37. package/src/session-backend/local-session-backend.ts +10 -2
  38. package/src/state/ai-usage-store.ts +29 -0
  39. package/src/state/git-pane-sizing.ts +15 -0
  40. package/src/state/reducers/auto-commit-state.ts +59 -0
  41. package/src/state/reducers/git-panel-state.ts +12 -7
  42. package/src/state/reducers/modal-state.ts +106 -2
  43. package/src/state/reducers/session-state.ts +26 -14
  44. package/src/state/reducers/ui-state.ts +6 -0
  45. package/src/state/session-persistence.ts +14 -5
  46. package/src/state/store.ts +26 -15
  47. package/src/state/types.ts +59 -3
  48. package/src/state/workspace-save.ts +6 -1
  49. package/src/ui/ai-usage/controller.ts +35 -0
  50. package/src/ui/components/ai-usage-indicator.tsx +131 -0
  51. package/src/ui/components/ai-usage-popover.tsx +152 -0
  52. package/src/ui/components/context-menu-overlay.tsx +4 -2
  53. package/src/ui/components/create-session-modal.tsx +2 -2
  54. package/src/ui/components/git-commit-modal.tsx +167 -18
  55. package/src/ui/components/git-pane-context-menu.ts +26 -0
  56. package/src/ui/components/session-bar.tsx +2 -2
  57. package/src/ui/components/session-picker-modal.tsx +4 -4
  58. package/src/ui/components/sidebar.tsx +117 -31
  59. package/src/ui/components/status-bar.tsx +2 -0
  60. package/src/ui/components/terminal-pane.tsx +18 -3
  61. package/src/ui/root.tsx +114 -13
  62. package/src/ui/status-bar-model.ts +1 -1
@@ -48,7 +48,7 @@ export function CreateSessionModal({
48
48
 
49
49
  return (
50
50
  <ModalShell
51
- title="Create session"
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}>Session name</text>
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({ activeField, body, cursorPos, title }: GitCommitModalProps) {
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="Commit" keybindsModeId="modal.git-commit" width={uiTokens.modalWidth.xl}>
20
- <box flexDirection="column">
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
- <box flexDirection="column">
30
- <text fg={bodyActive ? t.palette.ink : t.muted}>Body (optional)</text>
31
- <InputField
32
- active={bodyActive}
33
- cursorPos={bodyActive ? cursorPos : undefined}
34
- value={body}
35
- />
36
- </box>
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
  }
@@ -0,0 +1,26 @@
1
+ import type { GitPaneMode, GitPanePosition, GitPaneState } from '../../state/types'
2
+ import type { ContextMenuItem } from '../context-menu/controller'
3
+
4
+ const GIT_PANE_MENU_OPTIONS: Array<{
5
+ label: string
6
+ mode: GitPaneMode
7
+ position: GitPanePosition
8
+ }> = [
9
+ { label: 'Move to top', mode: 'embedded', position: 'top' },
10
+ { label: 'Move to bottom', mode: 'embedded', position: 'bottom' },
11
+ { label: 'Move to left', mode: 'pane', position: 'left' },
12
+ { label: 'Move to right', mode: 'pane', position: 'right' },
13
+ ]
14
+
15
+ export function buildGitPaneContextMenu(
16
+ gitPane: Pick<GitPaneState, 'mode' | 'position'>,
17
+ onToggle: () => void,
18
+ onMove: (mode: GitPaneMode, position: GitPanePosition) => void
19
+ ): ContextMenuItem[] {
20
+ return [
21
+ ['Toggle', onToggle],
22
+ ...GIT_PANE_MENU_OPTIONS.filter(
23
+ ({ mode, position }) => !(gitPane.mode === mode && gitPane.position === position)
24
+ ).map(({ label, mode, position }) => [label, () => onMove(mode, position)] as ContextMenuItem),
25
+ ]
26
+ }
@@ -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
- 'Close',
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 sessions.'
35
- return 'No sessions yet. Press Enter or n to create your first session.'
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 session
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="Sessions"
84
+ title="Workspaces"
85
85
  keybindsModeId="modal.session-picker.filtering"
86
86
  width={uiTokens.modalWidth.lg}
87
87
  gap={1}
@@ -1,10 +1,15 @@
1
- import { type ScrollBoxRenderable } from '@opentui/core'
1
+ import {
2
+ type BoxRenderable,
3
+ type MouseEvent as OtuiMouseEvent,
4
+ type ScrollBoxRenderable,
5
+ } from '@opentui/core'
2
6
  import { memo, useMemo, useRef } from 'react'
3
7
 
4
8
  import { useAppStore } from '../../state/app-store'
5
9
  import { dispatchGlobal } from '../../state/dispatch-ref'
6
10
  import { getCurrentTokens, type ThemeTokens, useBg, useTokens } from '../theme'
7
11
  import { ContextMenuBox } from './context-menu-box'
12
+ import { buildGitPaneContextMenu } from './git-pane-context-menu'
8
13
  import { GitPaneWidget } from './git-pane-widget'
9
14
  import { buildTabGroupInfo } from './sidebar-group-metadata'
10
15
  import { TabItem } from './tab-item'
@@ -13,12 +18,21 @@ import { useSidebarBranch } from './use-sidebar-branch'
13
18
 
14
19
  interface SidebarProps {
15
20
  onTabActivate?: (tabId: string) => void
21
+ onResizeDrag?: (event: OtuiMouseEvent) => boolean
22
+ onResizeDragEnd?: () => void
23
+ onSidebarResizeStart?: (info: { initialWidth: number; screenStart: number }) => void
24
+ onEmbeddedGitResizeStart?: (info: {
25
+ containerStart: number
26
+ position: 'top' | 'bottom'
27
+ totalSize: number
28
+ }) => void
16
29
  }
17
30
 
18
31
  const GUTTER_START = '╭'
19
32
  const GUTTER_MIDDLE = '├'
20
33
  const GUTTER_END = '╰'
21
34
  const GUTTER_PAD = '│'
35
+ const RESIZE_HANDLE = '│'
22
36
 
23
37
  function getRowBackground({
24
38
  alternate,
@@ -34,9 +48,8 @@ function getRowBackground({
34
48
  return undefined
35
49
  }
36
50
 
37
- const SidebarTop = memo(function SidebarTop() {
51
+ const SidebarTop = memo(function SidebarTop({ contentWidth }: { contentWidth: number }) {
38
52
  const tokens = useTokens()
39
- const sidebarWidth = useAppStore((s) => s.sidebar.width)
40
53
  const currentSessionId = useAppStore((s) => s.currentSessionId)
41
54
  const sessions = useAppStore((s) => s.sessions)
42
55
  const currentSession = currentSessionId
@@ -50,7 +63,9 @@ const SidebarTop = memo(function SidebarTop() {
50
63
  <text fg={tokens.palette.primary}>
51
64
  <strong>aimux</strong>
52
65
  </text>
53
- <text fg={tokens.accent}>{currentSession ? currentSession.name : 'No session selected'}</text>
66
+ <text fg={tokens.accent}>
67
+ {currentSession ? currentSession.name : 'No workspace selected'}
68
+ </text>
54
69
  {branch ? (
55
70
  <box flexDirection="row">
56
71
  <text fg={tokens.palette.primary}>{'\u{e702}'} </text>
@@ -70,7 +85,7 @@ const SidebarTop = memo(function SidebarTop() {
70
85
  >
71
86
  <text fg={tokens.palette.ink}>+ New assistant</text>
72
87
  </box>
73
- <text fg={tokens.hover}>{'·'.repeat(Math.max(0, sidebarWidth - 2))}</text>
88
+ <text fg={tokens.hover}>{'·'.repeat(Math.max(0, contentWidth - 2))}</text>
74
89
  </box>
75
90
  )
76
91
  })
@@ -164,13 +179,20 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
164
179
  )
165
180
  })
166
181
 
167
- export function Sidebar({ onTabActivate }: SidebarProps) {
182
+ export function Sidebar({
183
+ onEmbeddedGitResizeStart,
184
+ onResizeDrag,
185
+ onResizeDragEnd,
186
+ onSidebarResizeStart,
187
+ onTabActivate,
188
+ }: SidebarProps) {
168
189
  const tokens = useTokens()
169
190
  const sidebarBg = useBg('elevated')
170
191
  const sidebarVisible = useAppStore((s) => s.sidebar.visible)
171
192
  const sidebarWidth = useAppStore((s) => s.sidebar.width)
172
193
  const gitPane = useAppStore((s) => s.gitPane)
173
194
  const focusMode = useAppStore((s) => s.focusMode)
195
+ const bodyRef = useRef<BoxRenderable | null>(null)
174
196
 
175
197
  if (!sidebarVisible) {
176
198
  return null
@@ -179,15 +201,51 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
179
201
  const gitEmbedded = gitPane.mode === 'embedded' && gitPane.visible
180
202
  const gitOnTop = gitEmbedded && gitPane.position === 'top'
181
203
  const gitOnBottom = gitEmbedded && gitPane.position === 'bottom'
204
+ const contentWidth = Math.max(1, sidebarWidth - 1)
205
+ const gitPaneMenu = buildGitPaneContextMenu(
206
+ gitPane,
207
+ () => dispatchGlobal({ type: 'toggle-git-pane' }),
208
+ (mode, position) => {
209
+ dispatchGlobal({ mode, type: 'set-git-pane-mode' })
210
+ dispatchGlobal({ position, type: 'set-git-pane-position' })
211
+ }
212
+ )
182
213
 
183
214
  // flex-grow scaled by 100 (integer preferred); tabs gets (1-ratio), git gets ratio.
184
- const tabsGrow = gitEmbedded ? Math.max(1, Math.round((1 - gitPane.ratio) * 100)) : 1
185
- const gitGrow = gitEmbedded ? Math.max(1, Math.round(gitPane.ratio * 100)) : 0
215
+ const tabsGrow = gitEmbedded ? Math.max(1, Math.round((1 - gitPane.embeddedRatio) * 100)) : 1
216
+ const gitGrow = gitEmbedded ? Math.max(1, Math.round(gitPane.embeddedRatio * 100)) : 0
186
217
 
187
- const separator = <text fg={tokens.hover}>{'·'.repeat(Math.max(0, sidebarWidth - 2))}</text>
218
+ const separator = <text fg={tokens.hover}>{'·'.repeat(Math.max(0, contentWidth - 2))}</text>
188
219
  const gitBody = gitEmbedded ? (
189
- <box flexDirection="column" flexGrow={gitGrow} flexShrink={1} flexBasis={0} overflow="hidden">
220
+ <ContextMenuBox
221
+ flexDirection="column"
222
+ flexGrow={gitGrow}
223
+ flexShrink={1}
224
+ flexBasis={0}
225
+ overflow="hidden"
226
+ rightClickMenu={gitPaneMenu}
227
+ >
190
228
  <GitPaneWidget pollingEnabled={gitPane.visible} />
229
+ </ContextMenuBox>
230
+ ) : null
231
+ const embeddedHandle = gitEmbedded ? (
232
+ <box
233
+ minHeight={1}
234
+ flexShrink={0}
235
+ backgroundColor={tokens.border}
236
+ onMouseDown={(event) => {
237
+ const body = bodyRef.current
238
+ if (!body) return
239
+ event.preventDefault()
240
+ event.stopPropagation()
241
+ onEmbeddedGitResizeStart?.({
242
+ containerStart: body.y,
243
+ position: gitPane.position === 'top' ? 'top' : 'bottom',
244
+ totalSize: Math.max(1, body.height),
245
+ })
246
+ }}
247
+ >
248
+ <text fg={tokens.border}>{RESIZE_HANDLE.repeat(Math.max(1, contentWidth))}</text>
191
249
  </box>
192
250
  ) : null
193
251
 
@@ -198,6 +256,7 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
198
256
  flexDirection="column"
199
257
  backgroundColor={sidebarBg}
200
258
  gap={0}
259
+ overflow="hidden"
201
260
  rightClickMenu={[
202
261
  ['Hide sidebar', () => dispatchGlobal({ type: 'toggle-sidebar' })],
203
262
  ['Toggle git pane', () => dispatchGlobal({ type: 'toggle-git-pane' })],
@@ -214,29 +273,56 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
214
273
  dispatchGlobal({ focusMode: 'navigation', type: 'set-focus-mode' })
215
274
  }
216
275
  }}
276
+ onMouseDrag={(event) => {
277
+ if (onResizeDrag?.(event)) {
278
+ event.preventDefault()
279
+ event.stopPropagation()
280
+ }
281
+ }}
282
+ onMouseUp={() => {
283
+ onResizeDragEnd?.()
284
+ }}
217
285
  >
218
- <SidebarTop />
219
- {gitOnTop ? (
220
- <>
221
- {gitBody}
222
- {separator}
223
- </>
224
- ) : null}
225
- <box
226
- flexDirection="column"
227
- flexGrow={tabsGrow}
228
- flexShrink={1}
229
- flexBasis={0}
230
- overflow="hidden"
231
- >
232
- <TabsBody onTabActivate={onTabActivate} />
286
+ <box flexDirection="row" width={sidebarWidth} flexGrow={1} overflow="hidden">
287
+ <box width={contentWidth} flexGrow={1} flexDirection="column" overflow="hidden">
288
+ <SidebarTop contentWidth={contentWidth} />
289
+ <box ref={bodyRef} flexDirection="column" flexGrow={1} overflow="hidden">
290
+ {gitOnTop ? (
291
+ <>
292
+ {gitBody}
293
+ {embeddedHandle}
294
+ </>
295
+ ) : null}
296
+ <box
297
+ flexDirection="column"
298
+ flexGrow={tabsGrow}
299
+ flexShrink={1}
300
+ flexBasis={0}
301
+ overflow="hidden"
302
+ >
303
+ <TabsBody onTabActivate={onTabActivate} />
304
+ </box>
305
+ {gitOnBottom ? (
306
+ <>
307
+ {embeddedHandle}
308
+ {gitBody}
309
+ </>
310
+ ) : null}
311
+ </box>
312
+ {!gitEmbedded ? separator : null}
313
+ </box>
314
+ <box
315
+ height="100%"
316
+ width={1}
317
+ flexShrink={0}
318
+ backgroundColor={tokens.border}
319
+ onMouseDown={(event) => {
320
+ event.preventDefault()
321
+ event.stopPropagation()
322
+ onSidebarResizeStart?.({ initialWidth: sidebarWidth, screenStart: event.x })
323
+ }}
324
+ />
233
325
  </box>
234
- {gitOnBottom ? (
235
- <>
236
- {separator}
237
- {gitBody}
238
- </>
239
- ) : null}
240
326
  </ContextMenuBox>
241
327
  )
242
328
  }
@@ -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 session'
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 session output...'}</text>
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}>Restored snapshot. Press Ctrl+r to restart this session.</text>
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>