@brimveyn/aimux 1.21.0 → 1.22.2
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 -25
- package/package.json +2 -2
- package/src/app-runtime/auto-commit-driver.ts +22 -27
- package/src/app-runtime/backend-attach-runtime.ts +43 -36
- package/src/app-runtime/backend-runtime-events.ts +107 -88
- package/src/app-runtime/editor-actions.ts +201 -0
- package/src/app-runtime/git-actions.ts +197 -0
- package/src/app-runtime/measure-ref.ts +19 -0
- package/src/app-runtime/navigation-actions.ts +213 -0
- package/src/app-runtime/project-actions.ts +188 -0
- package/src/app-runtime/prompt-injection.ts +123 -0
- package/src/app-runtime/selection.ts +29 -0
- package/src/app-runtime/settings-actions.ts +156 -0
- package/src/app-runtime/setup-actions.ts +237 -0
- package/src/app-runtime/side-effect-context.ts +33 -0
- package/src/app-runtime/side-effects.ts +302 -1541
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/tab-actions.ts +218 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
- package/src/app-runtime/use-auto-commit-driver.ts +14 -13
- package/src/app-runtime/use-backend-runtime.ts +12 -11
- package/src/app-runtime/use-directory-search.ts +5 -4
- package/src/app-runtime/use-mouse-handlers.ts +2 -1
- package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app-runtime/use-setup-runner.ts +96 -0
- package/src/app-runtime/use-terminal-resize.ts +18 -14
- package/src/app-runtime/workspace-actions.ts +366 -0
- package/src/app-runtime/workspace-activity.ts +138 -0
- package/src/app-runtime/workspace-naming.ts +90 -0
- package/src/app.tsx +93 -48
- package/src/assets/sounds/bell.wav +0 -0
- package/src/assets/sounds/ding.wav +0 -0
- package/src/assets/sounds/plane.wav +0 -0
- package/src/cli/client/daemon-client.ts +2 -2
- package/src/cli/client/project-resolver.ts +112 -0
- package/src/cli/commands/project/close.ts +32 -0
- package/src/cli/commands/project/create.ts +93 -0
- package/src/cli/commands/project/list.ts +25 -0
- package/src/cli/commands/project/show.ts +32 -0
- package/src/cli/commands/{workspace → project}/switch.ts +19 -19
- package/src/cli/commands/tab/await.ts +2 -2
- package/src/cli/commands/tab/close.ts +3 -3
- package/src/cli/commands/tab/create.ts +74 -73
- package/src/cli/commands/tab/focus.ts +3 -3
- package/src/cli/commands/tab/list.ts +6 -6
- package/src/cli/commands/tab/run.ts +2 -2
- package/src/cli/commands/tab/send.ts +2 -2
- package/src/cli/commands/tab/snapshot.ts +2 -2
- package/src/cli/commands/tab/tail.ts +2 -2
- package/src/cli/commands/tab/wait.ts +2 -2
- package/src/cli/commands/worker/await.ts +3 -3
- package/src/cli/commands/worker/doctor.ts +30 -30
- package/src/cli/commands/worker/list.ts +15 -15
- package/src/cli/commands/worker/prompt.ts +3 -3
- package/src/cli/commands/worker/run.ts +23 -23
- package/src/cli/commands/worker/shared.ts +53 -59
- package/src/cli/commands/worker/stop.ts +29 -29
- package/src/cli/commands/worker/submit.ts +3 -3
- package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
- package/src/cli/commands/workspace/create.ts +30 -68
- package/src/cli/commands/workspace/list.ts +52 -9
- package/src/cli/commands/workspace/remove.ts +81 -0
- package/src/cli/completion/plan.ts +2 -2
- package/src/cli/completion/sources.ts +18 -18
- package/src/cli/context.ts +13 -13
- package/src/cli/flags.ts +30 -10
- package/src/cli/index.ts +26 -16
- package/src/cli/output.ts +1 -1
- package/src/cli/registry.ts +12 -12
- package/src/config/loader.ts +16 -5
- package/src/config.ts +70 -112
- package/src/daemon/catalog-writer.ts +67 -67
- package/src/daemon/daemon.ts +205 -201
- package/src/daemon/session-manager.ts +39 -39
- package/src/daemon/session-registry.ts +14 -14
- package/src/git/divergence.ts +33 -2
- package/src/git/git-poller.ts +1 -1
- package/src/git/git-status.ts +2 -2
- package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
- package/src/git/pr-status.ts +1 -1
- package/src/git/repo-discovery.ts +1 -1
- package/src/git/use-repo-discovery.ts +1 -1
- package/src/git/workspace-branch-poller.ts +54 -0
- package/src/git/workspace-divergence-poller.ts +65 -0
- package/src/git/worktree.ts +29 -0
- package/src/index.tsx +4 -4
- package/src/input/keymap/help-entries.ts +7 -5
- package/src/input/modes/bridge.ts +13 -12
- package/src/input/modes/handlers/shared.ts +1 -1
- package/src/input/modes/transitions.ts +34 -21
- package/src/input/modes/types.ts +48 -31
- package/src/ipc/manager-protocol.ts +44 -44
- package/src/ipc/protocol.ts +192 -166
- package/src/platform/play-sound.ts +184 -0
- package/src/platform/project-search.ts +8 -8
- package/src/platform/worktree-paths.ts +8 -6
- package/src/pty/assistant-question-extractor.ts +1 -1
- package/src/pty/assistant-status-detection-loop.ts +79 -55
- package/src/pty/assistant-status-detector.ts +40 -15
- package/src/pty/command-registry.ts +81 -1
- package/src/restart-terminal-manager.ts +1 -1
- package/src/services/ai-usage/provider.ts +9 -2
- package/src/services/ai-usage/spawn.ts +19 -6
- package/src/session-backend/bootstrap.ts +4 -4
- package/src/session-backend/local-session-backend.ts +78 -75
- package/src/session-backend/remote-session-backend.ts +64 -52
- package/src/session-backend/types.ts +43 -32
- package/src/settings/live.ts +90 -0
- package/src/settings/search.ts +43 -0
- package/src/settings/sections/about.ts +49 -0
- package/src/settings/sections/appearance.ts +54 -0
- package/src/settings/sections/automation.ts +105 -0
- package/src/settings/sections/commands.ts +72 -0
- package/src/settings/sections/editor.ts +56 -0
- package/src/settings/sections/experimental.ts +57 -0
- package/src/settings/sections/git.ts +112 -0
- package/src/settings/sections/index.ts +95 -0
- package/src/settings/sections/integrations.ts +23 -0
- package/src/settings/sections/layout.ts +72 -0
- package/src/settings/sections/notifications.ts +99 -0
- package/src/settings/sections/setup.ts +54 -0
- package/src/settings/sections/status-bar.ts +63 -0
- package/src/settings/settings-store.ts +207 -0
- package/src/settings/types.ts +108 -0
- package/src/snippets/run-shell-var.ts +48 -22
- package/src/state/actions.ts +342 -0
- package/src/state/app-store.ts +2 -1
- package/src/state/bars.ts +1 -1
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/layout-resize.ts +2 -2
- package/src/state/project-catalog.ts +275 -0
- package/src/state/project-data.ts +143 -0
- package/src/state/{session-persistence.ts → project-persistence.ts} +74 -68
- package/src/state/project-save.ts +46 -0
- package/src/state/project-workspaces.ts +392 -0
- package/src/state/reducers/auto-commit-state.ts +11 -10
- package/src/state/reducers/git-commit-modal-state.ts +122 -0
- package/src/state/reducers/git-mode-state.ts +1 -1
- package/src/state/reducers/git-panel-state.ts +10 -6
- package/src/state/reducers/modal-state.ts +290 -681
- package/src/state/reducers/multi-repo-state.ts +3 -6
- package/src/state/reducers/project-state.ts +312 -0
- package/src/state/reducers/settings-state.ts +80 -0
- package/src/state/reducers/tab-state.ts +107 -60
- package/src/state/reducers/ui-state.ts +4 -3
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +57 -24
- package/src/state/types.ts +244 -405
- package/src/state/validation.ts +14 -12
- package/src/terminal-manager/manager-client.ts +32 -32
- package/src/terminal-manager/terminal-manager.ts +24 -24
- package/src/ui/components/git/git-panel.tsx +2 -2
- package/src/ui/components/git/git-view.tsx +12 -12
- package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
- package/src/ui/components/git/pane/git-pane-widget.tsx +7 -7
- package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
- package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
- package/src/ui/components/layout/status-bar.tsx +6 -2
- package/src/ui/components/layout/terminal-pane.tsx +34 -29
- package/src/ui/components/layout/top-tab-bar.tsx +46 -36
- package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
- package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
- package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
- package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
- package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
- package/src/ui/components/modals/shared/form.tsx +34 -22
- package/src/ui/components/modals/shared/picker.tsx +10 -10
- package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
- package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
- package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
- package/src/ui/components/primitives/input-field.tsx +7 -3
- package/src/ui/components/primitives/surface.tsx +3 -0
- package/src/ui/components/settings/row-value.tsx +74 -0
- package/src/ui/components/settings/settings-row.tsx +78 -0
- package/src/ui/components/settings/settings-view.tsx +169 -0
- package/src/ui/components/setup/setup-widget.tsx +152 -0
- package/src/ui/flash/build-labels.ts +27 -27
- package/src/ui/hooks/use-activity-sprite.ts +73 -0
- package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
- package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
- package/src/ui/root.tsx +139 -121
- package/src/ui/status-bar-model.ts +48 -34
- package/src/ui/terminal-graphics/kitty.ts +28 -2
- package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
- package/src/ui/terminal-graphics/sprites.ts +246 -0
- package/src/ui/truncate.ts +7 -0
- package/src/ui/widgets/registry.tsx +6 -3
- package/src/ui/widgets/widget-context-menu.ts +26 -10
- package/src/update.ts +2 -2
- package/src/app-runtime/session-actions.ts +0 -187
- package/src/cli/client/workspace-resolver.ts +0 -110
- package/src/cli/commands/workspace/close.ts +0 -32
- package/src/cli/commands/workspace/show.ts +0 -32
- package/src/cli/commands/worktree/create.ts +0 -55
- package/src/cli/commands/worktree/list.ts +0 -68
- package/src/cli/commands/worktree/remove.ts +0 -81
- package/src/git/worktree-branch-poller.ts +0 -54
- package/src/git/worktree-divergence-poller.ts +0 -59
- package/src/state/reducers/session-state.ts +0 -262
- package/src/state/session-catalog.ts +0 -143
- package/src/state/session-worktrees.ts +0 -260
- package/src/state/workspace-save.ts +0 -43
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
- package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
- package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
- /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type DiscoveredRepo,
|
|
5
|
-
EMPTY_MULTI_REPO_STATE,
|
|
6
|
-
} from '../types'
|
|
1
|
+
import type { AppAction } from '../actions'
|
|
2
|
+
|
|
3
|
+
import { type AppState, type DiscoveredRepo, EMPTY_MULTI_REPO_STATE } from '../types'
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* Compute the shortest distinguishing prefix for each non-root repo name.
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type { AppAction } from '../actions'
|
|
2
|
+
import type { AppState } from '../types'
|
|
3
|
+
|
|
4
|
+
import { moveIdToIdPosition, orderProjectsForDisplay } from '../../ui/project-ordering'
|
|
5
|
+
import { restoreProjectState } from '../project-persistence'
|
|
6
|
+
import {
|
|
7
|
+
clearWorkspaceDone,
|
|
8
|
+
filterTabsForActiveWorkspace,
|
|
9
|
+
withActiveWorkspace,
|
|
10
|
+
} from '../project-workspaces'
|
|
11
|
+
import { filterProjects } from '../selectors'
|
|
12
|
+
// eslint-disable-next-line no-duplicate-imports
|
|
13
|
+
import { IDLE_WORKSPACE_ACTIVITY } from '../types'
|
|
14
|
+
|
|
15
|
+
const CLOSED_MODAL = {
|
|
16
|
+
editBuffer: null,
|
|
17
|
+
projectTargetId: null,
|
|
18
|
+
selectedIndex: 0,
|
|
19
|
+
type: null,
|
|
20
|
+
} as const
|
|
21
|
+
|
|
22
|
+
export function reduceProjectState(state: AppState, action: AppAction): AppState | null {
|
|
23
|
+
switch (action.type) {
|
|
24
|
+
case 'load-project': {
|
|
25
|
+
const snapshot =
|
|
26
|
+
action.projectSnapshot ??
|
|
27
|
+
state.projects.find((entry) => entry.id === action.projectId)?.projectSnapshot
|
|
28
|
+
// The project's activeWorkspaceId may have been patched right before
|
|
29
|
+
// this load (e.g. the cross-project branch of handleCycleSidebarItem
|
|
30
|
+
// sets it to the workspace the user just clicked). The snapshot's
|
|
31
|
+
// activeTabId still reflects the *last* workspace they were on, so
|
|
32
|
+
// honor the patched workspace by filtering the restored tab list.
|
|
33
|
+
const loadedProject = state.projects.find((entry) => entry.id === action.projectId)
|
|
34
|
+
const loadedWorkspaces = loadedProject?.workspaces ?? []
|
|
35
|
+
const restored = restoreProjectState(state, snapshot, {
|
|
36
|
+
forceDisconnected: action.forceDisconnected ?? true,
|
|
37
|
+
// Drop tabs bound to workspaces this project no longer owns so a stale
|
|
38
|
+
// id can't be caught by a later workspace delete (closing "another
|
|
39
|
+
// workspace's" tabs) and so corrupted catalogs self-heal on load. Only
|
|
40
|
+
// prune once the project has workspaces — an empty list means it hasn't
|
|
41
|
+
// been initialized yet, and pruning then would wrongly drop bound tabs.
|
|
42
|
+
validWorkspaceIds:
|
|
43
|
+
loadedWorkspaces.length > 0 ? new Set(loadedWorkspaces.map((w) => w.id)) : undefined,
|
|
44
|
+
})
|
|
45
|
+
const visible = filterTabsForActiveWorkspace(restored.tabs, loadedProject)
|
|
46
|
+
// Prefer the snapshot's tab; if it isn't visible under the active
|
|
47
|
+
// workspace (e.g. a multi-workspace project restored onto a different
|
|
48
|
+
// workspace), fall back to the last tab we viewed in that workspace,
|
|
49
|
+
// then to the first visible tab.
|
|
50
|
+
const rememberedForWorkspace =
|
|
51
|
+
loadedProject?.activeWorkspaceId != null && loadedProject.activeWorkspaceId !== ''
|
|
52
|
+
? state.lastActiveTabByWorkspace[loadedProject.activeWorkspaceId]
|
|
53
|
+
: undefined
|
|
54
|
+
const snapshotTabVisible =
|
|
55
|
+
restored.activeTabId != null &&
|
|
56
|
+
restored.activeTabId !== '' &&
|
|
57
|
+
visible.some((t) => t.id === restored.activeTabId)
|
|
58
|
+
const rememberedTabVisible =
|
|
59
|
+
rememberedForWorkspace != null &&
|
|
60
|
+
rememberedForWorkspace !== '' &&
|
|
61
|
+
visible.some((t) => t.id === rememberedForWorkspace)
|
|
62
|
+
const fallbackTabId = rememberedTabVisible ? rememberedForWorkspace : (visible[0]?.id ?? null)
|
|
63
|
+
const activeTabId = snapshotTabVisible ? restored.activeTabId : fallbackTabId
|
|
64
|
+
return {
|
|
65
|
+
...state,
|
|
66
|
+
...restored,
|
|
67
|
+
activeTabId,
|
|
68
|
+
currentProjectId: action.projectId,
|
|
69
|
+
focusMode: 'navigation',
|
|
70
|
+
modal: CLOSED_MODAL,
|
|
71
|
+
projects: state.projects.map((entry) =>
|
|
72
|
+
entry.id === action.projectId
|
|
73
|
+
? {
|
|
74
|
+
...entry,
|
|
75
|
+
lastOpenedAt: new Date().toISOString(),
|
|
76
|
+
updatedAt: new Date().toISOString(),
|
|
77
|
+
}
|
|
78
|
+
: entry
|
|
79
|
+
),
|
|
80
|
+
// Landing on a project lands on its active workspace — same rule as
|
|
81
|
+
// `set-active-workspace`: you have seen it, so the tick goes.
|
|
82
|
+
workspaceActivity: clearWorkspaceDone(
|
|
83
|
+
state.workspaceActivity,
|
|
84
|
+
loadedProject?.activeWorkspaceId
|
|
85
|
+
),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
case 'set-projects':
|
|
89
|
+
return { ...state, projects: action.projects }
|
|
90
|
+
case 'create-project-record':
|
|
91
|
+
return {
|
|
92
|
+
...state,
|
|
93
|
+
currentProjectId: action.project.id,
|
|
94
|
+
focusMode: 'navigation',
|
|
95
|
+
modal: CLOSED_MODAL,
|
|
96
|
+
projects: [...state.projects, action.project],
|
|
97
|
+
}
|
|
98
|
+
case 'rename-project-record':
|
|
99
|
+
return {
|
|
100
|
+
...state,
|
|
101
|
+
projects: state.projects.map((project) =>
|
|
102
|
+
project.id === action.projectId
|
|
103
|
+
? { ...project, name: action.name, updatedAt: new Date().toISOString() }
|
|
104
|
+
: project
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
case 'delete-project-record': {
|
|
108
|
+
const deletingCurrent = action.projectId === state.currentProjectId
|
|
109
|
+
const newProjects = state.projects.filter((project) => project.id !== action.projectId)
|
|
110
|
+
const nextStatuses = { ...state.projectStatuses }
|
|
111
|
+
delete nextStatuses[action.projectId]
|
|
112
|
+
if (action.openProjectPicker === true) {
|
|
113
|
+
const filteredNew = filterProjects(newProjects, state.modal.editBuffer)
|
|
114
|
+
const maxIndex = filteredNew.length
|
|
115
|
+
const clampedIndex = Math.min(state.modal.selectedIndex, maxIndex)
|
|
116
|
+
return {
|
|
117
|
+
...state,
|
|
118
|
+
activeTabId: deletingCurrent ? null : state.activeTabId,
|
|
119
|
+
currentProjectId: deletingCurrent ? null : state.currentProjectId,
|
|
120
|
+
focusMode: 'modal',
|
|
121
|
+
modal: {
|
|
122
|
+
editBuffer: null,
|
|
123
|
+
projectTargetId: null,
|
|
124
|
+
selectedIndex: clampedIndex,
|
|
125
|
+
type: 'project-picker',
|
|
126
|
+
},
|
|
127
|
+
projects: newProjects,
|
|
128
|
+
projectStatuses: nextStatuses,
|
|
129
|
+
tabs: deletingCurrent ? [] : state.tabs,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
...state,
|
|
134
|
+
activeTabId: deletingCurrent ? null : state.activeTabId,
|
|
135
|
+
currentProjectId: deletingCurrent ? null : state.currentProjectId,
|
|
136
|
+
focusMode: deletingCurrent ? 'navigation' : state.focusMode,
|
|
137
|
+
modal: deletingCurrent ? CLOSED_MODAL : state.modal,
|
|
138
|
+
projects: newProjects,
|
|
139
|
+
projectStatuses: nextStatuses,
|
|
140
|
+
tabs: deletingCurrent ? [] : state.tabs,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
case 'reorder-projects': {
|
|
144
|
+
const byId = new Map(state.projects.map((s) => [s.id, s]))
|
|
145
|
+
const ordered: typeof state.projects = []
|
|
146
|
+
let idx = 0
|
|
147
|
+
for (const id of action.orderedIds) {
|
|
148
|
+
const s = byId.get(id)
|
|
149
|
+
if (s) {
|
|
150
|
+
ordered.push({ ...s, order: idx })
|
|
151
|
+
byId.delete(id)
|
|
152
|
+
}
|
|
153
|
+
idx++
|
|
154
|
+
}
|
|
155
|
+
let nextOrder = ordered.length
|
|
156
|
+
for (const s of byId.values()) {
|
|
157
|
+
ordered.push({ ...s, order: nextOrder++ })
|
|
158
|
+
}
|
|
159
|
+
return { ...state, projects: ordered }
|
|
160
|
+
}
|
|
161
|
+
case 'reorder-active-project': {
|
|
162
|
+
const currentId = state.currentProjectId
|
|
163
|
+
if (currentId == null || currentId === '') return state
|
|
164
|
+
const ids = orderProjectsForDisplay(state.projects).map((s) => s.id)
|
|
165
|
+
const from = ids.indexOf(currentId)
|
|
166
|
+
if (from < 0) return state
|
|
167
|
+
const to = from + action.delta
|
|
168
|
+
const targetId = ids[to]
|
|
169
|
+
if (targetId == null) return state
|
|
170
|
+
const nextIds = moveIdToIdPosition(ids, currentId, targetId)
|
|
171
|
+
const byId = new Map(state.projects.map((s) => [s.id, s]))
|
|
172
|
+
const nextProjects = nextIds.map((id, idx) => {
|
|
173
|
+
const s = byId.get(id)
|
|
174
|
+
return s ? { ...s, order: idx } : s
|
|
175
|
+
})
|
|
176
|
+
const filtered = nextProjects.filter((s): s is NonNullable<typeof s> => s != null)
|
|
177
|
+
if (filtered.length !== state.projects.length) return state
|
|
178
|
+
return { ...state, projects: filtered }
|
|
179
|
+
}
|
|
180
|
+
case 'set-project-status': {
|
|
181
|
+
const prev = state.projectStatuses[action.projectId]
|
|
182
|
+
if (
|
|
183
|
+
prev !== undefined &&
|
|
184
|
+
prev.working === action.status.working &&
|
|
185
|
+
prev.waiting === action.status.waiting
|
|
186
|
+
) {
|
|
187
|
+
return state
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
...state,
|
|
191
|
+
projectStatuses: { ...state.projectStatuses, [action.projectId]: action.status },
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
case 'set-workspace-activity': {
|
|
195
|
+
const prev = state.workspaceActivity[action.workspaceId] ?? IDLE_WORKSPACE_ACTIVITY
|
|
196
|
+
// Going back to work is the other way a "finished, unseen" tick clears:
|
|
197
|
+
// the workspace has something newer to say than "it ended a turn once".
|
|
198
|
+
const done = action.working || action.waiting ? false : prev.done
|
|
199
|
+
if (
|
|
200
|
+
prev.working === action.working &&
|
|
201
|
+
prev.waiting === action.waiting &&
|
|
202
|
+
prev.done === done
|
|
203
|
+
) {
|
|
204
|
+
return state
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
...state,
|
|
208
|
+
workspaceActivity: {
|
|
209
|
+
...state.workspaceActivity,
|
|
210
|
+
[action.workspaceId]: { done, waiting: action.waiting, working: action.working },
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
case 'mark-workspace-done': {
|
|
215
|
+
const prev = state.workspaceActivity[action.workspaceId] ?? IDLE_WORKSPACE_ACTIVITY
|
|
216
|
+
if (prev.done) return state
|
|
217
|
+
return {
|
|
218
|
+
...state,
|
|
219
|
+
workspaceActivity: {
|
|
220
|
+
...state.workspaceActivity,
|
|
221
|
+
[action.workspaceId]: { ...prev, done: true },
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
case 'add-workspace-record':
|
|
226
|
+
return {
|
|
227
|
+
...state,
|
|
228
|
+
projects: state.projects.map((project) => {
|
|
229
|
+
if (project.id !== action.projectId) return project
|
|
230
|
+
const next = {
|
|
231
|
+
...project,
|
|
232
|
+
activeWorkspaceId:
|
|
233
|
+
action.activate === true ? action.workspace.id : project.activeWorkspaceId,
|
|
234
|
+
updatedAt: new Date().toISOString(),
|
|
235
|
+
workspaces: [...(project.workspaces ?? []), action.workspace],
|
|
236
|
+
}
|
|
237
|
+
return next.activeWorkspaceId != null && next.activeWorkspaceId !== ''
|
|
238
|
+
? next
|
|
239
|
+
: withActiveWorkspace(next, action.workspace.id)
|
|
240
|
+
}),
|
|
241
|
+
}
|
|
242
|
+
case 'set-active-workspace': {
|
|
243
|
+
const projects = state.projects.map((project) =>
|
|
244
|
+
project.id === action.projectId ? withActiveWorkspace(project, action.workspaceId) : project
|
|
245
|
+
)
|
|
246
|
+
// Remember the tab we're leaving so coming back to this workspace later
|
|
247
|
+
// restores it instead of snapping to the first tab. Keyed by the
|
|
248
|
+
// workspace we're departing (the current project's active one).
|
|
249
|
+
const workspaceActivity = clearWorkspaceDone(state.workspaceActivity, action.workspaceId)
|
|
250
|
+
const leavingProject = state.projects.find((s) => s.id === action.projectId)
|
|
251
|
+
const leavingWorkspaceId = leavingProject?.activeWorkspaceId
|
|
252
|
+
const lastActiveTabByWorkspace =
|
|
253
|
+
action.projectId === state.currentProjectId &&
|
|
254
|
+
leavingWorkspaceId != null &&
|
|
255
|
+
leavingWorkspaceId !== '' &&
|
|
256
|
+
state.activeTabId != null &&
|
|
257
|
+
state.activeTabId !== ''
|
|
258
|
+
? { ...state.lastActiveTabByWorkspace, [leavingWorkspaceId]: state.activeTabId }
|
|
259
|
+
: state.lastActiveTabByWorkspace
|
|
260
|
+
// Changing the workspace of a non-current project has no effect on
|
|
261
|
+
// which tab the user is looking at — leave activeTabId alone.
|
|
262
|
+
if (action.projectId !== state.currentProjectId) {
|
|
263
|
+
return { ...state, lastActiveTabByWorkspace, projects, workspaceActivity }
|
|
264
|
+
}
|
|
265
|
+
// For the current project: if the existing activeTabId belongs to
|
|
266
|
+
// the new workspace, keep it. Otherwise restore the last tab we viewed
|
|
267
|
+
// in that workspace, falling back to the first visible tab (or clearing
|
|
268
|
+
// it if the workspace is empty — the pane then renders the "this
|
|
269
|
+
// workspace has no tabs" placeholder).
|
|
270
|
+
const updated = projects.find((s) => s.id === action.projectId)
|
|
271
|
+
const visible = filterTabsForActiveWorkspace(state.tabs, updated)
|
|
272
|
+
const currentStillVisible =
|
|
273
|
+
state.activeTabId != null &&
|
|
274
|
+
state.activeTabId !== '' &&
|
|
275
|
+
visible.some((t) => t.id === state.activeTabId)
|
|
276
|
+
if (currentStillVisible) {
|
|
277
|
+
return { ...state, lastActiveTabByWorkspace, projects, workspaceActivity }
|
|
278
|
+
}
|
|
279
|
+
const remembered = lastActiveTabByWorkspace[action.workspaceId]
|
|
280
|
+
const nextActiveTabId =
|
|
281
|
+
remembered != null && remembered !== '' && visible.some((t) => t.id === remembered)
|
|
282
|
+
? remembered
|
|
283
|
+
: (visible[0]?.id ?? null)
|
|
284
|
+
return {
|
|
285
|
+
...state,
|
|
286
|
+
activeTabId: nextActiveTabId,
|
|
287
|
+
lastActiveTabByWorkspace,
|
|
288
|
+
projects,
|
|
289
|
+
workspaceActivity,
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
case 'update-workspace-record':
|
|
293
|
+
return {
|
|
294
|
+
...state,
|
|
295
|
+
projects: state.projects.map((project) =>
|
|
296
|
+
project.id === action.projectId
|
|
297
|
+
? {
|
|
298
|
+
...project,
|
|
299
|
+
updatedAt: new Date().toISOString(),
|
|
300
|
+
workspaces: project.workspaces?.map((workspace) =>
|
|
301
|
+
workspace.id === action.workspaceId
|
|
302
|
+
? { ...workspace, ...action.patch, updatedAt: new Date().toISOString() }
|
|
303
|
+
: workspace
|
|
304
|
+
),
|
|
305
|
+
}
|
|
306
|
+
: project
|
|
307
|
+
),
|
|
308
|
+
}
|
|
309
|
+
default:
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AppAction } from '../actions'
|
|
2
|
+
import type { AppState, SettingsUIState } from '../types'
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_SECTION_ID, getSectionRowCount, SETTING_SECTIONS } from '../../settings/sections'
|
|
5
|
+
|
|
6
|
+
export function emptySettingsUI(): SettingsUIState {
|
|
7
|
+
return { pane: 'nav', rowIndex: 0, sectionId: DEFAULT_SECTION_ID }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function clamp(value: number, max: number): number {
|
|
11
|
+
if (max < 0) return 0
|
|
12
|
+
return Math.max(0, Math.min(max, value))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function withSettings(state: AppState, patch: Partial<SettingsUIState>): AppState {
|
|
16
|
+
return { ...state, settings: { ...state.settings, ...patch } }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function moveSelection(state: AppState, delta: -1 | 1): AppState {
|
|
20
|
+
const { pane, rowIndex, sectionId } = state.settings
|
|
21
|
+
|
|
22
|
+
if (pane === 'nav') {
|
|
23
|
+
const index = SETTING_SECTIONS.findIndex((section) => section.id === sectionId)
|
|
24
|
+
const next = clamp((index === -1 ? 0 : index) + delta, SETTING_SECTIONS.length - 1)
|
|
25
|
+
const nextId = SETTING_SECTIONS[next]?.id
|
|
26
|
+
if (nextId == null || nextId === sectionId) return state
|
|
27
|
+
// A new section has its own rows, so the row cursor from the old one means
|
|
28
|
+
// nothing here.
|
|
29
|
+
return withSettings(state, { rowIndex: 0, sectionId: nextId })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return withSettings(state, {
|
|
33
|
+
rowIndex: clamp(rowIndex + delta, getSectionRowCount(sectionId, state.projects) - 1),
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function reduceSettingsState(state: AppState, action: AppAction): AppState | null {
|
|
38
|
+
switch (action.type) {
|
|
39
|
+
case 'enter-settings':
|
|
40
|
+
if (state.focusMode === 'settings') return state
|
|
41
|
+
// The section is remembered across a close/open within the session; the
|
|
42
|
+
// cursor inside it is not — reopening lands you at the top of it.
|
|
43
|
+
return {
|
|
44
|
+
...state,
|
|
45
|
+
focusMode: 'settings',
|
|
46
|
+
settings: { ...state.settings, pane: 'nav', rowIndex: 0 },
|
|
47
|
+
}
|
|
48
|
+
case 'exit-settings':
|
|
49
|
+
if (state.focusMode !== 'settings') return state
|
|
50
|
+
return { ...state, focusMode: 'navigation' }
|
|
51
|
+
case 'settings-focus-pane': {
|
|
52
|
+
// A section with no rows has nothing to focus, so `l` stays put rather
|
|
53
|
+
// than parking the cursor in an empty column.
|
|
54
|
+
if (
|
|
55
|
+
action.pane === 'rows' &&
|
|
56
|
+
getSectionRowCount(state.settings.sectionId, state.projects) === 0
|
|
57
|
+
) {
|
|
58
|
+
return state
|
|
59
|
+
}
|
|
60
|
+
return withSettings(state, { pane: action.pane })
|
|
61
|
+
}
|
|
62
|
+
case 'settings-move-selection':
|
|
63
|
+
return moveSelection(state, action.delta)
|
|
64
|
+
case 'settings-select-section':
|
|
65
|
+
if (action.sectionId === state.settings.sectionId) {
|
|
66
|
+
return withSettings(state, { pane: 'nav' })
|
|
67
|
+
}
|
|
68
|
+
return withSettings(state, { pane: 'nav', rowIndex: 0, sectionId: action.sectionId })
|
|
69
|
+
case 'settings-select-row':
|
|
70
|
+
return withSettings(state, {
|
|
71
|
+
pane: 'rows',
|
|
72
|
+
rowIndex: clamp(
|
|
73
|
+
action.rowIndex,
|
|
74
|
+
getSectionRowCount(state.settings.sectionId, state.projects) - 1
|
|
75
|
+
),
|
|
76
|
+
})
|
|
77
|
+
default:
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|