@brimveyn/aimux 1.6.2 → 1.7.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 (61) hide show
  1. package/README.md +3 -0
  2. package/package.json +2 -2
  3. package/src/app-runtime/backend-attach-runtime.ts +6 -0
  4. package/src/app-runtime/backend-runtime-events.ts +21 -21
  5. package/src/app-runtime/side-effects.ts +24 -12
  6. package/src/app-runtime/use-backend-runtime.ts +2 -20
  7. package/src/app-runtime/use-directory-search.ts +6 -1
  8. package/src/app.tsx +2 -1
  9. package/src/config.ts +9 -0
  10. package/src/daemon/daemon.ts +197 -15
  11. package/src/daemon/session-manager.ts +9 -0
  12. package/src/daemon/session-registry.ts +5 -5
  13. package/src/index.tsx +10 -2
  14. package/src/input/keymap/help-entries.ts +5 -5
  15. package/src/input/modes/bridge.ts +5 -8
  16. package/src/input/modes/transitions.ts +15 -23
  17. package/src/input/modes/types.ts +2 -5
  18. package/src/ipc/protocol.ts +49 -5
  19. package/src/platform/project-search.ts +45 -12
  20. package/src/pty/assistant-status-detection-loop.ts +192 -0
  21. package/src/pty/assistant-status-detector.ts +226 -0
  22. package/src/pty/pty-manager.ts +3 -37
  23. package/src/session-backend/bootstrap.ts +4 -1
  24. package/src/session-backend/local-session-backend.ts +43 -56
  25. package/src/session-backend/remote-session-backend.ts +15 -0
  26. package/src/session-backend/types.ts +10 -1
  27. package/src/state/reducers/modal-state.ts +91 -134
  28. package/src/state/reducers/session-state.ts +13 -6
  29. package/src/state/reducers/tab-state.ts +0 -10
  30. package/src/state/selectors.ts +12 -0
  31. package/src/state/session-persistence.ts +20 -15
  32. package/src/state/store.ts +11 -3
  33. package/src/state/types.ts +29 -13
  34. package/src/ui/breaking-update-screen.tsx +31 -0
  35. package/src/ui/components/bare-input.tsx +44 -0
  36. package/src/ui/components/create-session-modal.tsx +16 -8
  37. package/src/ui/components/diff-renderer/fold-strip.tsx +5 -13
  38. package/src/ui/components/diff-renderer/split-view.tsx +11 -17
  39. package/src/ui/components/diff-renderer/stacked-view.tsx +7 -14
  40. package/src/ui/components/git-panel.tsx +10 -3
  41. package/src/ui/components/git-view.tsx +4 -8
  42. package/src/ui/components/help-modal.tsx +45 -160
  43. package/src/ui/components/input-field.tsx +6 -2
  44. package/src/ui/components/list-item.tsx +36 -28
  45. package/src/ui/components/modal-shell.tsx +51 -13
  46. package/src/ui/components/new-tab-modal.tsx +78 -45
  47. package/src/ui/components/picker.tsx +179 -0
  48. package/src/ui/components/session-bar.tsx +47 -21
  49. package/src/ui/components/session-picker-modal.tsx +58 -56
  50. package/src/ui/components/sidebar.tsx +49 -22
  51. package/src/ui/components/snippet-picker-modal.tsx +43 -34
  52. package/src/ui/components/status-bar.tsx +3 -2
  53. package/src/ui/components/surface.tsx +11 -8
  54. package/src/ui/components/tab-item.tsx +38 -22
  55. package/src/ui/components/terminal-pane.tsx +8 -8
  56. package/src/ui/components/theme-picker-modal.tsx +51 -91
  57. package/src/ui/root.tsx +14 -23
  58. package/src/ui/status-bar-model.ts +5 -5
  59. package/src/ui/theme-store.ts +26 -2
  60. package/src/ui/theme.ts +9 -1
  61. package/src/ui/components/modal-filter-bar.tsx +0 -19
@@ -1,13 +1,12 @@
1
1
  import type { SessionRecord } from '../../state/types'
2
2
 
3
+ import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
3
4
  import { filterSessions } from '../../state/selectors'
4
5
  import { abbreviatePath } from '../path-format'
5
6
  import { orderSessionsForDisplay } from '../session-ordering'
6
7
  import { useTheme } from '../theme'
7
8
  import { uiTokens } from '../ui-tokens'
