@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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { $ } from 'bun'
|
|
2
|
+
|
|
3
|
+
import type { SideEffectContext } from './side-effect-context'
|
|
4
|
+
|
|
5
|
+
import { toast } from '../state/toast-store'
|
|
6
|
+
import { triggerAutoCommitNow } from './auto-commit-ref'
|
|
7
|
+
|
|
8
|
+
export async function runGitAction(
|
|
9
|
+
ctx: SideEffectContext,
|
|
10
|
+
args: string[],
|
|
11
|
+
pathToInvalidate?: string
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const fallback = ctx.getCurrentProjectProjectPath()
|
|
14
|
+
const repoPath =
|
|
15
|
+
pathToInvalidate != null && pathToInvalidate !== ''
|
|
16
|
+
? ctx.state.gitPanel.files.find((f) => f.path === pathToInvalidate)?.repoPath
|
|
17
|
+
: undefined
|
|
18
|
+
const cwd = repoPath ?? fallback
|
|
19
|
+
if (!(cwd != null && cwd !== '')) return
|
|
20
|
+
const result = await $`git -C ${cwd} ${args}`.quiet().nothrow()
|
|
21
|
+
if (result.exitCode !== 0) {
|
|
22
|
+
const stderr = result.stderr.toString().trim()
|
|
23
|
+
ctx.dispatch({ message: stderr || 'git action failed', type: 'git-mode-set-message' })
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
ctx.dispatch({ message: null, type: 'git-mode-set-message' })
|
|
27
|
+
if (pathToInvalidate != null && pathToInvalidate !== '') {
|
|
28
|
+
ctx.dispatch({ path: pathToInvalidate, type: 'git-mode-clear-diff-cache' })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runGitActionAll(
|
|
33
|
+
ctx: SideEffectContext,
|
|
34
|
+
args: string[],
|
|
35
|
+
pathsToInvalidate: string[]
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
const cwd = ctx.getCurrentProjectProjectPath()
|
|
38
|
+
if (!(cwd != null && cwd !== '')) return
|
|
39
|
+
const result = await $`git -C ${cwd} ${args}`.quiet().nothrow()
|
|
40
|
+
if (result.exitCode !== 0) {
|
|
41
|
+
const stderr = result.stderr.toString().trim()
|
|
42
|
+
ctx.dispatch({ message: stderr || 'git action failed', type: 'git-mode-set-message' })
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
ctx.dispatch({ message: null, type: 'git-mode-set-message' })
|
|
46
|
+
if (pathsToInvalidate.length > 0) {
|
|
47
|
+
ctx.dispatch({ paths: pathsToInvalidate, type: 'git-mode-invalidate-diffs' })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runGitRm(ctx: SideEffectContext, path: string): Promise<void> {
|
|
52
|
+
const repoPath = ctx.state.gitPanel.files.find((f) => f.path === path)?.repoPath
|
|
53
|
+
const cwd = repoPath ?? ctx.getCurrentProjectProjectPath()
|
|
54
|
+
if (!(cwd != null && cwd !== '')) return
|
|
55
|
+
const absolute = `${cwd}/${path}`
|
|
56
|
+
try {
|
|
57
|
+
const stat = await Bun.file(absolute).stat()
|
|
58
|
+
await (stat.isDirectory()
|
|
59
|
+
? Bun.$`rm -rf -- ${absolute}`.quiet().nothrow()
|
|
60
|
+
: Bun.file(absolute).unlink())
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : 'failed to delete file'
|
|
63
|
+
ctx.dispatch({ message, type: 'git-mode-set-message' })
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
ctx.dispatch({ message: null, type: 'git-mode-set-message' })
|
|
67
|
+
ctx.dispatch({ path, type: 'git-mode-clear-diff-cache' })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runGitCommit(
|
|
71
|
+
ctx: SideEffectContext,
|
|
72
|
+
title: string,
|
|
73
|
+
body: string
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
const cwd = ctx.getCurrentProjectProjectPath()
|
|
76
|
+
if (!(cwd != null && cwd !== '')) return
|
|
77
|
+
if (!title) {
|
|
78
|
+
ctx.dispatch({ message: 'empty commit title', type: 'git-mode-set-message' })
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
const result = body
|
|
82
|
+
? await $`git -C ${cwd} commit -m ${title} -m ${body}`.quiet().nothrow()
|
|
83
|
+
: await $`git -C ${cwd} commit -m ${title}`.quiet().nothrow()
|
|
84
|
+
if (result.exitCode !== 0) {
|
|
85
|
+
const stderr = result.stderr.toString().trim()
|
|
86
|
+
ctx.dispatch({ message: null, type: 'git-mode-set-message' })
|
|
87
|
+
toast.error(stderr || 'Commit failed')
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
clearAutoCommitForCurrentProject(ctx)
|
|
91
|
+
// Match the push flow: clear any inline git-pane message and surface the
|
|
92
|
+
// result as a toast so it's seen even after leaving git mode.
|
|
93
|
+
ctx.dispatch({ message: null, type: 'git-mode-set-message' })
|
|
94
|
+
toast.success(`Committed: ${title}`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function runGitCommitAuto(
|
|
98
|
+
ctx: SideEffectContext,
|
|
99
|
+
title: string,
|
|
100
|
+
body: string
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
if (!title) {
|
|
103
|
+
ctx.dispatch({ message: 'empty commit title', type: 'git-mode-set-message' })
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
const cwd = ctx.getCurrentProjectProjectPath()
|
|
107
|
+
if (!(cwd != null && cwd !== '')) return
|
|
108
|
+
|
|
109
|
+
// If the user has manually staged files, respect that intent and commit
|
|
110
|
+
// only the staged set — don't run `git add -A` which would sweep up
|
|
111
|
+
// unrelated unstaged/untracked changes. With nothing staged, `add -A`
|
|
112
|
+
// keeps the "commit everything" behaviour the user expects from auto-commit.
|
|
113
|
+
const hasStaged = ctx.state.gitPanel.files.some((f) => f.section === 'staged')
|
|
114
|
+
if (!hasStaged) {
|
|
115
|
+
const addArgs = ['add', '-A']
|
|
116
|
+
const addResult = await $`git -C ${cwd} ${addArgs}`.quiet().nothrow()
|
|
117
|
+
if (addResult.exitCode !== 0) {
|
|
118
|
+
ctx.dispatch({ message: null, type: 'git-mode-set-message' })
|
|
119
|
+
toast.error(addResult.stderr.toString().trim() || 'Auto-commit: git add failed')
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const commitResult = body
|
|
125
|
+
? await $`git -C ${cwd} commit -m ${title} -m ${body}`.quiet().nothrow()
|
|
126
|
+
: await $`git -C ${cwd} commit -m ${title}`.quiet().nothrow()
|
|
127
|
+
|
|
128
|
+
if (commitResult.exitCode !== 0) {
|
|
129
|
+
ctx.dispatch({ message: null, type: 'git-mode-set-message' })
|
|
130
|
+
toast.error(commitResult.stderr.toString().trim() || 'Auto-commit: commit failed')
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
clearAutoCommitForCurrentProject(ctx)
|
|
135
|
+
ctx.dispatch({ message: `committed: ${title}`, type: 'git-mode-set-message' })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function clearAutoCommitForCurrentProject(ctx: SideEffectContext): void {
|
|
139
|
+
const projectId = ctx.state.currentProjectId
|
|
140
|
+
if (!(projectId != null && projectId !== '')) return
|
|
141
|
+
ctx.dispatch({ projectId, type: 'auto-commit-clear' })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function runGenerateAutoCommitNow(
|
|
145
|
+
ctx: SideEffectContext,
|
|
146
|
+
projectId: string
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
const project = ctx.state.projects.find((s) => s.id === projectId)
|
|
149
|
+
const panel = ctx.state.gitPanel
|
|
150
|
+
if (panel.error !== null) {
|
|
151
|
+
toast.warning('Auto-commit: git panel unavailable')
|
|
152
|
+
ctx.dispatch({ projectId, type: 'auto-commit-clear' })
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
const tab = ctx.activeTab
|
|
156
|
+
if (!tab) {
|
|
157
|
+
toast.warning('Auto-commit: no active assistant tab — open a claude/codex tab first')
|
|
158
|
+
ctx.dispatch({ projectId, type: 'auto-commit-clear' })
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
await triggerAutoCommitNow({
|
|
162
|
+
assistant: tab.assistant,
|
|
163
|
+
git: {
|
|
164
|
+
ahead: panel.ahead,
|
|
165
|
+
behind: panel.behind,
|
|
166
|
+
branch: panel.branch,
|
|
167
|
+
files: panel.files,
|
|
168
|
+
},
|
|
169
|
+
projectId,
|
|
170
|
+
projectPath: project?.projectPath,
|
|
171
|
+
tabId: tab.id,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function runGitPush(ctx: SideEffectContext): Promise<void> {
|
|
176
|
+
const cwd = ctx.getCurrentProjectProjectPath()
|
|
177
|
+
if (!(cwd != null && cwd !== '')) return
|
|
178
|
+
ctx.dispatch({ message: 'pushing…', type: 'git-mode-set-message' })
|
|
179
|
+
|
|
180
|
+
const upstream = await $`git -C ${cwd} rev-parse --abbrev-ref --symbolic-full-name @{u}`
|
|
181
|
+
.quiet()
|
|
182
|
+
.nothrow()
|
|
183
|
+
const hasUpstream = upstream.exitCode === 0
|
|
184
|
+
|
|
185
|
+
const result = hasUpstream
|
|
186
|
+
? await $`git -C ${cwd} push`.quiet().nothrow()
|
|
187
|
+
: await $`git -C ${cwd} push --set-upstream origin HEAD`.quiet().nothrow()
|
|
188
|
+
|
|
189
|
+
// Clear the inline "pushing…" progress; surface the result as a toast so it's
|
|
190
|
+
// visible even after leaving git mode (and so push failures aren't missed).
|
|
191
|
+
ctx.dispatch({ message: null, type: 'git-mode-set-message' })
|
|
192
|
+
if (result.exitCode !== 0) {
|
|
193
|
+
toast.error(result.stderr.toString().trim() || 'Push failed')
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
toast.success('Pushed')
|
|
197
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MeasuredPaneRect } from './use-pane-size-report'
|
|
2
|
+
|
|
3
|
+
// `onMeasure` reaches TerminalPane as a prop chain from app.tsx through
|
|
4
|
+
// root.tsx. A widget hosting its own PTY sits in the bar tree, which that chain
|
|
5
|
+
// never touches — same problem `dispatchGlobal` solves for actions, same shape
|
|
6
|
+
// of answer. Going straight to backend.resizeTab instead would mean
|
|
7
|
+
// re-implementing the PTY_MIN clamps and the per-tab dedupe that handleMeasure
|
|
8
|
+
// owns, and a mismatch there makes the resize loop never settle.
|
|
9
|
+
type MeasureFn = (tabId: string, rect: MeasuredPaneRect) => void
|
|
10
|
+
|
|
11
|
+
let activeMeasure: MeasureFn | null = null
|
|
12
|
+
|
|
13
|
+
export function setActiveMeasure(measure: MeasureFn | null): void {
|
|
14
|
+
activeMeasure = measure
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function measureGlobal(tabId: string, rect: MeasuredPaneRect): void {
|
|
18
|
+
activeMeasure?.(tabId, rect)
|
|
19
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { AppState } from '../state/types'
|
|
2
|
+
import type { SideEffectContext } from './side-effect-context'
|
|
3
|
+
|
|
4
|
+
import { logInputDebug } from '../debug/input-log'
|
|
5
|
+
import { appStore } from '../state/app-store'
|
|
6
|
+
import { saveProjectCatalog } from '../state/project-catalog'
|
|
7
|
+
import {
|
|
8
|
+
filterTabsForActiveWorkspace,
|
|
9
|
+
getPrimaryWorkspace,
|
|
10
|
+
withActiveWorkspace,
|
|
11
|
+
} from '../state/project-workspaces'
|
|
12
|
+
import { appReducer } from '../state/store'
|
|
13
|
+
import { buildTabEntries } from '../state/tab-entries'
|
|
14
|
+
import { switchProjectRecords } from './project-actions'
|
|
15
|
+
|
|
16
|
+
export function handleSwitchProjectByIndex(
|
|
17
|
+
ctx: SideEffectContext,
|
|
18
|
+
index: number,
|
|
19
|
+
workspaceId?: string
|
|
20
|
+
): void {
|
|
21
|
+
const { backend, dispatch } = ctx
|
|
22
|
+
// Read fresh state. ctx.state is the snapshot from the previous render and
|
|
23
|
+
// lags behind dispatches that happened in the same JS turn.
|
|
24
|
+
const state = ctx.getState()
|
|
25
|
+
const ordered = [...state.projects].sort(
|
|
26
|
+
(a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER)
|
|
27
|
+
)
|
|
28
|
+
const target = ordered[index - 1]
|
|
29
|
+
if (!target) {
|
|
30
|
+
logInputDebug('app.projectBar.switchOutOfRange', { index, total: ordered.length })
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Resolve which workspace to land on. If the caller passed an explicit
|
|
35
|
+
// `workspaceId` (project-row tap → its primary, workspace-row tap → that
|
|
36
|
+
// workspace), honor it; otherwise let the target project keep its persisted
|
|
37
|
+
// activeWorkspaceId.
|
|
38
|
+
const resolvedWorkspaceId =
|
|
39
|
+
workspaceId != null &&
|
|
40
|
+
workspaceId !== '' &&
|
|
41
|
+
(target.workspaces?.some((w) => w.id === workspaceId) ?? false)
|
|
42
|
+
? workspaceId
|
|
43
|
+
: undefined
|
|
44
|
+
const needsWorkspaceChange =
|
|
45
|
+
resolvedWorkspaceId != null && resolvedWorkspaceId !== target.activeWorkspaceId
|
|
46
|
+
|
|
47
|
+
if (target.id === state.currentProjectId) {
|
|
48
|
+
if (needsWorkspaceChange) {
|
|
49
|
+
dispatch({
|
|
50
|
+
projectId: target.id,
|
|
51
|
+
type: 'set-active-workspace',
|
|
52
|
+
workspaceId: resolvedWorkspaceId,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
if (state.focusMode === 'git') {
|
|
56
|
+
dispatch({ type: 'exit-git-mode' })
|
|
57
|
+
}
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Cross-project: bundle the workspace change into the project record AND
|
|
62
|
+
// fold set-projects + load-project into a SINGLE setState call. Otherwise
|
|
63
|
+
// any subscriber notification (re-render, useEffect, backend re-attach)
|
|
64
|
+
// between dispatches can re-assert the project's previously-persisted
|
|
65
|
+
// activeWorkspaceId, dropping the user back on the last-visited workspace.
|
|
66
|
+
const patchedProject = needsWorkspaceChange
|
|
67
|
+
? withActiveWorkspace(target, resolvedWorkspaceId)
|
|
68
|
+
: target
|
|
69
|
+
const patchedState: AppState = needsWorkspaceChange
|
|
70
|
+
? {
|
|
71
|
+
...state,
|
|
72
|
+
projects: state.projects.map((s) => (s.id === patchedProject.id ? patchedProject : s)),
|
|
73
|
+
}
|
|
74
|
+
: state
|
|
75
|
+
const projects = switchProjectRecords(patchedState, patchedProject)
|
|
76
|
+
saveProjectCatalog(projects)
|
|
77
|
+
void backend.destroy(true)
|
|
78
|
+
appStore.setState((current) => {
|
|
79
|
+
const afterSet = appReducer(current, { projects, type: 'set-projects' })
|
|
80
|
+
return appReducer(afterSet, {
|
|
81
|
+
forceDisconnected: false,
|
|
82
|
+
projectId: patchedProject.id,
|
|
83
|
+
projectSnapshot: patchedProject.projectSnapshot,
|
|
84
|
+
type: 'load-project',
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* One stop for `j`/`k`, and it is always a workspace — the project's own row is
|
|
91
|
+
* a heading, not a destination. Every project owns a primary (every path into
|
|
92
|
+
* the catalog runs `ensureProjectWorkspaces`), so no project can be skipped by
|
|
93
|
+
* having nothing to land on.
|
|
94
|
+
*/
|
|
95
|
+
interface SidebarItem {
|
|
96
|
+
projectId: string
|
|
97
|
+
workspaceId: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildSidebarItems(state: AppState): SidebarItem[] {
|
|
101
|
+
const ordered = [...state.projects].sort(
|
|
102
|
+
(a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER)
|
|
103
|
+
)
|
|
104
|
+
const items: SidebarItem[] = []
|
|
105
|
+
for (const project of ordered) {
|
|
106
|
+
for (const workspace of project.workspaces ?? []) {
|
|
107
|
+
items.push({ projectId: project.id, workspaceId: workspace.id })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return items
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function findCurrentSidebarItem(state: AppState, items: SidebarItem[]): number {
|
|
114
|
+
const projectId = state.currentProjectId
|
|
115
|
+
if (projectId == null || projectId === '') return -1
|
|
116
|
+
const project = state.projects.find((s) => s.id === projectId)
|
|
117
|
+
// An unset active workspace means the project sits on its checkout, which now
|
|
118
|
+
// has a row of its own to land on.
|
|
119
|
+
const activeWorkspaceId =
|
|
120
|
+
project?.activeWorkspaceId ?? getPrimaryWorkspace(project?.workspaces)?.id
|
|
121
|
+
if (activeWorkspaceId == null || activeWorkspaceId === '') return -1
|
|
122
|
+
return items.findIndex(
|
|
123
|
+
(item) => item.projectId === projectId && item.workspaceId === activeWorkspaceId
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function handleCycleSidebarItem(ctx: SideEffectContext, direction: 1 | -1): void {
|
|
128
|
+
const { backend, dispatch } = ctx
|
|
129
|
+
// Read fresh from the store, not ctx.state (which is a per-render
|
|
130
|
+
// snapshot). Rapid key presses fire before React re-renders, so ctx.state
|
|
131
|
+
// can lag the actual store.
|
|
132
|
+
const state = appStore.getState()
|
|
133
|
+
const items = buildSidebarItems(state)
|
|
134
|
+
if (items.length === 0) return
|
|
135
|
+
const currentIdx = findCurrentSidebarItem(state, items)
|
|
136
|
+
// If we don't know the current, jump to first/last depending on direction.
|
|
137
|
+
let startIdx: number
|
|
138
|
+
if (currentIdx >= 0) {
|
|
139
|
+
startIdx = currentIdx
|
|
140
|
+
} else {
|
|
141
|
+
startIdx = direction === 1 ? -1 : 0
|
|
142
|
+
}
|
|
143
|
+
const len = items.length
|
|
144
|
+
const target = items[(((startIdx + direction) % len) + len) % len]
|
|
145
|
+
if (!target) return
|
|
146
|
+
|
|
147
|
+
const project = state.projects.find((s) => s.id === target.projectId)
|
|
148
|
+
if (!project) return
|
|
149
|
+
|
|
150
|
+
const targetWorkspaceId = target.workspaceId
|
|
151
|
+
const isCrossProject = project.id !== state.currentProjectId
|
|
152
|
+
const needsWorkspaceChange = targetWorkspaceId !== project.activeWorkspaceId
|
|
153
|
+
|
|
154
|
+
if (isCrossProject) {
|
|
155
|
+
// Bundle the workspace change into the project record AND fold the
|
|
156
|
+
// project switch's two dispatches (set-projects + load-project) into a
|
|
157
|
+
// SINGLE Zustand setState call — otherwise each dispatch fires a
|
|
158
|
+
// separate subscription notification and the @opentui/react reconciler
|
|
159
|
+
// paints an intermediate frame where the new project is current but
|
|
160
|
+
// the old activeWorkspaceId still holds, producing the visible flicker.
|
|
161
|
+
const patchedProject = needsWorkspaceChange
|
|
162
|
+
? withActiveWorkspace(project, targetWorkspaceId)
|
|
163
|
+
: project
|
|
164
|
+
const patchedState: AppState = needsWorkspaceChange
|
|
165
|
+
? {
|
|
166
|
+
...state,
|
|
167
|
+
projects: state.projects.map((s) => (s.id === patchedProject.id ? patchedProject : s)),
|
|
168
|
+
}
|
|
169
|
+
: state
|
|
170
|
+
const projects = switchProjectRecords(patchedState, patchedProject)
|
|
171
|
+
saveProjectCatalog(projects)
|
|
172
|
+
void backend.destroy(true)
|
|
173
|
+
appStore.setState((current) => {
|
|
174
|
+
const afterSet = appReducer(current, { projects, type: 'set-projects' })
|
|
175
|
+
return appReducer(afterSet, {
|
|
176
|
+
// Daemon is alive and attach() will hydrate real statuses within a
|
|
177
|
+
// frame, so skip the snapshot's running→disconnected downgrade —
|
|
178
|
+
// otherwise the "Restored snapshot" hint flashes on every j/k cycle.
|
|
179
|
+
forceDisconnected: false,
|
|
180
|
+
projectId: patchedProject.id,
|
|
181
|
+
projectSnapshot: patchedProject.projectSnapshot,
|
|
182
|
+
type: 'load-project',
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (needsWorkspaceChange) {
|
|
189
|
+
dispatch({
|
|
190
|
+
projectId: project.id,
|
|
191
|
+
type: 'set-active-workspace',
|
|
192
|
+
workspaceId: targetWorkspaceId,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function handleSwitchTabByIndex(ctx: SideEffectContext, index: number): void {
|
|
198
|
+
const { dispatch, state } = ctx
|
|
199
|
+
const currentProject =
|
|
200
|
+
state.currentProjectId != null && state.currentProjectId !== ''
|
|
201
|
+
? state.projects.find((s) => s.id === state.currentProjectId)
|
|
202
|
+
: undefined
|
|
203
|
+
const visible = filterTabsForActiveWorkspace(state.tabs, currentProject)
|
|
204
|
+
const entries = buildTabEntries(visible, state.layoutTrees, state.tabGroupMap, state.activeTabId)
|
|
205
|
+
const target = entries[index - 1]
|
|
206
|
+
if (!target) {
|
|
207
|
+
logInputDebug('app.tabBar.switchOutOfRange', { index, total: entries.length })
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
const targetTabId = target.kind === 'single' ? target.tab.id : target.activeLeafId
|
|
211
|
+
if (targetTabId === state.activeTabId) return
|
|
212
|
+
dispatch({ tabId: targetTabId, type: 'set-active-tab' })
|
|
213
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { SessionBackend } from '../session-backend/types'
|
|
2
|
+
import type { AppAction } from '../state/actions'
|
|
3
|
+
import type { AppState, ProjectRecord, TabSession } from '../state/types'
|
|
4
|
+
|
|
5
|
+
import { logInputDebug } from '../debug/input-log'
|
|
6
|
+
import { createPrefixedId } from '../platform/id'
|
|
7
|
+
import { saveProjectCatalog } from '../state/project-catalog'
|
|
8
|
+
import { createEmptyProjectSnapshot, serializeProject } from '../state/project-persistence'
|
|
9
|
+
import { buildProjectsWithCurrentSnapshot } from '../state/project-save'
|
|
10
|
+
import { createPrimaryWorkspace, ensureProjectWorkspaces } from '../state/project-workspaces'
|
|
11
|
+
import { toast } from '../state/toast-store'
|
|
12
|
+
|
|
13
|
+
export function createProjectFromCurrentState(
|
|
14
|
+
state: AppState,
|
|
15
|
+
name: string,
|
|
16
|
+
projectPath?: string
|
|
17
|
+
): { project: ProjectRecord; projects: ProjectRecord[] } {
|
|
18
|
+
const now = new Date().toISOString()
|
|
19
|
+
const projectSnapshot =
|
|
20
|
+
(state.currentProjectId != null && state.currentProjectId !== '') || state.tabs.length === 0
|
|
21
|
+
? createEmptyProjectSnapshot()
|
|
22
|
+
: serializeProject(state)
|
|
23
|
+
const project: ProjectRecord = {
|
|
24
|
+
activeWorkspaceId: undefined,
|
|
25
|
+
createdAt: now,
|
|
26
|
+
id: createPrefixedId('project'),
|
|
27
|
+
lastOpenedAt: now,
|
|
28
|
+
name,
|
|
29
|
+
projectPath,
|
|
30
|
+
projectSnapshot,
|
|
31
|
+
updatedAt: now,
|
|
32
|
+
workspaces: undefined,
|
|
33
|
+
}
|
|
34
|
+
if (projectPath != null && projectPath !== '') {
|
|
35
|
+
const workspace = createPrimaryWorkspace(projectPath, now)
|
|
36
|
+
project.activeWorkspaceId = workspace.id
|
|
37
|
+
project.workspaces = [workspace]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let updatedProjects = state.projects
|
|
41
|
+
if (state.currentProjectId != null && state.currentProjectId !== '') {
|
|
42
|
+
const currentSnapshot = serializeProject(state)
|
|
43
|
+
updatedProjects = state.projects.map((entry) =>
|
|
44
|
+
entry.id === state.currentProjectId
|
|
45
|
+
? { ...entry, projectSnapshot: currentSnapshot, updatedAt: now }
|
|
46
|
+
: entry
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
project,
|
|
52
|
+
projects: [...updatedProjects, ensureProjectWorkspaces(project)],
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function renameProjectRecords(
|
|
57
|
+
projects: ProjectRecord[],
|
|
58
|
+
projectId: string,
|
|
59
|
+
name: string
|
|
60
|
+
): ProjectRecord[] {
|
|
61
|
+
return projects.map((project) =>
|
|
62
|
+
project.id === projectId ? { ...project, name, updatedAt: new Date().toISOString() } : project
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function switchProjectRecords(state: AppState, project: ProjectRecord): ProjectRecord[] {
|
|
67
|
+
const projectsWithSnapshot = buildProjectsWithCurrentSnapshot(
|
|
68
|
+
state.projects,
|
|
69
|
+
state.currentProjectId,
|
|
70
|
+
state
|
|
71
|
+
)
|
|
72
|
+
return projectsWithSnapshot.map((entry) =>
|
|
73
|
+
entry.id === project.id ? { ...entry, lastOpenedAt: new Date().toISOString() } : entry
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function deleteProjectRecords(
|
|
78
|
+
projects: ProjectRecord[],
|
|
79
|
+
projectId: string
|
|
80
|
+
): ProjectRecord[] {
|
|
81
|
+
return projects.filter((project) => project.id !== projectId)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function handleCreateProjectEffect(
|
|
85
|
+
state: AppState,
|
|
86
|
+
dispatch: (action: AppAction) => void,
|
|
87
|
+
name: string,
|
|
88
|
+
projectPath?: string
|
|
89
|
+
): void {
|
|
90
|
+
const { project, projects } = createProjectFromCurrentState(state, name, projectPath)
|
|
91
|
+
logInputDebug('app.project.create', {
|
|
92
|
+
fromCurrentProject:
|
|
93
|
+
(state.currentProjectId == null || state.currentProjectId === '') && state.tabs.length > 0,
|
|
94
|
+
name,
|
|
95
|
+
projectId: project.id,
|
|
96
|
+
tabCount: project.projectSnapshot?.tabs.length ?? 0,
|
|
97
|
+
})
|
|
98
|
+
saveProjectCatalog(projects)
|
|
99
|
+
dispatch({ projects, type: 'set-projects' })
|
|
100
|
+
dispatch({
|
|
101
|
+
projectId: project.id,
|
|
102
|
+
projectSnapshot: project.projectSnapshot,
|
|
103
|
+
type: 'load-project',
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function handleRenameProjectEffect(
|
|
108
|
+
projects: ProjectRecord[],
|
|
109
|
+
dispatch: (action: AppAction) => void,
|
|
110
|
+
projectId: string,
|
|
111
|
+
name: string
|
|
112
|
+
): void {
|
|
113
|
+
logInputDebug('app.project.rename', { name, projectId })
|
|
114
|
+
const renamed = renameProjectRecords(projects, projectId, name)
|
|
115
|
+
saveProjectCatalog(renamed)
|
|
116
|
+
dispatch({ name, projectId, type: 'rename-project-record' })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function handleSwitchProjectEffect(
|
|
120
|
+
state: AppState,
|
|
121
|
+
backend: SessionBackend,
|
|
122
|
+
dispatch: (action: AppAction) => void,
|
|
123
|
+
project: ProjectRecord
|
|
124
|
+
): void {
|
|
125
|
+
logInputDebug('app.project.switch.start', {
|
|
126
|
+
currentTabCount: state.tabs.length,
|
|
127
|
+
fromProjectId: state.currentProjectId,
|
|
128
|
+
restoredTabCount: project.projectSnapshot?.tabs.length ?? 0,
|
|
129
|
+
toName: project.name,
|
|
130
|
+
toProjectId: project.id,
|
|
131
|
+
})
|
|
132
|
+
const projects = switchProjectRecords(state, project)
|
|
133
|
+
saveProjectCatalog(projects)
|
|
134
|
+
void backend.destroy(true)
|
|
135
|
+
dispatch({ projects, type: 'set-projects' })
|
|
136
|
+
dispatch({
|
|
137
|
+
projectId: project.id,
|
|
138
|
+
projectSnapshot: project.projectSnapshot,
|
|
139
|
+
type: 'load-project',
|
|
140
|
+
})
|
|
141
|
+
logInputDebug('app.project.switch.dispatched', { toProjectId: project.id })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function handleDeleteProjectEffect(
|
|
145
|
+
state: AppState,
|
|
146
|
+
backend: SessionBackend,
|
|
147
|
+
dispatch: (action: AppAction) => void,
|
|
148
|
+
projectId: string,
|
|
149
|
+
options?: { openProjectPicker?: boolean }
|
|
150
|
+
): void {
|
|
151
|
+
const name = state.projects.find((entry) => entry.id === projectId)?.name
|
|
152
|
+
const remaining = deleteProjectRecords(state.projects, projectId)
|
|
153
|
+
logInputDebug('app.project.delete', {
|
|
154
|
+
projectId,
|
|
155
|
+
remainingCount: remaining.length,
|
|
156
|
+
wasCurrent: projectId === state.currentProjectId,
|
|
157
|
+
})
|
|
158
|
+
saveProjectCatalog(remaining)
|
|
159
|
+
if (projectId === state.currentProjectId) {
|
|
160
|
+
void backend.destroy(true)
|
|
161
|
+
}
|
|
162
|
+
dispatch({
|
|
163
|
+
openProjectPicker: options?.openProjectPicker,
|
|
164
|
+
projectId,
|
|
165
|
+
type: 'delete-project-record',
|
|
166
|
+
})
|
|
167
|
+
toast.success(name != null && name !== '' ? `Deleted project "${name}"` : 'Project deleted')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function restartTabSession(
|
|
171
|
+
backend: SessionBackend,
|
|
172
|
+
dispatch: (action: AppAction) => void,
|
|
173
|
+
clearIdleTimer: (tabId: string) => void,
|
|
174
|
+
clearStartupGrace: (tabId: string) => void,
|
|
175
|
+
startTabSession: (tab: TabSession) => void,
|
|
176
|
+
tab: TabSession
|
|
177
|
+
): void {
|
|
178
|
+
logInputDebug('app.restartTab', {
|
|
179
|
+
command: tab.command,
|
|
180
|
+
status: tab.status,
|
|
181
|
+
tabId: tab.id,
|
|
182
|
+
})
|
|
183
|
+
clearIdleTimer(tab.id)
|
|
184
|
+
clearStartupGrace(tab.id)
|
|
185
|
+
backend.disposeSession(tab.id)
|
|
186
|
+
dispatch({ tabId: tab.id, type: 'reset-tab-project' })
|
|
187
|
+
startTabSession(tab)
|
|
188
|
+
}
|