@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
|
@@ -3,7 +3,7 @@ import type { ModeId, ResolvedKeymapConfig } from '@brimveyn/aimux-config'
|
|
|
3
3
|
import type { AppState } from '../state/types'
|
|
4
4
|
|
|
5
5
|
import { describeBindings } from '../input/keymap/describe-bindings'
|
|
6
|
-
import {
|
|
6
|
+
import { getActiveWorkspacePath } from '../state/project-workspaces'
|
|
7
7
|
import { buildHintText } from './keymap-context'
|
|
8
8
|
import { abbreviatePath } from './path-format'
|
|
9
9
|
|
|
@@ -16,26 +16,32 @@ export interface IdentitySegment {
|
|
|
16
16
|
export interface StatusBarModel {
|
|
17
17
|
help: string
|
|
18
18
|
right: string
|
|
19
|
-
|
|
19
|
+
projectSegments: IdentitySegment[]
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const HINT_LIMIT = 6
|
|
23
23
|
const HELP_DESCRIPTION = 'Help'
|
|
24
24
|
const SEP = ' · '
|
|
25
25
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
function projectSegments(
|
|
27
|
+
projectName: string,
|
|
28
|
+
projectPath: string | null | undefined
|
|
29
29
|
): IdentitySegment[] {
|
|
30
|
-
const segs: IdentitySegment[] = [{ id: '
|
|
31
|
-
if (
|
|
32
|
-
segs.push({ id: 'sep-
|
|
33
|
-
segs.push({ id: 'path', text: abbreviatePath(
|
|
30
|
+
const segs: IdentitySegment[] = [{ id: 'project', text: projectName, tone: 'primary' }]
|
|
31
|
+
if (projectPath != null && projectPath !== '') {
|
|
32
|
+
segs.push({ id: 'sep-project-path', text: SEP, tone: 'muted' })
|
|
33
|
+
segs.push({ id: 'path', text: abbreviatePath(projectPath), tone: 'muted' })
|
|
34
34
|
}
|
|
35
35
|
return segs
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const STAGING_DESCRIPTIONS = ['Stage', 'Unstage/delete', 'Commit', 'Push']
|
|
39
|
+
/**
|
|
40
|
+
* Moving around a two-column screen is the one thing you can see from the screen,
|
|
41
|
+
* so the bar spends its six slots on what you cannot: searching, resetting, and
|
|
42
|
+
* how to change the thing under the cursor.
|
|
43
|
+
*/
|
|
44
|
+
const SETTINGS_OBVIOUS_DESCRIPTIONS = ['Sections', 'Settings of the section', 'Next', 'Prev']
|
|
39
45
|
|
|
40
46
|
function hintForMode(config: ResolvedKeymapConfig, modeId: ModeId): string {
|
|
41
47
|
return buildHintText(config, modeId, HINT_LIMIT, { excludeDescriptions: [HELP_DESCRIPTION] })
|
|
@@ -57,27 +63,27 @@ function helpHintForMode(config: ResolvedKeymapConfig, modeId: ModeId): string {
|
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
export function getStatusBarModel(state: AppState, config: ResolvedKeymapConfig): StatusBarModel {
|
|
60
|
-
const
|
|
61
|
-
state.
|
|
62
|
-
? state.
|
|
66
|
+
const currentProject =
|
|
67
|
+
state.currentProjectId != null && state.currentProjectId !== ''
|
|
68
|
+
? state.projects.find((project) => project.id === state.currentProjectId)
|
|
63
69
|
: undefined
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
70
|
+
const projectName = currentProject?.name ?? 'no project'
|
|
71
|
+
const projectPath = getActiveWorkspacePath(currentProject)
|
|
72
|
+
const projectSegs = projectSegments(projectName, projectPath)
|
|
67
73
|
|
|
68
74
|
switch (state.focusMode) {
|
|
69
75
|
case 'terminal-input':
|
|
70
76
|
return {
|
|
71
77
|
help: '',
|
|
78
|
+
projectSegments: projectSegs,
|
|
72
79
|
right: hintForMode(config, 'terminal-input'),
|
|
73
|
-
sessionSegments: sessionSegs,
|
|
74
80
|
}
|
|
75
81
|
case 'modal': {
|
|
76
82
|
const modalMode = deriveModalModeId(state.modal.type)
|
|
77
83
|
return {
|
|
78
84
|
help: '',
|
|
85
|
+
projectSegments: projectSegs,
|
|
79
86
|
right: modalMode ? hintForMode(config, modalMode) : '',
|
|
80
|
-
sessionSegments: sessionSegs,
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
case 'git': {
|
|
@@ -93,24 +99,32 @@ export function getStatusBarModel(state: AppState, config: ResolvedKeymapConfig)
|
|
|
93
99
|
}
|
|
94
100
|
return {
|
|
95
101
|
help: helpHintForMode(config, 'git-mode'),
|
|
102
|
+
projectSegments: [...projectSegs, ...extras],
|
|
96
103
|
right: hintForGitMode(config, headOffset),
|
|
97
|
-
sessionSegments: [...sessionSegs, ...extras],
|
|
98
104
|
}
|
|
99
105
|
}
|
|
106
|
+
case 'settings':
|
|
107
|
+
return {
|
|
108
|
+
help: helpHintForMode(config, 'settings'),
|
|
109
|
+
projectSegments: projectSegs,
|
|
110
|
+
right: buildHintText(config, 'settings', HINT_LIMIT, {
|
|
111
|
+
excludeDescriptions: [HELP_DESCRIPTION, ...SETTINGS_OBVIOUS_DESCRIPTIONS],
|
|
112
|
+
}),
|
|
113
|
+
}
|
|
100
114
|
case 'command-edit': {
|
|
101
115
|
const commandEditMode = deriveCommandEditModeId(state.modal.type)
|
|
102
116
|
return {
|
|
103
117
|
help: '',
|
|
118
|
+
projectSegments: projectSegs,
|
|
104
119
|
right: commandEditMode ? hintForMode(config, commandEditMode) : '',
|
|
105
|
-
sessionSegments: sessionSegs,
|
|
106
120
|
}
|
|
107
121
|
}
|
|
108
122
|
case 'navigation':
|
|
109
123
|
default:
|
|
110
124
|
return {
|
|
111
125
|
help: helpHintForMode(config, 'navigation'),
|
|
126
|
+
projectSegments: projectSegs,
|
|
112
127
|
right: hintForMode(config, 'navigation'),
|
|
113
|
-
sessionSegments: sessionSegs,
|
|
114
128
|
}
|
|
115
129
|
}
|
|
116
130
|
}
|
|
@@ -121,8 +135,8 @@ function deriveModalModeId(modalType: AppState['modal']['type']): ModeId | null
|
|
|
121
135
|
return 'modal.help.filtering'
|
|
122
136
|
case 'new-tab':
|
|
123
137
|
return 'modal.new-tab.command-edit'
|
|
124
|
-
case '
|
|
125
|
-
return 'modal.
|
|
138
|
+
case 'project-picker':
|
|
139
|
+
return 'modal.project-picker.filtering'
|
|
126
140
|
case 'snippet-picker':
|
|
127
141
|
return 'modal.snippet-picker.filtering'
|
|
128
142
|
case 'split-picker':
|
|
@@ -131,10 +145,10 @@ function deriveModalModeId(modalType: AppState['modal']['type']): ModeId | null
|
|
|
131
145
|
return 'modal.theme-picker.filtering'
|
|
132
146
|
case 'update-available':
|
|
133
147
|
return 'modal.update-available'
|
|
134
|
-
case '
|
|
135
|
-
return 'modal.
|
|
136
|
-
case '
|
|
137
|
-
return 'modal.
|
|
148
|
+
case 'workspace-move':
|
|
149
|
+
return 'modal.workspace-move'
|
|
150
|
+
case 'workspace-move-confirm':
|
|
151
|
+
return 'modal.workspace-move-confirm'
|
|
138
152
|
default:
|
|
139
153
|
return null
|
|
140
154
|
}
|
|
@@ -142,20 +156,20 @@ function deriveModalModeId(modalType: AppState['modal']['type']): ModeId | null
|
|
|
142
156
|
|
|
143
157
|
function deriveCommandEditModeId(modalType: AppState['modal']['type']): ModeId | null {
|
|
144
158
|
switch (modalType) {
|
|
145
|
-
case 'create-
|
|
146
|
-
return 'modal.create-
|
|
159
|
+
case 'create-project':
|
|
160
|
+
return 'modal.create-project'
|
|
147
161
|
case 'git-commit':
|
|
148
162
|
return 'modal.git-commit'
|
|
149
163
|
case 'new-tab':
|
|
150
164
|
return 'modal.new-tab.command-edit'
|
|
151
165
|
case 'rename-tab':
|
|
152
166
|
return 'modal.rename-tab'
|
|
153
|
-
case 'rename-
|
|
154
|
-
return 'modal.rename-
|
|
155
|
-
case '
|
|
156
|
-
return 'modal.
|
|
157
|
-
case '
|
|
158
|
-
return 'modal.
|
|
167
|
+
case 'rename-workspace':
|
|
168
|
+
return 'modal.rename-workspace'
|
|
169
|
+
case 'project-name':
|
|
170
|
+
return 'modal.project-name'
|
|
171
|
+
case 'project-picker':
|
|
172
|
+
return 'modal.project-picker.filtering'
|
|
159
173
|
case 'snippet-editor':
|
|
160
174
|
return 'modal.snippet-editor'
|
|
161
175
|
case 'snippet-picker':
|
|
@@ -16,7 +16,7 @@ let nextId = 0x100000
|
|
|
16
16
|
|
|
17
17
|
export function nextImageId(): number {
|
|
18
18
|
const id = nextId++
|
|
19
|
-
// Wrap at 24-bit; we don't expect to leak more than ~16M IDs per
|
|
19
|
+
// Wrap at 24-bit; we don't expect to leak more than ~16M IDs per project but
|
|
20
20
|
// be safe in case of a long-running daemon.
|
|
21
21
|
if (nextId > 0xffffff) nextId = 0x100000
|
|
22
22
|
return id
|
|
@@ -46,10 +46,16 @@ function chunkString(s: string, size: number): string[] {
|
|
|
46
46
|
|
|
47
47
|
// Build the upload escape(s) for a PNG (f=100) image. Chunked per Kitty spec
|
|
48
48
|
// recommendation (≤ 4096 base64 bytes per escape).
|
|
49
|
+
//
|
|
50
|
+
// The upload is preceded by a delete of the same id. Images outlive the
|
|
51
|
+
// process — they belong to the terminal window — and this process allocates
|
|
52
|
+
// ids from a fixed base, so a previous run may have left this very id carrying
|
|
53
|
+
// a *different* placement. An image with two virtual placements and nothing to
|
|
54
|
+
// disambiguate them draws at whichever size the terminal happens to pick.
|
|
49
55
|
export function uploadPngEscape(pngBytes: Uint8Array, id: number): string {
|
|
50
56
|
const b64 = encodeBase64(pngBytes)
|
|
51
57
|
const chunks = chunkString(b64, MAX_BASE64_CHUNK)
|
|
52
|
-
const parts: string[] = []
|
|
58
|
+
const parts: string[] = [`${ESC}_Ga=d,d=I,i=${id},q=2;${ST}`]
|
|
53
59
|
for (let i = 0; i < chunks.length; i++) {
|
|
54
60
|
const isLast = i === chunks.length - 1
|
|
55
61
|
const m = isLast ? 0 : 1
|
|
@@ -60,6 +66,19 @@ export function uploadPngEscape(pngBytes: Uint8Array, id: number): string {
|
|
|
60
66
|
return wrapForTmux(parts.join(''))
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
// A virtual placement (U=1) is not painted over the screen: the terminal
|
|
70
|
+
// composites the image into whichever cells carry the Unicode placeholder,
|
|
71
|
+
// which is what lets an image survive redraws the app makes on its own. `cols`
|
|
72
|
+
// and `rows` size the placeholder box, and the image is scaled to fill it.
|
|
73
|
+
//
|
|
74
|
+
// Deliberately no source rectangle (`x`/`y`/`w`/`h`): terminals that speak the
|
|
75
|
+
// rest of this protocol do not all honour it on a virtual placement, and one
|
|
76
|
+
// that ignores it silently draws the whole image instead of the slice asked
|
|
77
|
+
// for. Callers upload one image per frame.
|
|
78
|
+
export function virtualPlacementEscape(id: number, cols: number, rows: number): string {
|
|
79
|
+
return wrapForTmux(`${ESC}_Ga=p,U=1,i=${id},c=${cols},r=${rows},q=2;${ST}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
export function deleteImageEscape(id: number): string {
|
|
64
83
|
return wrapForTmux(`${ESC}_Ga=d,d=I,i=${id},q=2;${ST}`)
|
|
65
84
|
}
|
|
@@ -68,6 +87,13 @@ export function imageIdToRgb(id: number): [number, number, number] {
|
|
|
68
87
|
return idToRgb(id)
|
|
69
88
|
}
|
|
70
89
|
|
|
90
|
+
// Same id, spelled the way a renderable's `fg` wants it. The colour is not a
|
|
91
|
+
// colour here: it is what tells the terminal which image a placeholder cell
|
|
92
|
+
// stands for, so it has to survive the round trip through the renderer intact.
|
|
93
|
+
export function imageIdToHex(id: number): string {
|
|
94
|
+
return `#${(id & 0xffffff).toString(16).padStart(6, '0')}`
|
|
95
|
+
}
|
|
96
|
+
|
|
71
97
|
export function writeRaw(seq: string): void {
|
|
72
98
|
// Synchronous write so we don't interleave with opentui frames.
|
|
73
99
|
process.stdout.write(seq)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Activity sprites: a small animation per agent state, drawn where the sidebar
|
|
2
|
+
// would otherwise put a spinner or a dot.
|
|
3
|
+
//
|
|
4
|
+
// Every frame is its own image, uploaded once and given a virtual placement, so
|
|
5
|
+
// animating is nothing but changing the foreground colour of two cells — no
|
|
6
|
+
// escape written per frame, and nothing an opentui redraw can wipe. One image
|
|
7
|
+
// per frame rather than one sheet cropped per placement: the source rectangle
|
|
8
|
+
// of a placement is not honoured by every terminal that speaks the protocol,
|
|
9
|
+
// and a sheet placed without it draws all its frames at once.
|
|
10
|
+
//
|
|
11
|
+
// Naming carries everything, so there is no manifest to keep in step with the
|
|
12
|
+
// files. Either form works:
|
|
13
|
+
// - a folder `<state>@<ms>/` of numbered PNGs, one per frame (`0.png`, `1.png`…)
|
|
14
|
+
// - a `<state>.gif`, which brings its own frames and delay, cut by ImageMagick
|
|
15
|
+
|
|
16
|
+
import { readdir } from 'node:fs/promises'
|
|
17
|
+
|
|
18
|
+
import { logDebug } from '../../debug/input-log'
|
|
19
|
+
import { getProfileConfigDir } from '../../profile-paths'
|
|
20
|
+
import {
|
|
21
|
+
imageIdToHex,
|
|
22
|
+
nextImageId,
|
|
23
|
+
uploadPngEscape,
|
|
24
|
+
virtualPlacementEscape,
|
|
25
|
+
writeRaw,
|
|
26
|
+
} from './kitty'
|
|
27
|
+
|
|
28
|
+
export const SPRITE_STATES = ['idle', 'working', 'waiting', 'done'] as const
|
|
29
|
+
|
|
30
|
+
export type SpriteState = (typeof SPRITE_STATES)[number]
|
|
31
|
+
|
|
32
|
+
export type SpriteSet = Partial<Record<SpriteState, Sprite>>
|
|
33
|
+
|
|
34
|
+
export interface Sprite {
|
|
35
|
+
/**
|
|
36
|
+
* One entry per frame. Not a colour: it is the 24-bit id of the image the
|
|
37
|
+
* placeholder cells stand for, spelled the way a renderable's `fg` wants it.
|
|
38
|
+
*/
|
|
39
|
+
colors: readonly string[]
|
|
40
|
+
frameMs: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The one shape a sprite is ever placed into: the two lines of a workspace row,
|
|
45
|
+
* three cells across.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately the only one. A frame gets a single image and a single placement,
|
|
48
|
+
* so a cell cannot resolve to a box other than the one it was drawn for. Earlier
|
|
49
|
+
* revisions placed each frame at several sizes for the different rows that
|
|
50
|
+
* wanted them, and sprites drew at the wrong size — three placements of visually
|
|
51
|
+
* identical images stacked in one column is a resolution the protocol gives no
|
|
52
|
+
* way to make unambiguous from a cell alone.
|
|
53
|
+
*
|
|
54
|
+
* Three cells across because the terminal fits an image to its box keeping the
|
|
55
|
+
* aspect ratio: for a roughly square frame in cells twice as tall as they are
|
|
56
|
+
* wide, the width is what caps the sprite, and two rows of height buy nothing
|
|
57
|
+
* under two cells of width.
|
|
58
|
+
*/
|
|
59
|
+
export const SPRITE_COLS = 3
|
|
60
|
+
const SPRITE_ROWS = 2
|
|
61
|
+
|
|
62
|
+
// Kitty encodes a placeholder cell's row and column as combining marks, the Nth
|
|
63
|
+
// entry of the spec's table standing for N. Three of each is all this needs.
|
|
64
|
+
const MARKS = ['̅', '̍', '̎'] as const
|
|
65
|
+
const DEFAULT_FRAME_MS = 150
|
|
66
|
+
const BUNDLED_DIR = `${import.meta.dir}/sprites`
|
|
67
|
+
const FOLDER_NAME = /^(idle|working|waiting|done)@(\d+)$/
|
|
68
|
+
const GIF_NAME = /^(idle|working|waiting|done)\.gif$/
|
|
69
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
|
|
70
|
+
|
|
71
|
+
/** U+10EEEE, the Kitty placeholder. */
|
|
72
|
+
const PLACEHOLDER = '\u{10EEEE}'
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The cells of a sprite, one string per line of its box, top line first.
|
|
76
|
+
*
|
|
77
|
+
* Every cell names the row and the column it stands for, always — never a bare
|
|
78
|
+
* placeholder left for the terminal to work out. Its rule for an unmarked cell
|
|
79
|
+
* looks up as well as left: a placeholder sitting directly below another of the
|
|
80
|
+
* same image reads as the next row down. A sidebar is a column of rows drawing
|
|
81
|
+
* the same idle sprite, so each one would inherit the position of the row above
|
|
82
|
+
* it and draw a slice of the image rather than the image.
|
|
83
|
+
*
|
|
84
|
+
* The marks are zero-width, so a line is exactly `SPRITE_COLS` columns wide.
|
|
85
|
+
*/
|
|
86
|
+
function placeholderLines(): string[] {
|
|
87
|
+
return Array.from({ length: SPRITE_ROWS }, (_, row) => {
|
|
88
|
+
const rowMark = MARKS[row] ?? MARKS[0]
|
|
89
|
+
let line = ''
|
|
90
|
+
for (let col = 0; col < SPRITE_COLS; col++) {
|
|
91
|
+
line += `${PLACEHOLDER}${rowMark}${MARKS[col] ?? MARKS[0]}`
|
|
92
|
+
}
|
|
93
|
+
return line
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Resolved once: the same two strings every row of the sidebar draws. */
|
|
98
|
+
export const SPRITE_PLACEHOLDERS: readonly string[] = placeholderLines()
|
|
99
|
+
|
|
100
|
+
/** Where a user drops their own sprites. Anything here beats what aimux ships. */
|
|
101
|
+
export function userSpriteDir(): string {
|
|
102
|
+
return `${getProfileConfigDir()}/sprites`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface SpriteName {
|
|
106
|
+
frameMs: number
|
|
107
|
+
/** True when the frames come from a GIF and only ImageMagick can read them. */
|
|
108
|
+
gif: boolean
|
|
109
|
+
state: SpriteState
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* What an entry in a sprite folder declares, or null when it declares nothing —
|
|
114
|
+
* the folder is the user's, and a stray screenshot in it is not a configuration
|
|
115
|
+
* error.
|
|
116
|
+
*/
|
|
117
|
+
export function parseSpriteName(entry: string): SpriteName | null {
|
|
118
|
+
const folder = FOLDER_NAME.exec(entry)
|
|
119
|
+
if (folder) {
|
|
120
|
+
const ms = Number.parseInt(folder[2] ?? '', 10)
|
|
121
|
+
return {
|
|
122
|
+
frameMs: Number.isFinite(ms) && ms > 0 ? ms : DEFAULT_FRAME_MS,
|
|
123
|
+
gif: false,
|
|
124
|
+
state: folder[1] as SpriteState,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const gif = GIF_NAME.exec(entry)
|
|
128
|
+
if (gif === null) return null
|
|
129
|
+
return { frameMs: DEFAULT_FRAME_MS, gif: true, state: gif[1] as SpriteState }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface Frames {
|
|
133
|
+
frameMs: number
|
|
134
|
+
pngs: Uint8Array[]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function runMagick(args: string[], timeoutMs: number): Promise<Uint8Array | null> {
|
|
138
|
+
for (const bin of ['magick', 'convert']) {
|
|
139
|
+
try {
|
|
140
|
+
const proc = Bun.spawn([bin, ...args], { stderr: 'ignore', stdin: 'ignore', stdout: 'pipe' })
|
|
141
|
+
const timer = setTimeout(() => proc.kill(), timeoutMs)
|
|
142
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).bytes(), proc.exited])
|
|
143
|
+
clearTimeout(timer)
|
|
144
|
+
if (code === 0 && out.byteLength > 0) return out
|
|
145
|
+
} catch {
|
|
146
|
+
// Next binary, then give up: a missing ImageMagick is not an error, it
|
|
147
|
+
// only means GIF sprites are unavailable.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* ImageMagick writes one PNG per frame back to back on stdout. They are split on
|
|
155
|
+
* the signature rather than counted, so a frame count that disagrees with the
|
|
156
|
+
* stream cannot desynchronise the two.
|
|
157
|
+
*/
|
|
158
|
+
function splitPngs(bytes: Uint8Array): Uint8Array[] {
|
|
159
|
+
const starts: number[] = []
|
|
160
|
+
for (let i = 0; i + PNG_SIGNATURE.length <= bytes.length; i++) {
|
|
161
|
+
if (PNG_SIGNATURE.every((byte, k) => bytes[i + k] === byte)) starts.push(i)
|
|
162
|
+
}
|
|
163
|
+
return starts.map((start, i) => bytes.subarray(start, starts[i + 1] ?? bytes.length))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A GIF's own frame count and delay are authoritative, so it names neither. */
|
|
167
|
+
async function gifFrames(path: string): Promise<Frames | null> {
|
|
168
|
+
const pngs = splitPngs((await runMagick([path, '-coalesce', 'png:-'], 5000)) ?? new Uint8Array())
|
|
169
|
+
if (pngs.length === 0) return null
|
|
170
|
+
const meta = await runMagick(['identify', '-format', '%T\\n', path], 2000)
|
|
171
|
+
const ticks = Number.parseInt(new TextDecoder().decode(meta ?? new Uint8Array()), 10)
|
|
172
|
+
// GIF delays are in hundredths of a second, and 0 is the "as fast as you can"
|
|
173
|
+
// some encoders write — which at this size would be a strobe.
|
|
174
|
+
const frameMs = Number.isFinite(ticks) && ticks > 0 ? ticks * 10 : DEFAULT_FRAME_MS
|
|
175
|
+
return { frameMs, pngs }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Frames in numeric order — `10.png` follows `9.png`, it does not precede it. */
|
|
179
|
+
async function folderFrames(dir: string, frameMs: number): Promise<Frames | null> {
|
|
180
|
+
const files = (await readdir(dir))
|
|
181
|
+
.filter((file) => file.endsWith('.png'))
|
|
182
|
+
.sort((a, b) => Number.parseInt(a, 10) - Number.parseInt(b, 10))
|
|
183
|
+
if (files.length === 0) return null
|
|
184
|
+
return {
|
|
185
|
+
frameMs,
|
|
186
|
+
pngs: await Promise.all(files.map(async (file) => Bun.file(`${dir}/${file}`).bytes())),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Every sprite a directory declares, keyed by the state it stands for. */
|
|
191
|
+
async function scanDir(dir: string): Promise<Map<SpriteState, Frames>> {
|
|
192
|
+
const found = new Map<SpriteState, Frames>()
|
|
193
|
+
let entries: string[]
|
|
194
|
+
try {
|
|
195
|
+
entries = await readdir(dir)
|
|
196
|
+
} catch {
|
|
197
|
+
return found
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const entry of entries.sort()) {
|
|
201
|
+
const name = parseSpriteName(entry)
|
|
202
|
+
if (name === null) continue
|
|
203
|
+
const frames = name.gif
|
|
204
|
+
? await gifFrames(`${dir}/${entry}`)
|
|
205
|
+
: await folderFrames(`${dir}/${entry}`, name.frameMs)
|
|
206
|
+
if (frames) found.set(name.state, frames)
|
|
207
|
+
else if (name.gif) {
|
|
208
|
+
logDebug('sprites.gif.failed', { entry, reason: 'ImageMagick unavailable or failed' })
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return found
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function upload(frames: Frames): Sprite {
|
|
215
|
+
const colors = frames.pngs.map((png) => {
|
|
216
|
+
const id = nextImageId()
|
|
217
|
+
writeRaw(uploadPngEscape(png, id) + virtualPlacementEscape(id, SPRITE_COLS, SPRITE_ROWS))
|
|
218
|
+
return imageIdToHex(id)
|
|
219
|
+
})
|
|
220
|
+
return { colors, frameMs: Math.max(16, frames.frameMs) }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function loadAll(): Promise<SpriteSet> {
|
|
224
|
+
// The user's folder is scanned second so a state it declares replaces the one
|
|
225
|
+
// aimux ships, rather than being merged with it.
|
|
226
|
+
const found = new Map([...(await scanDir(BUNDLED_DIR)), ...(await scanDir(userSpriteDir()))])
|
|
227
|
+
const set: SpriteSet = {}
|
|
228
|
+
for (const [state, frames] of found) set[state] = upload(frames)
|
|
229
|
+
return set
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let loaded: Promise<SpriteSet> | null = null
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Uploaded once per process — every row animates off the same images. Read from
|
|
236
|
+
* disk at first use, so a sprite dropped into the folder appears at the next
|
|
237
|
+
* launch, not mid-session.
|
|
238
|
+
*/
|
|
239
|
+
export async function loadSprites(): Promise<SpriteSet> {
|
|
240
|
+
loaded ??= loadAll()
|
|
241
|
+
try {
|
|
242
|
+
return await loaded
|
|
243
|
+
} catch {
|
|
244
|
+
return {}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Fit a label into `max` columns, marking the cut with an ellipsis. */
|
|
2
|
+
export function truncate(label: string, max: number): string {
|
|
3
|
+
if (max <= 0) return ''
|
|
4
|
+
if (label.length <= max) return label
|
|
5
|
+
if (max === 1) return '…'
|
|
6
|
+
return `${label.slice(0, max - 1)}…`
|
|
7
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
|
|
3
3
|
import { GitPaneWidget } from '../components/git/pane/git-pane-widget'
|
|
4
|
-
import {
|
|
4
|
+
import { ProjectList } from '../components/layout/sidebar/project-list'
|
|
5
|
+
import { SetupWidget } from '../components/setup/setup-widget'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Everything a bar can host. Adding a widget = one entry here plus its id in
|
|
@@ -9,10 +10,12 @@ import { WorkspaceList } from '../components/layout/sidebar/workspace-list'
|
|
|
9
10
|
*/
|
|
10
11
|
export const WIDGET_RENDERERS: Record<string, (contentWidth: number) => ReactNode> = {
|
|
11
12
|
git: (contentWidth) => <GitPaneWidget contentWidth={contentWidth} pollingEnabled />,
|
|
12
|
-
|
|
13
|
+
projects: (contentWidth) => <ProjectList contentWidth={contentWidth} />,
|
|
14
|
+
setup: () => <SetupWidget />,
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export const WIDGET_LABELS: Record<string, string> = {
|
|
16
18
|
git: 'Git',
|
|
17
|
-
|
|
19
|
+
projects: 'Projects',
|
|
20
|
+
setup: 'Setup',
|
|
18
21
|
}
|
|
@@ -5,9 +5,30 @@ import { visibleWidgets } from '../../state/bars'
|
|
|
5
5
|
import { dispatchGlobal } from '../../state/dispatch-ref'
|
|
6
6
|
import { WIDGET_LABELS } from './registry'
|
|
7
7
|
|
|
8
|
+
/** `Show <label>` for every hidden widget in either bar. */
|
|
9
|
+
function showHiddenItems(bars: BarsState): ContextMenuItem[] {
|
|
10
|
+
const items: ContextMenuItem[] = []
|
|
11
|
+
for (const side of ['left', 'right'] as const) {
|
|
12
|
+
for (const widget of bars[side].widgets) {
|
|
13
|
+
if (widget.visible) continue
|
|
14
|
+
items.push([
|
|
15
|
+
`Show ${WIDGET_LABELS[widget.id] ?? widget.id}`,
|
|
16
|
+
() => dispatchGlobal({ type: 'toggle-widget', widgetId: widget.id }),
|
|
17
|
+
])
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return items
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
/**
|
|
9
24
|
* Right-click menu for a widget hosted in a bar: move it across, reorder it
|
|
10
|
-
* within its bar, or
|
|
25
|
+
* within its bar, hide it, or bring back one that is hidden. Generic — a new
|
|
26
|
+
* widget gets this for free.
|
|
27
|
+
*
|
|
28
|
+
* The unhide entries live here and not only on the bar's own menu because the
|
|
29
|
+
* bar's menu is unreachable: widget slots cover the whole body and stop
|
|
30
|
+
* right-click propagation, and the edge and boundary handles swallow every
|
|
31
|
+
* button. Without this, hiding a widget is a one-way door for a mouse user.
|
|
11
32
|
*/
|
|
12
33
|
export function buildWidgetContextMenu(
|
|
13
34
|
bars: BarsState,
|
|
@@ -50,20 +71,15 @@ export function buildWidgetContextMenu(
|
|
|
50
71
|
if (visibleWidgets(bars[side]).length > 1) {
|
|
51
72
|
items.push(['Hide', () => dispatchGlobal({ type: 'toggle-widget', widgetId })])
|
|
52
73
|
}
|
|
74
|
+
|
|
75
|
+
items.push(...showHiddenItems(bars))
|
|
53
76
|
return items
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
/** Menu for the bar itself. Offers a way back for every hidden widget. */
|
|
57
80
|
export function buildBarContextMenu(bars: BarsState, side: BarSide): ContextMenuItem[] {
|
|
58
|
-
|
|
81
|
+
return [
|
|
59
82
|
[`Hide ${side} bar`, () => dispatchGlobal({ side, type: 'toggle-bar' })],
|
|
83
|
+
...showHiddenItems(bars),
|
|
60
84
|
]
|
|
61
|
-
for (const widget of bars[side].widgets) {
|
|
62
|
-
if (widget.visible) continue
|
|
63
|
-
items.push([
|
|
64
|
-
`Show ${WIDGET_LABELS[widget.id] ?? widget.id}`,
|
|
65
|
-
() => dispatchGlobal({ type: 'toggle-widget', widgetId: widget.id }),
|
|
66
|
-
])
|
|
67
|
-
}
|
|
68
|
-
return items
|
|
69
85
|
}
|
package/src/update.ts
CHANGED
|
@@ -60,7 +60,7 @@ export async function runUpdate(): Promise<number> {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// The terminal-manager is intentionally NOT restarted here: doing so kills
|
|
63
|
-
// live AI
|
|
63
|
+
// live AI projects. The flip side: the running TM keeps executing the
|
|
64
64
|
// previous version's code path, so any TM-side fix (perf, lifecycle) only
|
|
65
65
|
// takes effect after a manual restart. Surface that explicitly so users
|
|
66
66
|
// aren't silently stuck on stale behaviour.
|
|
@@ -73,7 +73,7 @@ export async function runUpdate(): Promise<number> {
|
|
|
73
73
|
'TM-side fixes in this update apply to new TMs only — the running one',
|
|
74
74
|
'keeps its old behaviour until restarted.',
|
|
75
75
|
'Run `aimux restart-terminal-manager` when you can afford to lose your',
|
|
76
|
-
'current PTY
|
|
76
|
+
'current PTY projects to upgrade it.',
|
|
77
77
|
'',
|
|
78
78
|
].join('\n')
|
|
79
79
|
)
|