@brimveyn/aimux 1.6.1 → 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.
- package/README.md +3 -0
- package/package.json +2 -2
- package/src/app-runtime/backend-attach-runtime.ts +6 -0
- package/src/app-runtime/backend-runtime-events.ts +21 -21
- package/src/app-runtime/side-effects.ts +43 -12
- package/src/app-runtime/use-backend-runtime.ts +2 -20
- package/src/app-runtime/use-directory-search.ts +6 -1
- package/src/app.tsx +6 -1
- package/src/config.ts +28 -1
- package/src/daemon/daemon.ts +197 -15
- package/src/daemon/session-manager.ts +9 -0
- package/src/daemon/session-registry.ts +5 -5
- package/src/git/diff-hash.ts +10 -0
- package/src/index.tsx +10 -2
- package/src/input/keymap/help-entries.ts +5 -5
- package/src/input/modes/bridge.ts +5 -8
- package/src/input/modes/transitions.ts +15 -23
- package/src/input/modes/types.ts +3 -5
- package/src/ipc/protocol.ts +49 -5
- package/src/platform/project-search.ts +45 -12
- package/src/pty/assistant-status-detection-loop.ts +192 -0
- package/src/pty/assistant-status-detector.ts +226 -0
- package/src/pty/pty-manager.ts +3 -37
- package/src/session-backend/bootstrap.ts +4 -1
- package/src/session-backend/local-session-backend.ts +43 -56
- package/src/session-backend/remote-session-backend.ts +15 -0
- package/src/session-backend/types.ts +10 -1
- package/src/state/git-tree.ts +42 -16
- package/src/state/reducers/diff-cache.ts +64 -0
- package/src/state/reducers/git-mode-state.ts +91 -33
- package/src/state/reducers/git-panel-state.ts +33 -2
- package/src/state/reducers/modal-state.ts +91 -134
- package/src/state/reducers/session-state.ts +13 -6
- package/src/state/reducers/tab-state.ts +0 -10
- package/src/state/selectors.ts +12 -0
- package/src/state/session-persistence.ts +20 -15
- package/src/state/store.ts +13 -3
- package/src/state/types.ts +87 -14
- package/src/ui/breaking-update-screen.tsx +31 -0
- package/src/ui/components/bare-input.tsx +44 -0
- package/src/ui/components/create-session-modal.tsx +16 -8
- package/src/ui/components/diff-renderer/fold-strip.tsx +5 -13
- package/src/ui/components/diff-renderer/pierre-diff.tsx +9 -31
- package/src/ui/components/diff-renderer/prepare-diff.ts +66 -0
- package/src/ui/components/diff-renderer/split-view.tsx +35 -16
- package/src/ui/components/diff-renderer/stacked-view.tsx +30 -12
- package/src/ui/components/diff-renderer/use-diff-prefetch.ts +191 -0
- package/src/ui/components/diff-renderer/use-diff-preparation.ts +106 -0
- package/src/ui/components/git-pane-widget.tsx +2 -0
- package/src/ui/components/git-panel.tsx +27 -10
- package/src/ui/components/git-view.tsx +23 -9
- package/src/ui/components/help-modal.tsx +45 -160
- package/src/ui/components/input-field.tsx +6 -2
- package/src/ui/components/list-item.tsx +36 -28
- package/src/ui/components/modal-shell.tsx +51 -13
- package/src/ui/components/new-tab-modal.tsx +78 -45
- package/src/ui/components/picker.tsx +179 -0
- package/src/ui/components/session-bar.tsx +47 -21
- package/src/ui/components/session-picker-modal.tsx +58 -56
- package/src/ui/components/sidebar.tsx +49 -22
- package/src/ui/components/snippet-picker-modal.tsx +43 -34
- package/src/ui/components/status-bar.tsx +3 -2
- package/src/ui/components/surface.tsx +11 -8
- package/src/ui/components/tab-item.tsx +38 -22
- package/src/ui/components/terminal-pane.tsx +8 -8
- package/src/ui/components/theme-picker-modal.tsx +51 -91
- package/src/ui/root.tsx +14 -23
- package/src/ui/status-bar-model.ts +5 -5
- package/src/ui/theme-store.ts +26 -2
- package/src/ui/theme.ts +9 -1
- package/src/ui/components/modal-filter-bar.tsx +0 -19
|
@@ -5,6 +5,7 @@ import type { SessionBackend, SessionBackendEvents } from './types'
|
|
|
5
5
|
|
|
6
6
|
import { SessionManager } from '../daemon/session-manager'
|
|
7
7
|
import { logDebug } from '../debug/input-log'
|
|
8
|
+
import { runStatusDetectionLoop } from '../pty/assistant-status-detection-loop'
|
|
8
9
|
import {
|
|
9
10
|
createTerminalBounds,
|
|
10
11
|
forEachSplitPaneRect,
|
|
@@ -12,16 +13,13 @@ import {
|
|
|
12
13
|
toTerminalContentSize,
|
|
13
14
|
} from '../state/layout-resize'
|
|
14
15
|
|
|
15
|
-
const SESSION_IDLE_TIMEOUT_MS = 2_000
|
|
16
|
-
|
|
17
16
|
export class LocalSessionBackend
|
|
18
17
|
extends EventEmitter<SessionBackendEvents>
|
|
19
18
|
implements SessionBackend
|
|
20
19
|
{
|
|
21
20
|
private readonly sessionManager = new SessionManager()
|
|
22
21
|
private currentSessionId: string | null = null
|
|
23
|
-
private readonly
|
|
24
|
-
private readonly sessionBusy = new Map<string, boolean>()
|
|
22
|
+
private readonly statusLoop: ReturnType<typeof runStatusDetectionLoop>
|
|
25
23
|
|
|
26
24
|
constructor() {
|
|
27
25
|
super()
|
|
@@ -29,7 +27,6 @@ export class LocalSessionBackend
|
|
|
29
27
|
if (sessionId === this.currentSessionId) {
|
|
30
28
|
this.emit('render', tabId, viewport, terminalModes)
|
|
31
29
|
}
|
|
32
|
-
this.markSessionBusy(sessionId)
|
|
33
30
|
})
|
|
34
31
|
this.sessionManager.on('exit', (sessionId, tabId, exitCode) => {
|
|
35
32
|
if (sessionId === this.currentSessionId) {
|
|
@@ -41,23 +38,18 @@ export class LocalSessionBackend
|
|
|
41
38
|
this.emit('error', tabId, message)
|
|
42
39
|
}
|
|
43
40
|
})
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.sessionBusy.set(sessionId, false)
|
|
57
|
-
this.emit('sessionActivity', sessionId, false)
|
|
58
|
-
}
|
|
59
|
-
}, SESSION_IDLE_TIMEOUT_MS)
|
|
60
|
-
this.sessionIdleTimers.set(sessionId, timer)
|
|
41
|
+
this.statusLoop = runStatusDetectionLoop({
|
|
42
|
+
listSessions: () => this.sessionManager.listSessionIds(),
|
|
43
|
+
listTabs: (sessionId) => this.sessionManager.listTabs(sessionId),
|
|
44
|
+
onSessionStatus: (sessionId, status) => {
|
|
45
|
+
this.emit('sessionActivity', sessionId, status)
|
|
46
|
+
},
|
|
47
|
+
onTabStatus: (tabId, status, sessionId) => {
|
|
48
|
+
if (sessionId === this.currentSessionId) {
|
|
49
|
+
this.emit('tabActivity', tabId, status)
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
})
|
|
61
53
|
}
|
|
62
54
|
|
|
63
55
|
async attach(options: {
|
|
@@ -84,7 +76,24 @@ export class LocalSessionBackend
|
|
|
84
76
|
} else {
|
|
85
77
|
this.sessionManager.resize(options.sessionId, options.cols, options.rows)
|
|
86
78
|
}
|
|
87
|
-
|
|
79
|
+
const attachResult = this.sessionManager.attachSession(
|
|
80
|
+
options.sessionId,
|
|
81
|
+
options.workspaceSnapshot
|
|
82
|
+
)
|
|
83
|
+
// Run a synchronous classification pass so every tab's activity and the
|
|
84
|
+
// session-status snapshot are available to embed in the reply — mirrors
|
|
85
|
+
// the remote backend's behavior and keeps hydrate dispatches atomic on
|
|
86
|
+
// the client side.
|
|
87
|
+
this.statusLoop.classifyNow(options.sessionId, this.sessionManager.listTabs(options.sessionId))
|
|
88
|
+
const tabsWithActivity = attachResult.tabs.map((tab) => ({
|
|
89
|
+
...tab,
|
|
90
|
+
activity: this.statusLoop.getTabStatus(tab.id) ?? tab.activity,
|
|
91
|
+
}))
|
|
92
|
+
return {
|
|
93
|
+
activeTabId: attachResult.activeTabId,
|
|
94
|
+
initialSessionStatuses: this.statusLoop.snapshotSessions(),
|
|
95
|
+
tabs: tabsWithActivity,
|
|
96
|
+
}
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
createSession(options: {
|
|
@@ -123,30 +132,22 @@ export class LocalSessionBackend
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
scrollViewport(tabId: string, deltaLines: number): void {
|
|
126
|
-
if (!this.currentSessionId)
|
|
127
|
-
return
|
|
128
|
-
}
|
|
135
|
+
if (!this.currentSessionId) return
|
|
129
136
|
this.sessionManager.scroll(this.currentSessionId, tabId, deltaLines)
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
scrollViewportToBottom(tabId: string): void {
|
|
133
|
-
if (!this.currentSessionId)
|
|
134
|
-
return
|
|
135
|
-
}
|
|
140
|
+
if (!this.currentSessionId) return
|
|
136
141
|
this.sessionManager.scrollToBottom(this.currentSessionId, tabId)
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
140
|
-
if (!this.currentSessionId)
|
|
141
|
-
return
|
|
142
|
-
}
|
|
145
|
+
if (!this.currentSessionId) return
|
|
143
146
|
this.sessionManager.reapplyScrollIntent(this.currentSessionId, tabId, intent)
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
setActiveTab(tabId: string | null): void {
|
|
147
|
-
if (!this.currentSessionId)
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
+
if (!this.currentSessionId) return
|
|
150
151
|
logDebug('backend.local.setActiveTab', { sessionId: this.currentSessionId, tabId })
|
|
151
152
|
this.sessionManager.setActiveTab(this.currentSessionId, tabId)
|
|
152
153
|
}
|
|
@@ -157,9 +158,7 @@ export class LocalSessionBackend
|
|
|
157
158
|
intents?: Map<string, ScrollIntent>,
|
|
158
159
|
options?: { sync?: boolean }
|
|
159
160
|
): void {
|
|
160
|
-
if (!this.currentSessionId)
|
|
161
|
-
return
|
|
162
|
-
}
|
|
161
|
+
if (!this.currentSessionId) return
|
|
163
162
|
this.sessionManager.resize(this.currentSessionId, cols, rows, intents, options)
|
|
164
163
|
}
|
|
165
164
|
|
|
@@ -170,40 +169,28 @@ export class LocalSessionBackend
|
|
|
170
169
|
intent?: ScrollIntent,
|
|
171
170
|
options?: { sync?: boolean }
|
|
172
171
|
): void {
|
|
173
|
-
if (!this.currentSessionId)
|
|
174
|
-
return
|
|
175
|
-
}
|
|
172
|
+
if (!this.currentSessionId) return
|
|
176
173
|
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows, intent, options)
|
|
177
174
|
}
|
|
178
175
|
|
|
179
176
|
disposeSession(tabId: string): void {
|
|
180
|
-
if (!this.currentSessionId)
|
|
181
|
-
return
|
|
182
|
-
}
|
|
177
|
+
if (!this.currentSessionId) return
|
|
183
178
|
logDebug('backend.local.disposeSession', { sessionId: this.currentSessionId, tabId })
|
|
184
179
|
this.sessionManager.closeTab(this.currentSessionId, tabId)
|
|
185
180
|
}
|
|
186
181
|
|
|
187
182
|
disposeAll(): void {
|
|
188
|
-
if (!this.currentSessionId)
|
|
189
|
-
return
|
|
190
|
-
}
|
|
183
|
+
if (!this.currentSessionId) return
|
|
191
184
|
logDebug('backend.local.disposeAll', { sessionId: this.currentSessionId })
|
|
192
185
|
this.sessionManager.disposeSession(this.currentSessionId)
|
|
193
186
|
}
|
|
194
187
|
|
|
195
188
|
destroy(keepSessions = true): void {
|
|
196
189
|
logDebug('backend.local.destroy', { keepSessions, sessionId: this.currentSessionId })
|
|
197
|
-
if (!keepSessions) {
|
|
198
|
-
|
|
199
|
-
this.sessionManager.disposeSession(this.currentSessionId)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
for (const timer of this.sessionIdleTimers.values()) {
|
|
203
|
-
clearTimeout(timer)
|
|
190
|
+
if (!keepSessions && this.currentSessionId) {
|
|
191
|
+
this.sessionManager.disposeSession(this.currentSessionId)
|
|
204
192
|
}
|
|
205
|
-
this.
|
|
206
|
-
this.sessionBusy.clear()
|
|
193
|
+
this.statusLoop.stop()
|
|
207
194
|
this.currentSessionId = null
|
|
208
195
|
}
|
|
209
196
|
}
|
|
@@ -161,6 +161,21 @@ export class RemoteSessionBackend
|
|
|
161
161
|
case 'tabError':
|
|
162
162
|
this.emit('error', message.payload.tabId, message.payload.message)
|
|
163
163
|
break
|
|
164
|
+
case 'tabStatus':
|
|
165
|
+
logDebug('backend.remote.tabStatus', {
|
|
166
|
+
sessionId: message.payload.sessionId,
|
|
167
|
+
status: message.payload.status,
|
|
168
|
+
tabId: message.payload.tabId,
|
|
169
|
+
})
|
|
170
|
+
this.emit('tabActivity', message.payload.tabId, message.payload.status)
|
|
171
|
+
break
|
|
172
|
+
case 'sessionStatus':
|
|
173
|
+
logDebug('backend.remote.sessionStatus', {
|
|
174
|
+
sessionId: message.payload.sessionId,
|
|
175
|
+
status: message.payload.status,
|
|
176
|
+
})
|
|
177
|
+
this.emit('sessionActivity', message.payload.sessionId, message.payload.status)
|
|
178
|
+
break
|
|
164
179
|
}
|
|
165
180
|
}
|
|
166
181
|
|
|
@@ -2,6 +2,8 @@ import type { EventEmitter } from 'node:events'
|
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
ScrollIntent,
|
|
5
|
+
SessionStatus,
|
|
6
|
+
TabActivity,
|
|
5
7
|
TabSession,
|
|
6
8
|
TerminalModeState,
|
|
7
9
|
TerminalSnapshot,
|
|
@@ -12,12 +14,19 @@ export type SessionBackendEvents = {
|
|
|
12
14
|
render: [tabId: string, viewport: TerminalSnapshot, terminalModes: TerminalModeState]
|
|
13
15
|
exit: [tabId: string, exitCode: number]
|
|
14
16
|
error: [tabId: string, message: string]
|
|
15
|
-
sessionActivity: [sessionId: string,
|
|
17
|
+
sessionActivity: [sessionId: string, status: SessionStatus]
|
|
18
|
+
tabActivity: [tabId: string, activity: TabActivity]
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export interface BackendAttachResult {
|
|
19
22
|
tabs: TabSession[]
|
|
20
23
|
activeTabId: string | null
|
|
24
|
+
/**
|
|
25
|
+
* Per-session status snapshot taken at attach time and applied
|
|
26
|
+
* atomically with tab hydration to prevent an unknown-tab race on
|
|
27
|
+
* separate `sessionActivity` events.
|
|
28
|
+
*/
|
|
29
|
+
initialSessionStatuses: Array<{ sessionId: string; status: SessionStatus }>
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
export interface SessionBackend extends EventEmitter<SessionBackendEvents> {
|
package/src/state/git-tree.ts
CHANGED
|
@@ -53,7 +53,8 @@ export function gitFolderKey(section: GitFileSection, folderPath: string): strin
|
|
|
53
53
|
export function buildGitTreeRows(
|
|
54
54
|
files: GitFileEntry[],
|
|
55
55
|
collapsedFolders: Record<string, true>,
|
|
56
|
-
fileListMode: GitFileListMode = 'tree'
|
|
56
|
+
fileListMode: GitFileListMode = 'tree',
|
|
57
|
+
compact: boolean = false
|
|
57
58
|
): GitTreeRows {
|
|
58
59
|
const sections = SECTION_ORDER.map((section) => {
|
|
59
60
|
const sectionFiles = files.filter((file) => file.section === section)
|
|
@@ -68,7 +69,7 @@ export function buildGitTreeRows(
|
|
|
68
69
|
kind: 'file' as const,
|
|
69
70
|
section,
|
|
70
71
|
}))
|
|
71
|
-
: flattenSectionRows(sectionFiles, collapsedFolders),
|
|
72
|
+
: flattenSectionRows(sectionFiles, collapsedFolders, compact),
|
|
72
73
|
section,
|
|
73
74
|
}
|
|
74
75
|
})
|
|
@@ -81,10 +82,16 @@ export function getSelectedGitRow(
|
|
|
81
82
|
collapsedFolders: Record<string, true>
|
|
82
83
|
fileListMode: GitFileListMode
|
|
83
84
|
selectedEntryKey: string | null
|
|
85
|
+
compact?: boolean
|
|
84
86
|
}
|
|
85
87
|
): GitTreeRow | null {
|
|
86
88
|
if (!options.selectedEntryKey) return null
|
|
87
|
-
const { visibleRows } = buildGitTreeRows(
|
|
89
|
+
const { visibleRows } = buildGitTreeRows(
|
|
90
|
+
files,
|
|
91
|
+
options.collapsedFolders,
|
|
92
|
+
options.fileListMode,
|
|
93
|
+
options.compact ?? false
|
|
94
|
+
)
|
|
88
95
|
return visibleRows.find((row) => row.key === options.selectedEntryKey) ?? null
|
|
89
96
|
}
|
|
90
97
|
|
|
@@ -94,6 +101,7 @@ export function getSelectedGitFile(
|
|
|
94
101
|
collapsedFolders: Record<string, true>
|
|
95
102
|
fileListMode: GitFileListMode
|
|
96
103
|
selectedEntryKey: string | null
|
|
104
|
+
compact?: boolean
|
|
97
105
|
}
|
|
98
106
|
): GitFileEntry | null {
|
|
99
107
|
const row = getSelectedGitRow(files, options)
|
|
@@ -105,9 +113,10 @@ export function reconcileSelectedGitEntryKey(
|
|
|
105
113
|
collapsedFolders: Record<string, true>,
|
|
106
114
|
fileListMode: GitFileListMode,
|
|
107
115
|
selectedEntryKey: string | null | undefined,
|
|
108
|
-
preferredKeys: string[] = []
|
|
116
|
+
preferredKeys: string[] = [],
|
|
117
|
+
compact: boolean = false
|
|
109
118
|
): string | null {
|
|
110
|
-
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode)
|
|
119
|
+
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode, compact)
|
|
111
120
|
if (visibleRows.length === 0) return null
|
|
112
121
|
const candidates = [...preferredKeys, selectedEntryKey ?? '']
|
|
113
122
|
for (const key of candidates) {
|
|
@@ -122,9 +131,10 @@ export function moveGitSelection(
|
|
|
122
131
|
collapsedFolders: Record<string, true>,
|
|
123
132
|
fileListMode: GitFileListMode,
|
|
124
133
|
selectedEntryKey: string | null,
|
|
125
|
-
delta: -1 | 1
|
|
134
|
+
delta: -1 | 1,
|
|
135
|
+
compact: boolean = false
|
|
126
136
|
): string | null {
|
|
127
|
-
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode)
|
|
137
|
+
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode, compact)
|
|
128
138
|
const total = visibleRows.length
|
|
129
139
|
if (total === 0) return null
|
|
130
140
|
const current = visibleRows.findIndex((row) => row.key === selectedEntryKey)
|
|
@@ -137,9 +147,10 @@ export function moveGitFileSelection(
|
|
|
137
147
|
collapsedFolders: Record<string, true>,
|
|
138
148
|
fileListMode: GitFileListMode,
|
|
139
149
|
selectedEntryKey: string | null,
|
|
140
|
-
delta: -1 | 1
|
|
150
|
+
delta: -1 | 1,
|
|
151
|
+
compact: boolean = false
|
|
141
152
|
): string | null {
|
|
142
|
-
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode)
|
|
153
|
+
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode, compact)
|
|
143
154
|
const fileRows = visibleRows.filter((row) => row.kind === 'file')
|
|
144
155
|
const total = fileRows.length
|
|
145
156
|
if (total === 0) return null
|
|
@@ -153,13 +164,14 @@ export function moveGitFileSelection(
|
|
|
153
164
|
|
|
154
165
|
function flattenSectionRows(
|
|
155
166
|
files: GitFileEntry[],
|
|
156
|
-
collapsedFolders: Record<string, true
|
|
167
|
+
collapsedFolders: Record<string, true>,
|
|
168
|
+
compact: boolean
|
|
157
169
|
): GitTreeRow[] {
|
|
158
170
|
if (files.length === 0) return []
|
|
159
171
|
const section = files[0]?.section
|
|
160
172
|
if (!section) return []
|
|
161
173
|
const root = buildSectionTree(files, section)
|
|
162
|
-
return flattenTreeNode(root, collapsedFolders, 0)
|
|
174
|
+
return flattenTreeNode(root, collapsedFolders, 0, undefined, compact)
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
function buildSectionTree(files: GitFileEntry[], section: GitFileSection): GitTreeNode {
|
|
@@ -187,16 +199,28 @@ function flattenTreeNode(
|
|
|
187
199
|
node: GitTreeNode,
|
|
188
200
|
collapsedFolders: Record<string, true>,
|
|
189
201
|
depth: number,
|
|
190
|
-
parentKey
|
|
202
|
+
parentKey: string | undefined,
|
|
203
|
+
compact: boolean
|
|
191
204
|
): GitTreeRow[] {
|
|
192
205
|
const rows: GitTreeRow[] = []
|
|
193
206
|
for (const child of node.folders.values()) {
|
|
194
|
-
|
|
195
|
-
|
|
207
|
+
// Compaction: while a folder has exactly one child folder and no direct
|
|
208
|
+
// files, fold the chain into a single row ("src/keymap/keymap.ts" instead
|
|
209
|
+
// of three nested rows).
|
|
210
|
+
let current = child
|
|
211
|
+
const segments: string[] = [current.path.split('/').pop() ?? current.path]
|
|
212
|
+
while (compact && current.files.length === 0 && current.folders.size === 1) {
|
|
213
|
+
const next = current.folders.values().next().value
|
|
214
|
+
if (!next) break
|
|
215
|
+
segments.push(next.path.split('/').pop() ?? next.path)
|
|
216
|
+
current = next
|
|
217
|
+
}
|
|
218
|
+
const name = segments.join('/')
|
|
219
|
+
const key = gitFolderKey(node.section, current.path)
|
|
196
220
|
const isCollapsed = key in collapsedFolders
|
|
197
221
|
rows.push({
|
|
198
222
|
depth,
|
|
199
|
-
folderPath:
|
|
223
|
+
folderPath: current.path,
|
|
200
224
|
isCollapsed,
|
|
201
225
|
key,
|
|
202
226
|
kind: 'folder',
|
|
@@ -204,7 +228,9 @@ function flattenTreeNode(
|
|
|
204
228
|
parentKey,
|
|
205
229
|
section: node.section,
|
|
206
230
|
})
|
|
207
|
-
if (!isCollapsed)
|
|
231
|
+
if (!isCollapsed) {
|
|
232
|
+
rows.push(...flattenTreeNode(current, collapsedFolders, depth + 1, key, compact))
|
|
233
|
+
}
|
|
208
234
|
}
|
|
209
235
|
for (const file of node.files) {
|
|
210
236
|
rows.push({
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AppState } from '../types'
|
|
2
|
+
|
|
3
|
+
export function clearDiffCacheForPath(state: AppState, path: string): AppState {
|
|
4
|
+
return clearDiffCacheForPaths(state, new Set([path]))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Drops every cache slice derived from the given file paths: diffs, folds,
|
|
8
|
+
// loading flags, parsed patches, and per-theme highlight tokens. Called when
|
|
9
|
+
// a file's on-disk content shifts or a file disappears from git status.
|
|
10
|
+
export function clearDiffCacheForPaths(state: AppState, paths: Set<string>): AppState {
|
|
11
|
+
if (paths.size === 0) return state
|
|
12
|
+
const nextDiffs = { ...state.gitMode.diffs }
|
|
13
|
+
const nextFolds = { ...state.gitMode.folds }
|
|
14
|
+
const nextLoading = { ...state.gitMode.loading }
|
|
15
|
+
const nextParsed = { ...state.gitMode.parsedFiles }
|
|
16
|
+
const nextHighlights = { ...state.gitMode.highlights }
|
|
17
|
+
let changed = false
|
|
18
|
+
const keyMatches = (key: string): boolean => {
|
|
19
|
+
for (const path of paths) {
|
|
20
|
+
if (key.endsWith(`:${path}`) || key.startsWith(`${path}|`) || key.includes(`:${path}|`)) {
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
for (const key of Object.keys(nextDiffs)) {
|
|
27
|
+
const entry = nextDiffs[key]
|
|
28
|
+
if (!entry || !paths.has(entry.path)) continue
|
|
29
|
+
delete nextDiffs[key]
|
|
30
|
+
changed = true
|
|
31
|
+
}
|
|
32
|
+
for (const key of Object.keys(nextFolds)) {
|
|
33
|
+
if (!keyMatches(key)) continue
|
|
34
|
+
delete nextFolds[key]
|
|
35
|
+
changed = true
|
|
36
|
+
}
|
|
37
|
+
for (const key of Object.keys(nextLoading)) {
|
|
38
|
+
if (!keyMatches(key)) continue
|
|
39
|
+
delete nextLoading[key]
|
|
40
|
+
changed = true
|
|
41
|
+
}
|
|
42
|
+
for (const key of Object.keys(nextParsed)) {
|
|
43
|
+
if (!keyMatches(key)) continue
|
|
44
|
+
delete nextParsed[key]
|
|
45
|
+
changed = true
|
|
46
|
+
}
|
|
47
|
+
for (const key of Object.keys(nextHighlights)) {
|
|
48
|
+
if (!keyMatches(key)) continue
|
|
49
|
+
delete nextHighlights[key]
|
|
50
|
+
changed = true
|
|
51
|
+
}
|
|
52
|
+
if (!changed) return state
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
gitMode: {
|
|
56
|
+
...state.gitMode,
|
|
57
|
+
diffs: nextDiffs,
|
|
58
|
+
folds: nextFolds,
|
|
59
|
+
highlights: nextHighlights,
|
|
60
|
+
loading: nextLoading,
|
|
61
|
+
parsedFiles: nextParsed,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|