8
- import { ListItem } from './list-item'
9
- import { ModalFilterBar } from './modal-filter-bar'
10
- import { ModalShell } from './modal-shell'
9
+ import { Picker, type PickerItem } from './picker'
11
10
 
12
11
  interface SessionPickerModalProps {
13
12
  sessions: SessionRecord[]
@@ -15,6 +14,7 @@ interface SessionPickerModalProps {
15
14
  currentSessionId: string | null
16
15
  currentTabCount: number
17
16
  filter: string | null
17
+ cursorPos?: number
18
18
  }
19
19
 
20
20
  function formatSessionLine(
@@ -41,6 +41,7 @@ function getEmptyStateMessage(hasFilter: boolean): string {
41
41
  export function SessionPickerModal({
42
42
  currentSessionId,
43
43
  currentTabCount,
44
+ cursorPos,
44
45
  filter,
45
46
  selectedIndex,
46
47
  sessions,
@@ -50,62 +51,63 @@ export function SessionPickerModal({
50
51
  const baselineOrder = ordered.map((s) => s.id)
51
52
  const filtered = filterSessions(ordered, filter)
52
53
  const hasFilter = !!filter
53
- const showFilteredEmptyState = filtered.length === 0 && sessions.length > 0
54
- const showInitialEmptyState = filtered.length === 0 && sessions.length === 0
54
+
55
+ const sessionItems: PickerItem[] = filtered.map((session, index) => {
56
+ const active = index === selectedIndex
57
+ const displayIndex = baselineOrder.indexOf(session.id) + 1
58
+ return {
59
+ key: session.id,
60
+ onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
61
+ onDelete: () => runSideEffectGlobal({ type: 'delete-selected-session' }),
62
+ subtitle: session.projectPath ? (
63
+ <text fg={theme.colors['descriptionForeground']}>
64
+ {abbreviatePath(session.projectPath)}
65
+ </text>
66
+ ) : undefined,
67
+ title: (
68
+ <text
69
+ fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
70
+ >
71
+ {formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
72
+ </text>
73
+ ),
74
+ }
75
+ })
76
+
77
+ const createNewItem: PickerItem = {
78
+ key: '__create-new__',
79
+ onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
80
+ title: (
81
+ <text
82
+ fg={
83
+ selectedIndex === filtered.length
84
+ ? theme.colors['editor.foreground']
85
+ : theme.colors['descriptionForeground']
86
+ }
87
+ >
88
+ Create new session
89
+ </text>
90
+ ),
91
+ }
92
+
93
+ const items = [...sessionItems, createNewItem]
55
94
 
56
95
  return (
57
- <ModalShell
96
+ <Picker
58
97
  title="Sessions"
59
- keybindsModeId="modal.session-picker"
98
+ keybindsModeId="modal.session-picker.filtering"
60
99
  width={uiTokens.modalWidth.lg}
61
- footer={<ModalFilterBar filter={filter} />}
62
- >
63
- {showFilteredEmptyState ? (
64
- <text fg={theme.colors['descriptionForeground']}>{getEmptyStateMessage(hasFilter)}</text>
65
- ) : null}
66
- {showInitialEmptyState ? (
67
- <text fg={theme.colors['descriptionForeground']}>{getEmptyStateMessage(false)}</text>
68
- ) : null}
69
- {filtered.map((session, index) => {
70
- const active = index === selectedIndex
71
- const displayIndex = baselineOrder.indexOf(session.id) + 1
72
- return (
73
- <ListItem
74
- key={session.id}
75
- active={active}
76
- title={
77
- <text
78
- fg={
79
- active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
80
- }
81
- >
82
- {formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
83
- </text>
84
- }
85
- subtitle={
86
- session.projectPath ? (
87
- <text fg={theme.colors['descriptionForeground']}>
88
- {abbreviatePath(session.projectPath)}
89
- </text>
90
- ) : undefined
91
- }
92
- />
93
- )
94
- })}
95
- <ListItem
96
- active={selectedIndex === filtered.length}
97
- title={
98
- <text
99
- fg={
100
- selectedIndex === filtered.length
101
- ? theme.colors['editor.foreground']
102
- : theme.colors['descriptionForeground']
103
- }
104
- >
105
- Create new session
106
- </text>
107
- }
108
- />
109
- </ModalShell>
100
+ gap={1}
101
+ filter={filter}
102
+ cursorPos={cursorPos}
103
+ items={items}
104
+ selectedIndex={selectedIndex}
105
+ emptyState={
106
+ filtered.length === 0 ? (
107
+ <text fg={theme.colors['descriptionForeground']}>{getEmptyStateMessage(hasFilter)}</text>
108
+ ) : undefined
109
+ }
110
+ onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
111
+ />
110
112
  )
111
113
  }
@@ -1,9 +1,11 @@
1
- import type { ScrollBoxRenderable } from '@opentui/core'
1
+ import type { Theme } from '@brimveyn/aimux-config'
2
2
 
3
+ import { type ScrollBoxRenderable } from '@opentui/core'
3
4
  import { memo, useMemo, useRef } from 'react'
4
5
 
5
6
  import { useAppStore } from '../../state/app-store'
6
- import { getCurrentTheme, useTheme } from '../theme'
7
+ import { dispatchGlobal } from '../../state/dispatch-ref'
8
+ import { getCurrentTheme, useBg, useTheme } from '../theme'
7
9
  import { GitPaneWidget } from './git-pane-widget'
8
10
  import { buildTabGroupInfo } from './sidebar-group-metadata'
9
11
  import { TabItem } from './tab-item'
@@ -19,6 +21,20 @@ const GUTTER_MIDDLE = '├'
19
21
  const GUTTER_END = '╰'
20
22
  const GUTTER_PAD = '│'
21
23
 
24
+ function getRowBackground({
25
+ alternate,
26
+ isActive,
27
+ theme,
28
+ }: {
29
+ isActive: boolean
30
+ alternate: boolean
31
+ theme: Theme
32
+ }): string | undefined {
33
+ if (isActive) return theme.colors['list.activeSelectionBackground']
34
+ if (alternate) return theme.colors['editor.lineHighlightBackground']
35
+ return undefined
36
+ }
37
+
22
38
  const SidebarTop = memo(function SidebarTop() {
23
39
  const theme = useTheme()
24
40
  const sidebarWidth = useAppStore((s) => s.sidebar.width)
@@ -44,6 +60,19 @@ const SidebarTop = memo(function SidebarTop() {
44
60
  <text fg={theme.colors['descriptionForeground']}>{branch}</text>
45
61
  </box>
46
62
  ) : null}
63
+ <box
64
+ flexDirection="row"
65
+ paddingY={1}
66
+ backgroundColor={theme.colors['list.activeSelectionBackground']}
67
+ justifyContent="center"
68
+ marginTop={1}
69
+ onMouseDown={(e) => {
70
+ e.stopPropagation()
71
+ dispatchGlobal({ type: 'open-new-tab-modal' })
72
+ }}
73
+ >
74
+ <text fg={theme.colors['editor.foreground']}>+ New assistant</text>
75
+ </box>
47
76
  <text fg={theme.colors['editor.lineHighlightBackground']}>
48
77
  {'·'.repeat(Math.max(0, sidebarWidth - 2))}
49
78
  </text>
@@ -51,25 +80,14 @@ const SidebarTop = memo(function SidebarTop() {
51
80
  )
52
81
  })
53
82
 
54
- function renderGroupGutter(
55
- isGroupStart: boolean,
56
- isGroupMiddle: boolean,
57
- isGroupEnd: boolean,
58
- isActive: boolean
59
- ) {
83
+ function renderGroupGutter(isGroupStart: boolean, isGroupMiddle: boolean, isGroupEnd: boolean) {
60
84
  return (
61
85
  <box flexDirection="column" width={1} overflow="hidden">
62
- <text
63
- fg={getCurrentTheme().colors['terminal.ansiMagenta']}
64
- bg={isActive ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined}
65
- >
86
+ <text fg={getCurrentTheme().colors['terminal.ansiMagenta']}>
66
87
  {/* oxlint-disable-next-line no-nested-ternary */}
67
88
  {isGroupStart ? GUTTER_START : isGroupMiddle ? GUTTER_MIDDLE : GUTTER_PAD}
68
89
  </text>
69
- <text
70
- fg={getCurrentTheme().colors['terminal.ansiMagenta']}
71
- bg={isActive ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined}
72
- >
90
+ <text fg={getCurrentTheme().colors['terminal.ansiMagenta']}>
73
91
  {isGroupEnd ? GUTTER_END : GUTTER_PAD}
74
92
  </text>
75
93
  </box>
@@ -116,6 +134,7 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
116
134
  ) : (
117
135
  tabs.map((tab, index) => {
118
136
  const isActive = tab.id === activeTabId
137
+ const alternate = index % 2 === 1
119
138
  const info = tabGroupInfo.get(tab.id)
120
139
  const inLayout = !!info?.inLayout
121
140
  const inGroup = info ? index >= info.groupStart && index <= info.groupEnd : false
@@ -126,19 +145,20 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
126
145
  return (
127
146
  <box
128
147
  key={tab.id}
148
+ backgroundColor={getRowBackground({ alternate, isActive, theme })}
129
149
  flexDirection="row"
130
- onMouseDown={onTabActivate ? () => onTabActivate(tab.id) : undefined}
150
+ onMouseDown={(event) => {
151
+ event.stopPropagation()
152
+ onTabActivate?.(tab.id)
153
+ }}
131
154
  >
132
- {inGroup
133
- ? renderGroupGutter(isGroupStart, isGroupMiddle, isGroupEnd, isActive)
134
- : null}
155
+ {inGroup ? renderGroupGutter(isGroupStart, isGroupMiddle, isGroupEnd) : null}
135
156
  <box flexGrow={1}>
136
157
  <TabItem
137
158
  id={`sidebar-tab-${tab.id}`}
138
159
  tab={tab}
139
160
  active={isActive}
140
161
  focused={focusMode === 'navigation'}
141
- isFocusedInput={isActive && focusMode === 'terminal-input'}
142
162
  inLayout={inLayout}
143
163
  />
144
164
  </box>
@@ -152,9 +172,11 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
152
172
 
153
173
  export function Sidebar({ onTabActivate }: SidebarProps) {
154
174
  const theme = useTheme()
175
+ const sidebarBg = useBg('sideBar.background')
155
176
  const sidebarVisible = useAppStore((s) => s.sidebar.visible)
156
177
  const sidebarWidth = useAppStore((s) => s.sidebar.width)
157
178
  const gitPane = useAppStore((s) => s.gitPane)
179
+ const focusMode = useAppStore((s) => s.focusMode)
158
180
 
159
181
  if (!sidebarVisible) {
160
182
  return null
@@ -184,8 +206,13 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
184
206
  width={sidebarWidth}
185
207
  padding={0}
186
208
  flexDirection="column"
187
- backgroundColor={theme.colors['sideBar.background']}
209
+ backgroundColor={sidebarBg}
188
210
  gap={0}
211
+ onMouseDown={() => {
212
+ if (focusMode === 'terminal-input') {
213
+ dispatchGlobal({ focusMode: 'navigation', type: 'set-focus-mode' })
214
+ }
215
+ }}
189
216
  >
190
217
  <SidebarTop />
191
218
  {gitOnTop ? (
@@ -1,16 +1,16 @@
1
1
  import type { SnippetRecord } from '../../state/types'
2
2
 
3
+ import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
3
4
  import { filterSnippets } from '../../state/selectors'
4
5
  import { useTheme } from '../theme'
5
6
  import { uiTokens } from '../ui-tokens'
6
- import { ListItem } from './list-item'
7
- import { ModalFilterBar } from './modal-filter-bar'
8
- import { ModalShell } from './modal-shell'
7
+ import { Picker, type PickerItem } from './picker'
9
8
 
10
9
  interface SnippetPickerModalProps {
11
10
  snippets: SnippetRecord[]
12
11
  selectedIndex: number
13
12
  filter: string | null
13
+ cursorPos?: number
14
14
  }
15
15
 
16
16
  const MAX_PREVIEW_LENGTH = 60
@@ -21,45 +21,54 @@ function truncateContent(content: string): string {
21
21
  return `${normalized.slice(0, MAX_PREVIEW_LENGTH - 3)}...`
22
22
  }
23
23
 
24
- export function SnippetPickerModal({ filter, selectedIndex, snippets }: SnippetPickerModalProps) {
24
+ export function SnippetPickerModal({
25
+ cursorPos,
26
+ filter,
27
+ selectedIndex,
28
+ snippets,
29
+ }: SnippetPickerModalProps) {
25
30
  const theme = useTheme()
26
31
  const filtered = filterSnippets(snippets, filter)
27
32
 
33
+ const items: PickerItem[] = filtered.map((snippet, index) => {
34
+ const active = index === selectedIndex
35
+ return {
36
+ key: snippet.id,
37
+ onClick: () => {
38
+ dispatchGlobal({ type: 'close-modal' })
39
+ runSideEffectGlobal({ type: 'paste-selected-snippet' })
40
+ },
41
+ onDelete: () => runSideEffectGlobal({ type: 'delete-selected-snippet' }),
42
+ onEdit: () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
43
+ subtitle: (
44
+ <text fg={theme.colors['descriptionForeground']}>{truncateContent(snippet.content)}</text>
45
+ ),
46
+ title: (
47
+ <text
48
+ fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
49
+ >
50
+ <strong>{snippet.name}</strong>
51
+ </text>
52
+ ),
53
+ }
54
+ })
55
+
28
56
  return (
29
- <ModalShell
57
+ <Picker
30
58
  title="Snippets"
31
- keybindsModeId="modal.snippet-picker"
59
+ keybindsModeId="modal.snippet-picker.filtering"
32
60
  width={uiTokens.modalWidth.xl}
33
- footer={<ModalFilterBar filter={filter} />}
34
- >
35
- {filtered.length === 0 ? (
61
+ gap={1}
62
+ filter={filter}
63
+ cursorPos={cursorPos}
64
+ items={items}
65
+ selectedIndex={selectedIndex}
66
+ emptyState={
36
67
  <text fg={theme.colors['descriptionForeground']}>
37
68
  {filter ? 'No matching snippets.' : 'No snippets yet. Press n to create one.'}
38
69
  </text>
39
- ) : null}
40
- {filtered.map((snippet, index) => {
41
- const active = index === selectedIndex
42
- return (
43
- <ListItem
44
- key={snippet.id}
45
- active={active}
46
- title={
47
- <text
48
- fg={
49
- active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
50
- }
51
- >
52
- <strong>{snippet.name}</strong>
53
- </text>
54
- }
55
- subtitle={
56
- <text fg={theme.colors['descriptionForeground']}>
57
- {truncateContent(snippet.content)}
58
- </text>
59
- }
60
- />
61
- )
62
- })}
63
- </ModalShell>
70
+ }
71
+ onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
72
+ />
64
73
  )
65
74
  }
@@ -4,7 +4,7 @@ import { version as APP_VERSION } from '../../../package.json'
4
4
  import { useAppStore } from '../../state/app-store'
5
5
  import { useKeymap } from '../keymap-context'
6
6
  import { getStatusBarModel } from '../status-bar-model'
7
- import { getCurrentTheme, useTheme } from '../theme'
7
+ import { getCurrentTheme, useBg, useTheme } from '../theme'
8
8
 
9
9
  function getModeColor(focusMode: AppState['focusMode']): string {
10
10
  const t = getCurrentTheme()
@@ -41,6 +41,7 @@ function getModeLabel(focusMode: AppState['focusMode']): string {
41
41
 
42
42
  export function StatusBar() {
43
43
  const theme = useTheme()
44
+ const headerBg = useBg('sideBarSectionHeader.background')
44
45
  const state = useAppStore((s) => s)
45
46
  const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId)
46
47
  const config = useKeymap()
@@ -54,7 +55,7 @@ export function StatusBar() {
54
55
  paddingTop={0}
55
56
  paddingBottom={0}
56
57
  flexDirection="column"
57
- backgroundColor={theme.colors['sideBarSectionHeader.background']}
58
+ backgroundColor={headerBg}
58
59
  >
59
60
  <box width="100%" flexDirection="row">
60
61
  <text fg={getModeColor(state.focusMode)}>[{getModeLabel(state.focusMode)}]</text>
@@ -1,22 +1,24 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import { getCurrentTheme } from '../theme'
3
+ import type { ThemeColorMap } from '../themes'
4
+
5
+ import { useBg } from '../theme'
4
6
 
5
7
  type SurfaceTone = 'muted' | 'elevated' | 'selected' | 'input' | 'inputActive'
6
8
 
7
- function getSurfaceColor(tone: SurfaceTone): string {
9
+ function toneToColorKey(tone: SurfaceTone): keyof ThemeColorMap {
8
10
  switch (tone) {
9
11
  case 'elevated':
10
- return getCurrentTheme().colors['sideBar.background']
12
+ return 'sideBar.background'
11
13
  case 'selected':
12
- return getCurrentTheme().colors['list.activeSelectionBackground']
14
+ return 'list.activeSelectionBackground'
13
15
  case 'input':
14
- return getCurrentTheme().colors['editor.background']
16
+ return 'editor.background'
15
17
  case 'inputActive':
16
- return getCurrentTheme().colors['list.activeSelectionBackground']
18
+ return 'list.activeSelectionBackground'
17
19
  case 'muted':
18
20
  default:
19
- return getCurrentTheme().colors['sideBarSectionHeader.background']
21
+ return 'sideBarSectionHeader.background'
20
22
  }
21
23
  }
22
24
 
@@ -45,9 +47,10 @@ export function Surface({
45
47
  tone = 'muted',
46
48
  width,
47
49
  }: SurfaceProps) {
50
+ const bg = useBg(toneToColorKey(tone))
48
51
  return (
49
52
  <box
50
- backgroundColor={getSurfaceColor(tone)}
53
+ backgroundColor={bg}
51
54
  flexDirection={flexDirection}
52
55
  gap={gap}
53
56
  padding={padding}
@@ -1,5 +1,8 @@
1
+ import { useState } from 'react'
2
+
1
3
  import type { TabSession } from '../../state/types'
2
4
 
5
+ import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
3
6
  import { useBusySpinner } from '../hooks/use-busy-spinner'
4
7
  import { getCurrentTheme, useTheme } from '../theme'
5
8
 
@@ -8,7 +11,6 @@ interface TabItemProps {
8
11
  tab: TabSession
9
12
  active: boolean
10
13
  focused: boolean
11
- isFocusedInput: boolean
12
14
  inLayout?: boolean
13
15
  }
14
16
 
@@ -20,8 +22,6 @@ function getStatusColor(status: TabSession['status']): string {
20
22
  return getCurrentTheme().colors['editorWarning.foreground']
21
23
  case 'error':
22
24
  return getCurrentTheme().colors['editorError.foreground']
23
- case 'exited':
24
- return getCurrentTheme().colors['editorWarning.foreground']
25
25
  default:
26
26
  return getCurrentTheme().colors['descriptionForeground']
27
27
  }
@@ -50,10 +50,15 @@ function getIndicatorColor(active: boolean, focused: boolean, inLayout: boolean)
50
50
  function BusyIndicator() {
51
51
  const theme = useTheme()
52
52
  const frame = useBusySpinner()
53
- return <text fg={theme.colors['textLink.foreground']}>{frame} busy</text>
53
+ return <text fg={theme.colors['textLink.foreground']}>{frame} working</text>
54
+ }
55
+
56
+ function WaitingIndicator() {
57
+ const theme = useTheme()
58
+ return <text fg={theme.colors['editorWarning.foreground']}>? waiting</text>
54
59
  }
55
60
 
56
- function ActivityIndicator({ isFocusedInput, tab }: { tab: TabSession; isFocusedInput: boolean }) {
61
+ function ActivityIndicator({ tab }: { tab: TabSession }) {
57
62
  const theme = useTheme()
58
63
  if (tab.status === 'error') {
59
64
  return <text fg={theme.colors['editorError.foreground']}>✗ error</text>
@@ -63,16 +68,12 @@ function ActivityIndicator({ isFocusedInput, tab }: { tab: TabSession; isFocused
63
68
  return <text fg={theme.colors['editorWarning.foreground']}>⏸ restore</text>
64
69
  }
65
70
 
66
- if (tab.status === 'exited') {
67
- return <text fg={theme.colors['editorWarning.foreground']}>⏹ exited</text>
71
+ if (tab.activity === 'working') {
72
+ return <BusyIndicator />
68
73
  }
69
74
 
70
- if (isFocusedInput) {
71
- return <text fg={theme.colors['focusBorder']}>▸ focused</text>
72
- }
73
-
74
- if (tab.activity === 'busy') {
75
- return <BusyIndicator />
75
+ if (tab.activity === 'waiting-input') {
76
+ return <WaitingIndicator />
76
77
  }
77
78
 
78
79
  if (tab.activity === 'idle') {
@@ -82,12 +83,13 @@ function ActivityIndicator({ isFocusedInput, tab }: { tab: TabSession; isFocused
82
83
  return <text fg={getStatusColor(tab.status)}>{tab.status}</text>
83
84
  }
84
85
 
85
- export function TabItem({ active, focused, id, inLayout, isFocusedInput, tab }: TabItemProps) {
86
+ export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
86
87
  const theme = useTheme()
87
88
  const label = tab.command.split(' ')[0]
88
89
  const isInLayout = inLayout ?? false
89
90
  const indicator = getIndicator(active, focused, isInLayout)
90
91
  const indicatorColor = getIndicatorColor(active, focused, isInLayout)
92
+ const [hovered, setHovered] = useState(false)
91
93
 
92
94
  return (
93
95
  <box
@@ -96,21 +98,35 @@ export function TabItem({ active, focused, id, inLayout, isFocusedInput, tab }:
96
98
  paddingRight={1}
97
99
  paddingTop={0}
98
100
  paddingBottom={0}
99
- backgroundColor={active ? theme.colors['list.activeSelectionBackground'] : undefined}
100
101
  flexDirection="column"
101
102
  gap={0}
103
+ onMouseOver={() => setHovered(true)}
104
+ onMouseOut={() => setHovered(false)}
102
105
  >
103
- <box flexDirection="row">
106
+ <box flexDirection="row" alignItems="center">
104
107
  <text fg={indicatorColor}>{indicator} </text>
105
- <text
106
- fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
107
- >
108
- {tab.title}
109
- </text>
108
+ <box flexGrow={1}>
109
+ <text
110
+ fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
111
+ >
112
+ {tab.title}
113
+ </text>
114
+ </box>
115
+ {hovered ? (
116
+ <box
117
+ onMouseDown={(event) => {
118
+ event.stopPropagation()
119
+ dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
120
+ runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
121
+ }}
122
+ >
123
+ <text fg={theme.colors['descriptionForeground']}>×</text>
124
+ </box>
125
+ ) : null}
110
126
  </box>
111
127
  <box flexDirection="row">
112
128
  <text fg={theme.colors['descriptionForeground']}> {label} </text>
113
- <ActivityIndicator tab={tab} isFocusedInput={isFocusedInput} />
129
+ <ActivityIndicator tab={tab} />
114
130
  </box>
115
131
  </box>
116
132
  )
@@ -6,7 +6,7 @@ import type { TerminalContentOrigin } from '../../input/raw-input-handler'
6
6
  import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../state/types'
7
7
 
8
8
  import { logInputDebug } from '../../debug/input-log'
9
- import { getCurrentTheme, useTheme } from '../theme'
9
+ import { getCurrentTheme, useBg, useTheme } from '../theme'
10
10
 
11
11
  interface TerminalPaneProps {
12
12
  tab?: TabSession
@@ -125,6 +125,7 @@ export function TerminalPane({
125
125
  tabId,
126
126
  }: TerminalPaneProps) {
127
127
  const theme = useTheme()
128
+ const editorBg = useBg('editor.background')
128
129
  const paneIsActive = isActive ?? true
129
130
  const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
130
131
  const canUseLocalScrollback = focusMode === 'terminal-input' && !!tab && localScrollbackEnabled
@@ -185,7 +186,8 @@ export function TerminalPane({
185
186
  padding={0}
186
187
  flexDirection="column"
187
188
  flexGrow={1}
188
- backgroundColor={theme.colors['editor.background']}
189
+ backgroundColor={editorBg}
190
+ onMouseDown={forwardMouseEvent}
189
191
  onMouseDrag={forwardMouseEvent}
190
192
  onMouseScroll={forwardScrollEvent}
191
193
  onMouseUp={forwardMouseEvent}
@@ -205,7 +207,10 @@ export function TerminalPane({
205
207
  flexDirection="column"
206
208
  flexGrow={1}
207
209
  width="100%"
208
- onMouseDown={forwardMouseEvent}
210
+ onMouseDown={(e) => {
211
+ e.stopPropagation()
212
+ forwardMouseEvent(e)
213
+ }}
209
214
  onMouseUp={forwardMouseEvent}
210
215
  onMouseDrag={forwardMouseEvent}
211
216
  onMouseScroll={forwardScrollEvent}
@@ -214,11 +219,6 @@ export function TerminalPane({
214
219
  </box>
215
220
  )}
216
221
  </box>
217
- {tab?.status === 'exited' && tab.exitCode !== undefined ? (
218
- <text fg={theme.colors['editorWarning.foreground']}>
219
- Process exited with code {tab.exitCode}
220
- </text>
221
- ) : null}
222
222
  {tab?.status === 'disconnected' ? (
223
223
  <text fg={theme.colors['editorWarning.foreground']}>
224
224
  Restored snapshot. Press Ctrl+r to restart this session.