@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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getSnapshotTrees,
|
|
13
13
|
toTerminalContentSize,
|
|
14
14
|
} from '../state/layout-resize'
|
|
15
|
+
import { getSnapshotScrollIntents } from '../state/session-persistence'
|
|
15
16
|
|
|
16
17
|
export class LocalSessionBackend
|
|
17
18
|
extends EventEmitter<SessionBackendEvents>
|
|
@@ -66,15 +67,22 @@ export class LocalSessionBackend
|
|
|
66
67
|
})
|
|
67
68
|
this.currentSessionId = options.sessionId
|
|
68
69
|
const trees = getSnapshotTrees(options.workspaceSnapshot)
|
|
70
|
+
const intents = getSnapshotScrollIntents(options.workspaceSnapshot)
|
|
69
71
|
const splitTrees = trees.filter((t) => t.type === 'split')
|
|
70
72
|
if (splitTrees.length > 0) {
|
|
71
73
|
const bounds = createTerminalBounds(options.cols, options.rows)
|
|
72
74
|
forEachSplitPaneRect(splitTrees, bounds, (tabId, rect) => {
|
|
73
75
|
const size = toTerminalContentSize(rect)
|
|
74
|
-
this.sessionManager.resizeTab(
|
|
76
|
+
this.sessionManager.resizeTab(
|
|
77
|
+
options.sessionId,
|
|
78
|
+
tabId,
|
|
79
|
+
size.cols,
|
|
80
|
+
size.rows,
|
|
81
|
+
intents.get(tabId)
|
|
82
|
+
)
|
|
75
83
|
})
|
|
76
84
|
} else {
|
|
77
|
-
this.sessionManager.resize(options.sessionId, options.cols, options.rows)
|
|
85
|
+
this.sessionManager.resize(options.sessionId, options.cols, options.rows, intents)
|
|
78
86
|
}
|
|
79
87
|
const attachResult = this.sessionManager.attachSession(
|
|
80
88
|
options.sessionId,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AIUsageTool } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
3
|
+
import { useStore } from 'zustand'
|
|
4
|
+
import { createStore } from 'zustand/vanilla'
|
|
5
|
+
|
|
6
|
+
import type { UsageSnapshot } from '../services/ai-usage/types'
|
|
7
|
+
|
|
8
|
+
export interface AIUsageState {
|
|
9
|
+
enabled: boolean
|
|
10
|
+
snapshots: Partial<Record<AIUsageTool, UsageSnapshot>>
|
|
11
|
+
setEnabled: (enabled: boolean) => void
|
|
12
|
+
setSnapshot: (snap: UsageSnapshot) => void
|
|
13
|
+
clear: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const aiUsageStore = createStore<AIUsageState>((set) => ({
|
|
17
|
+
clear: () => set({ snapshots: {} }),
|
|
18
|
+
enabled: false,
|
|
19
|
+
setEnabled: (enabled: boolean) => set({ enabled }),
|
|
20
|
+
setSnapshot: (snap: UsageSnapshot) =>
|
|
21
|
+
set((state) => ({
|
|
22
|
+
snapshots: { ...state.snapshots, [snap.tool]: snap },
|
|
23
|
+
})),
|
|
24
|
+
snapshots: {},
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
export function useAIUsageStore<T>(selector: (state: AIUsageState) => T): T {
|
|
28
|
+
return useStore(aiUsageStore, selector)
|
|
29
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AppAction, AppState, AutoCommitState } from '../types'
|
|
2
|
+
|
|
3
|
+
function setBySession(
|
|
4
|
+
state: AutoCommitState,
|
|
5
|
+
sessionId: string,
|
|
6
|
+
next: AutoCommitState['bySession'][string]
|
|
7
|
+
): AutoCommitState {
|
|
8
|
+
return { bySession: { ...state.bySession, [sessionId]: next } }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function reduceAutoCommitState(
|
|
12
|
+
state: AutoCommitState,
|
|
13
|
+
action: AppAction
|
|
14
|
+
): AutoCommitState | null {
|
|
15
|
+
switch (action.type) {
|
|
16
|
+
case 'auto-commit-generation-started': {
|
|
17
|
+
return setBySession(state, action.sessionId, {
|
|
18
|
+
abortController: action.abortController,
|
|
19
|
+
kind: 'generating',
|
|
20
|
+
startedAt: action.startedAt,
|
|
21
|
+
tabId: action.tabId,
|
|
22
|
+
workingTreeHash: action.workingTreeHash,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
case 'auto-commit-generation-ready': {
|
|
26
|
+
const current = state.bySession[action.sessionId]
|
|
27
|
+
if (!current || current.kind !== 'generating') return null
|
|
28
|
+
if (current.workingTreeHash !== action.workingTreeHash) return null
|
|
29
|
+
return setBySession(state, action.sessionId, {
|
|
30
|
+
body: action.body,
|
|
31
|
+
generatedAt: action.generatedAt,
|
|
32
|
+
kind: 'ready',
|
|
33
|
+
tabId: current.tabId,
|
|
34
|
+
title: action.title,
|
|
35
|
+
workingTreeHash: current.workingTreeHash,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
case 'auto-commit-clear': {
|
|
39
|
+
const current = state.bySession[action.sessionId]
|
|
40
|
+
if (!current || current.kind === 'idle') return null
|
|
41
|
+
if (current.kind === 'generating') {
|
|
42
|
+
try {
|
|
43
|
+
current.abortController.abort()
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return setBySession(state, action.sessionId, { kind: 'idle' })
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function reduceAutoCommit(state: AppState, action: AppAction): AppState | null {
|
|
56
|
+
const next = reduceAutoCommitState(state.autoCommit, action)
|
|
57
|
+
if (next === null) return null
|
|
58
|
+
return { ...state, autoCommit: next }
|
|
59
|
+
}
|
|
@@ -7,6 +7,7 @@ import { getActiveKeymap } from '../../input/keymap/keymap-ref'
|
|
|
7
7
|
import { getAllAssistantOptions } from '../../pty/command-registry'
|
|
8
8
|
import { filterThemeIds } from '../../ui/filter-themes'
|
|
9
9
|
import { filterAssistants, filterSessions, filterSnippets } from '../selectors'
|
|
10
|
+
import { reduceAutoCommitState } from './auto-commit-state'
|
|
10
11
|
|
|
11
12
|
function emptyModal() {
|
|
12
13
|
return {
|
|
@@ -199,7 +200,8 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
199
200
|
type: 'update-available',
|
|
200
201
|
},
|
|
201
202
|
}
|
|
202
|
-
case 'open-git-commit-modal':
|
|
203
|
+
case 'open-git-commit-modal': {
|
|
204
|
+
const sessionId = action.sessionId
|
|
203
205
|
return {
|
|
204
206
|
...state,
|
|
205
207
|
focusMode: 'command-edit',
|
|
@@ -209,10 +211,112 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
209
211
|
cursorPos: 0,
|
|
210
212
|
editBuffer: '',
|
|
211
213
|
selectedIndex: 0,
|
|
212
|
-
sessionTargetId: null,
|
|
214
|
+
sessionTargetId: sessionId ?? null,
|
|
215
|
+
stage: 'edit',
|
|
213
216
|
type: 'git-commit',
|
|
214
217
|
},
|
|
215
218
|
}
|
|
219
|
+
}
|
|
220
|
+
case 'git-commit-enter-confirm': {
|
|
221
|
+
if (state.modal.type !== 'git-commit') return state
|
|
222
|
+
return {
|
|
223
|
+
...state,
|
|
224
|
+
focusMode: 'command-edit',
|
|
225
|
+
modal: { ...state.modal, stage: 'confirm' },
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
case 'git-commit-leave-confirm': {
|
|
229
|
+
if (state.modal.type !== 'git-commit') return state
|
|
230
|
+
return {
|
|
231
|
+
...state,
|
|
232
|
+
focusMode: 'command-edit',
|
|
233
|
+
modal: { ...state.modal, stage: 'edit' },
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
case 'git-commit-enter-generating': {
|
|
237
|
+
if (state.modal.type !== 'git-commit') return state
|
|
238
|
+
return {
|
|
239
|
+
...state,
|
|
240
|
+
focusMode: 'modal',
|
|
241
|
+
modal: { ...state.modal, sessionTargetId: action.sessionId, stage: 'generating' },
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
case 'git-commit-leave-generating': {
|
|
245
|
+
if (state.modal.type !== 'git-commit') return state
|
|
246
|
+
return {
|
|
247
|
+
...state,
|
|
248
|
+
focusMode: 'command-edit',
|
|
249
|
+
modal: { ...state.modal, stage: 'edit' },
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
case 'auto-commit-generation-ready': {
|
|
253
|
+
if (
|
|
254
|
+
state.modal.type !== 'git-commit' ||
|
|
255
|
+
state.modal.stage !== 'generating' ||
|
|
256
|
+
state.modal.sessionTargetId !== action.sessionId
|
|
257
|
+
) {
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
260
|
+
const nextAutoCommit = reduceAutoCommitState(state.autoCommit, action)
|
|
261
|
+
if (!nextAutoCommit) {
|
|
262
|
+
// Stale result (hash mismatch or slice was cleared mid-flight): don't
|
|
263
|
+
// strand the modal in `generating` — flip back to edit so the user
|
|
264
|
+
// isn't stuck staring at a spinner that will never resolve.
|
|
265
|
+
return {
|
|
266
|
+
...state,
|
|
267
|
+
focusMode: 'command-edit',
|
|
268
|
+
modal: { ...state.modal, stage: 'edit' },
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
...state,
|
|
273
|
+
autoCommit: nextAutoCommit,
|
|
274
|
+
focusMode: 'command-edit',
|
|
275
|
+
modal: {
|
|
276
|
+
...state.modal,
|
|
277
|
+
activeField: 'title',
|
|
278
|
+
contentBuffer: action.body,
|
|
279
|
+
cursorPos: action.title.length,
|
|
280
|
+
editBuffer: action.title,
|
|
281
|
+
stage: 'confirm',
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
case 'git-commit-use-background-suggestion': {
|
|
286
|
+
if (state.modal.type !== 'git-commit' || state.modal.sessionTargetId !== action.sessionId) {
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
const suggestion = state.autoCommit.bySession[action.sessionId]
|
|
290
|
+
if (!suggestion || suggestion.kind !== 'ready') return null
|
|
291
|
+
return {
|
|
292
|
+
...state,
|
|
293
|
+
focusMode: 'command-edit',
|
|
294
|
+
modal: {
|
|
295
|
+
...state.modal,
|
|
296
|
+
activeField: 'title',
|
|
297
|
+
contentBuffer: suggestion.body,
|
|
298
|
+
cursorPos: suggestion.title.length,
|
|
299
|
+
editBuffer: suggestion.title,
|
|
300
|
+
stage: 'confirm',
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
case 'auto-commit-clear': {
|
|
305
|
+
if (
|
|
306
|
+
state.modal.type !== 'git-commit' ||
|
|
307
|
+
state.modal.stage !== 'generating' ||
|
|
308
|
+
state.modal.sessionTargetId !== action.sessionId
|
|
309
|
+
) {
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
const nextAutoCommit = reduceAutoCommitState(state.autoCommit, action)
|
|
313
|
+
return {
|
|
314
|
+
...state,
|
|
315
|
+
autoCommit: nextAutoCommit ?? state.autoCommit,
|
|
316
|
+
focusMode: 'command-edit',
|
|
317
|
+
modal: { ...state.modal, stage: 'edit' },
|
|
318
|
+
}
|
|
319
|
+
}
|
|
216
320
|
case 'set-help-entry-count': {
|
|
217
321
|
if (state.modal.type !== 'help') return state
|
|
218
322
|
if (state.modal.entryCount === action.count) {
|
|
@@ -53,27 +53,39 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
|
|
|
53
53
|
),
|
|
54
54
|
}
|
|
55
55
|
case 'delete-session-record': {
|
|
56
|
+
const deletingCurrent = action.sessionId === state.currentSessionId
|
|
56
57
|
const newSessions = state.sessions.filter((session) => session.id !== action.sessionId)
|
|
57
|
-
const filteredNew = filterSessions(newSessions, state.modal.editBuffer)
|
|
58
|
-
const maxIndex = filteredNew.length
|
|
59
|
-
const clampedIndex = Math.min(state.modal.selectedIndex, maxIndex)
|
|
60
58
|
const nextStatuses = { ...state.sessionStatuses }
|
|
61
59
|
delete nextStatuses[action.sessionId]
|
|
60
|
+
if (action.openSessionPicker) {
|
|
61
|
+
const filteredNew = filterSessions(newSessions, state.modal.editBuffer)
|
|
62
|
+
const maxIndex = filteredNew.length
|
|
63
|
+
const clampedIndex = Math.min(state.modal.selectedIndex, maxIndex)
|
|
64
|
+
return {
|
|
65
|
+
...state,
|
|
66
|
+
activeTabId: deletingCurrent ? null : state.activeTabId,
|
|
67
|
+
currentSessionId: deletingCurrent ? null : state.currentSessionId,
|
|
68
|
+
focusMode: 'modal',
|
|
69
|
+
modal: {
|
|
70
|
+
editBuffer: null,
|
|
71
|
+
selectedIndex: clampedIndex,
|
|
72
|
+
sessionTargetId: null,
|
|
73
|
+
type: 'session-picker',
|
|
74
|
+
},
|
|
75
|
+
sessions: newSessions,
|
|
76
|
+
sessionStatuses: nextStatuses,
|
|
77
|
+
tabs: deletingCurrent ? [] : state.tabs,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
62
80
|
return {
|
|
63
81
|
...state,
|
|
64
|
-
activeTabId:
|
|
65
|
-
currentSessionId:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
modal: {
|
|
69
|
-
editBuffer: null,
|
|
70
|
-
selectedIndex: clampedIndex,
|
|
71
|
-
sessionTargetId: null,
|
|
72
|
-
type: 'session-picker',
|
|
73
|
-
},
|
|
82
|
+
activeTabId: deletingCurrent ? null : state.activeTabId,
|
|
83
|
+
currentSessionId: deletingCurrent ? null : state.currentSessionId,
|
|
84
|
+
focusMode: deletingCurrent ? 'navigation' : state.focusMode,
|
|
85
|
+
modal: deletingCurrent ? CLOSED_MODAL : state.modal,
|
|
74
86
|
sessions: newSessions,
|
|
75
87
|
sessionStatuses: nextStatuses,
|
|
76
|
-
tabs:
|
|
88
|
+
tabs: deletingCurrent ? [] : state.tabs,
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
91
|
case 'reorder-sessions': {
|
|
@@ -2,6 +2,7 @@ import { allLeafIds, createGroupId, type LayoutNode, pruneLayoutTree } from './l
|
|
|
2
2
|
import {
|
|
3
3
|
type AppState,
|
|
4
4
|
DEFAULT_SCROLL_INTENT,
|
|
5
|
+
type ScrollIntent,
|
|
5
6
|
type TabSession,
|
|
6
7
|
type TabStatus,
|
|
7
8
|
type WorkspaceSnapshotV1,
|
|
@@ -82,6 +83,18 @@ export function restoreTabsFromWorkspace(snapshot: WorkspaceSnapshotV1 | undefin
|
|
|
82
83
|
}))
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
export function getSnapshotScrollIntents(
|
|
87
|
+
snapshot: WorkspaceSnapshotV1 | undefined
|
|
88
|
+
): Map<string, ScrollIntent> {
|
|
89
|
+
if (!snapshot || snapshot.version !== 1) {
|
|
90
|
+
return new Map()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return new Map(
|
|
94
|
+
snapshot.tabs.map((tab) => [tab.id, tab.scrollIntent ?? DEFAULT_SCROLL_INTENT] as const)
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
85
98
|
export function restoreLayoutTrees(
|
|
86
99
|
snapshot: WorkspaceSnapshotV1 | undefined,
|
|
87
100
|
tabs: TabSession[]
|
|
@@ -192,11 +205,7 @@ export function restoreWorkspaceState(
|
|
|
192
205
|
activeTabId,
|
|
193
206
|
focusMode: 'navigation',
|
|
194
207
|
layoutTrees,
|
|
195
|
-
sidebar:
|
|
196
|
-
...state.sidebar,
|
|
197
|
-
visible: workspaceSnapshot?.sidebar.visible ?? state.sidebar.visible,
|
|
198
|
-
width: workspaceSnapshot?.sidebar.width ?? state.sidebar.width,
|
|
199
|
-
},
|
|
208
|
+
sidebar: state.sidebar,
|
|
200
209
|
tabGroupMap,
|
|
201
210
|
tabs: orderedTabs,
|
|
202
211
|
}
|
package/src/state/store.ts
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
AppAction,
|
|
3
|
-
AppState,
|
|
4
|
-
GitModeState,
|
|
5
|
-
GitPaneMode,
|
|
6
|
-
GitPanePosition,
|
|
7
|
-
GitPaneState,
|
|
8
|
-
SessionBarPosition,
|
|
9
|
-
SessionRecord,
|
|
10
|
-
SnippetRecord,
|
|
11
|
-
} from './types'
|
|
12
|
-
|
|
1
|
+
import { reduceAutoCommit } from './reducers/auto-commit-state'
|
|
13
2
|
import { emptyGitMode, reduceGitModeState } from './reducers/git-mode-state'
|
|
14
3
|
import { emptyGitPanel, reduceGitPanelState } from './reducers/git-panel-state'
|
|
15
4
|
import { emptyModal, reduceModalState } from './reducers/modal-state'
|
|
@@ -17,6 +6,18 @@ import { reduceSessionState } from './reducers/session-state'
|
|
|
17
6
|
import { reduceTabState } from './reducers/tab-state'
|
|
18
7
|
import { reduceUIState } from './reducers/ui-state'
|
|
19
8
|
import { filterSnippets } from './selectors'
|
|
9
|
+
import {
|
|
10
|
+
type AppAction,
|
|
11
|
+
type AppState,
|
|
12
|
+
EMPTY_AUTO_COMMIT_STATE,
|
|
13
|
+
type GitModeState,
|
|
14
|
+
type GitPaneMode,
|
|
15
|
+
type GitPanePosition,
|
|
16
|
+
type GitPaneState,
|
|
17
|
+
type SessionBarPosition,
|
|
18
|
+
type SessionRecord,
|
|
19
|
+
type SnippetRecord,
|
|
20
|
+
} from './types'
|
|
20
21
|
|
|
21
22
|
const DEFAULT_SIDEBAR_WIDTH = 28
|
|
22
23
|
const DEFAULT_SIDEBAR_MIN_WIDTH = 18
|
|
@@ -27,6 +28,7 @@ const DEFAULT_TERMINAL_ROWS = 24
|
|
|
27
28
|
export interface InitialStateOverrides {
|
|
28
29
|
gitMode?: Partial<GitModeState>
|
|
29
30
|
gitPane?: Partial<GitPaneState>
|
|
31
|
+
sidebar?: Pick<AppState['sidebar'], 'visible' | 'width'>
|
|
30
32
|
sessionBarVisible?: boolean
|
|
31
33
|
sessionBarPosition?: SessionBarPosition
|
|
32
34
|
}
|
|
@@ -52,6 +54,10 @@ function resolveGitPanePosition(mode: GitPaneMode, position: GitPanePosition): G
|
|
|
52
54
|
return position === 'left' || position === 'right' ? position : 'left'
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function clampSidebarWidth(width: number): number {
|
|
58
|
+
return Math.min(DEFAULT_SIDEBAR_MAX_WIDTH, Math.max(DEFAULT_SIDEBAR_MIN_WIDTH, width))
|
|
59
|
+
}
|
|
60
|
+
|
|
55
61
|
export function createInitialState(
|
|
56
62
|
customCommands: Record<string, string> = {},
|
|
57
63
|
sessions: SessionRecord[] = [],
|
|
@@ -66,6 +72,7 @@ export function createInitialState(
|
|
|
66
72
|
)
|
|
67
73
|
return {
|
|
68
74
|
activeTabId: null,
|
|
75
|
+
autoCommit: EMPTY_AUTO_COMMIT_STATE,
|
|
69
76
|
currentSessionId: null,
|
|
70
77
|
customCommands,
|
|
71
78
|
focusMode: showSessionPicker ? 'command-edit' : 'navigation',
|
|
@@ -101,8 +108,8 @@ export function createInitialState(
|
|
|
101
108
|
sidebar: {
|
|
102
109
|
maxWidth: DEFAULT_SIDEBAR_MAX_WIDTH,
|
|
103
110
|
minWidth: DEFAULT_SIDEBAR_MIN_WIDTH,
|
|
104
|
-
visible: true,
|
|
105
|
-
width: DEFAULT_SIDEBAR_WIDTH,
|
|
111
|
+
visible: overrides.sidebar?.visible ?? true,
|
|
112
|
+
width: clampSidebarWidth(overrides.sidebar?.width ?? DEFAULT_SIDEBAR_WIDTH),
|
|
106
113
|
},
|
|
107
114
|
snippets,
|
|
108
115
|
tabGroupMap: {},
|
|
@@ -129,6 +136,9 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
129
136
|
const gitModeState = reduceGitModeState(state, action)
|
|
130
137
|
if (gitModeState) return gitModeState
|
|
131
138
|
|
|
139
|
+
const autoCommitState = reduceAutoCommit(state, action)
|
|
140
|
+
if (autoCommitState) return autoCommitState
|
|
141
|
+
|
|
132
142
|
switch (action.type) {
|
|
133
143
|
case 'set-snippets':
|
|
134
144
|
return { ...state, snippets: action.snippets }
|
package/src/state/types.ts
CHANGED
|
@@ -62,6 +62,7 @@ export interface TerminalLine {
|
|
|
62
62
|
|
|
63
63
|
export interface TerminalSnapshot {
|
|
64
64
|
lines: TerminalLine[]
|
|
65
|
+
tailLines?: TerminalLine[]
|
|
65
66
|
viewportY: number
|
|
66
67
|
baseY: number
|
|
67
68
|
cursorVisible: boolean
|
|
@@ -323,6 +324,7 @@ export interface ModalGitCommit extends ModalBase {
|
|
|
323
324
|
type: 'git-commit'
|
|
324
325
|
activeField: 'title' | 'body'
|
|
325
326
|
contentBuffer: string
|
|
327
|
+
stage: 'edit' | 'generating' | 'confirm'
|
|
326
328
|
}
|
|
327
329
|
|
|
328
330
|
export interface ModalCreateSession extends ModalBase {
|
|
@@ -397,6 +399,7 @@ export interface AppState {
|
|
|
397
399
|
customCommands: Record<AssistantId, string>
|
|
398
400
|
gitPanel: GitPanelState
|
|
399
401
|
gitMode: GitModeState
|
|
402
|
+
autoCommit: AutoCommitState
|
|
400
403
|
/** Chord prefix the sequence resolver is currently waiting on, or null when idle. */
|
|
401
404
|
pendingChords: string[] | null
|
|
402
405
|
}
|
|
@@ -438,7 +441,7 @@ export type SessionAction =
|
|
|
438
441
|
| { type: 'set-sessions'; sessions: SessionRecord[] }
|
|
439
442
|
| { type: 'create-session-record'; session: SessionRecord }
|
|
440
443
|
| { type: 'rename-session-record'; sessionId: string; name: string }
|
|
441
|
-
| { type: 'delete-session-record'; sessionId: string }
|
|
444
|
+
| { type: 'delete-session-record'; sessionId: string; openSessionPicker?: boolean }
|
|
442
445
|
| { type: 'reorder-sessions'; orderedIds: string[] }
|
|
443
446
|
| { type: 'set-session-status'; sessionId: string; status: SessionStatus }
|
|
444
447
|
|
|
@@ -527,6 +530,50 @@ export type GitPanelAction =
|
|
|
527
530
|
| { type: 'git-refresh-error'; kind: GitPanelError }
|
|
528
531
|
| { type: 'git-panel-reset' }
|
|
529
532
|
|
|
533
|
+
// -- Auto-commit state & actions --
|
|
534
|
+
export type AutoCommitSuggestion =
|
|
535
|
+
| { kind: 'idle' }
|
|
536
|
+
| {
|
|
537
|
+
kind: 'generating'
|
|
538
|
+
tabId: string
|
|
539
|
+
workingTreeHash: string
|
|
540
|
+
abortController: AbortController
|
|
541
|
+
startedAt: number
|
|
542
|
+
}
|
|
543
|
+
| {
|
|
544
|
+
kind: 'ready'
|
|
545
|
+
tabId: string
|
|
546
|
+
workingTreeHash: string
|
|
547
|
+
title: string
|
|
548
|
+
body: string
|
|
549
|
+
generatedAt: number
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export interface AutoCommitState {
|
|
553
|
+
bySession: Record<string, AutoCommitSuggestion>
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export const EMPTY_AUTO_COMMIT_STATE: AutoCommitState = { bySession: {} }
|
|
557
|
+
|
|
558
|
+
export type AutoCommitAction =
|
|
559
|
+
| {
|
|
560
|
+
type: 'auto-commit-generation-started'
|
|
561
|
+
sessionId: string
|
|
562
|
+
tabId: string
|
|
563
|
+
workingTreeHash: string
|
|
564
|
+
abortController: AbortController
|
|
565
|
+
startedAt: number
|
|
566
|
+
}
|
|
567
|
+
| {
|
|
568
|
+
type: 'auto-commit-generation-ready'
|
|
569
|
+
sessionId: string
|
|
570
|
+
workingTreeHash: string
|
|
571
|
+
title: string
|
|
572
|
+
body: string
|
|
573
|
+
generatedAt: number
|
|
574
|
+
}
|
|
575
|
+
| { type: 'auto-commit-clear'; sessionId: string }
|
|
576
|
+
|
|
530
577
|
export type GitModeAction =
|
|
531
578
|
| { type: 'enter-git-mode' }
|
|
532
579
|
| { type: 'exit-git-mode' }
|
|
@@ -576,7 +623,12 @@ export type GitModeAction =
|
|
|
576
623
|
fromSection: GitFileSection
|
|
577
624
|
toSection: GitFileSection | null
|
|
578
625
|
}
|
|
579
|
-
| { type: 'open-git-commit-modal' }
|
|
626
|
+
| { type: 'open-git-commit-modal'; sessionId?: string }
|
|
627
|
+
| { type: 'git-commit-enter-confirm' }
|
|
628
|
+
| { type: 'git-commit-leave-confirm' }
|
|
629
|
+
| { type: 'git-commit-enter-generating'; sessionId: string }
|
|
630
|
+
| { type: 'git-commit-leave-generating' }
|
|
631
|
+
| { type: 'git-commit-use-background-suggestion'; sessionId: string }
|
|
580
632
|
|
|
581
633
|
// -- Data actions --
|
|
582
634
|
export type DataAction =
|
|
@@ -593,3 +645,4 @@ export type AppAction =
|
|
|
593
645
|
| DataAction
|
|
594
646
|
| GitPanelAction
|
|
595
647
|
| GitModeAction
|
|
648
|
+
| AutoCommitAction
|
|
@@ -35,6 +35,10 @@ export function saveCurrentWorkspace(state: AppState): void {
|
|
|
35
35
|
},
|
|
36
36
|
sessionBarPosition: state.sessionBar.position,
|
|
37
37
|
sessionBarVisible: state.sessionBar.visible,
|
|
38
|
+
sidebar: {
|
|
39
|
+
visible: state.sidebar.visible,
|
|
40
|
+
width: state.sidebar.width,
|
|
41
|
+
},
|
|
38
42
|
})
|
|
39
43
|
saveSessionCatalog(
|
|
40
44
|
buildSessionsWithCurrentSnapshot(state.sessions, state.currentSessionId, state)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface AIUsagePopoverState {
|
|
2
|
+
anchorX: number
|
|
3
|
+
anchorY: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type Listener = (state: AIUsagePopoverState | null) => void
|
|
7
|
+
|
|
8
|
+
let current: AIUsagePopoverState | null = null
|
|
9
|
+
const listeners = new Set<Listener>()
|
|
10
|
+
|
|
11
|
+
export function openAIUsagePopover(anchorX: number, anchorY: number): void {
|
|
12
|
+
current = { anchorX, anchorY }
|
|
13
|
+
for (const l of listeners) l(current)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function closeAIUsagePopover(): void {
|
|
17
|
+
if (current === null) return
|
|
18
|
+
current = null
|
|
19
|
+
for (const l of listeners) l(null)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function toggleAIUsagePopover(anchorX: number, anchorY: number): void {
|
|
23
|
+
if (current) {
|
|
24
|
+
closeAIUsagePopover()
|
|
25
|
+
} else {
|
|
26
|
+
openAIUsagePopover(anchorX, anchorY)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function subscribeAIUsagePopover(listener: Listener): () => void {
|
|
31
|
+
listeners.add(listener)
|
|
32
|
+
return () => {
|
|
33
|
+
listeners.delete(listener)
|
|
34
|
+
}
|
|
35
|
+
}
|