@brimveyn/aimux 1.16.3 → 1.18.1
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/package.json +4 -2
- package/src/app-runtime/backend-runtime-events.ts +179 -0
- package/src/app-runtime/side-effects.ts +3 -1
- package/src/app.tsx +26 -0
- package/src/cli/chord.ts +77 -0
- package/src/cli/client/bootstrap.ts +76 -0
- package/src/cli/client/daemon-client.ts +228 -0
- package/src/cli/client/workspace-resolver.ts +57 -0
- package/src/cli/commands/tab/close.ts +33 -0
- package/src/cli/commands/tab/create.ts +109 -0
- package/src/cli/commands/tab/focus.ts +33 -0
- package/src/cli/commands/tab/list.ts +52 -0
- package/src/cli/commands/tab/send.ts +83 -0
- package/src/cli/commands/tab/snapshot.ts +127 -0
- package/src/cli/commands/tab/tail.ts +171 -0
- package/src/cli/commands/tab/wait.ts +86 -0
- package/src/cli/commands/workspace/close.ts +32 -0
- package/src/cli/commands/workspace/create.ts +92 -0
- package/src/cli/commands/workspace/list.ts +25 -0
- package/src/cli/commands/workspace/show.ts +32 -0
- package/src/cli/commands/workspace/switch.ts +75 -0
- package/src/cli/commands/worktree/create.ts +113 -0
- package/src/cli/commands/worktree/list.ts +44 -0
- package/src/cli/commands/worktree/remove.ts +59 -0
- package/src/cli/context.ts +16 -0
- package/src/cli/flags.ts +101 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/output.ts +30 -0
- package/src/cli/registry.ts +54 -0
- package/src/cli/snapshot-render.ts +54 -0
- package/src/daemon/catalog-writer.ts +123 -0
- package/src/daemon/daemon.ts +566 -11
- package/src/daemon/reexec-client.ts +147 -0
- package/src/daemon/runtime-paths.ts +161 -1
- package/src/daemon/session-registry.ts +17 -0
- package/src/index.tsx +9 -0
- package/src/input/modes/bridge.ts +6 -0
- package/src/input/modes/transitions.ts +2 -0
- package/src/input/modes/types.ts +1 -0
- package/src/ipc/README.md +112 -0
- package/src/ipc/manager-protocol.ts +54 -4
- package/src/ipc/protocol.ts +466 -25
- package/src/platform/daemon-control.ts +14 -0
- package/src/restart-daemon.ts +36 -4
- package/src/session-backend/bootstrap.ts +110 -0
- package/src/session-backend/local-session-backend.ts +6 -0
- package/src/session-backend/remote-session-backend.ts +63 -0
- package/src/session-backend/types.ts +34 -0
- package/src/state/reducers/modal-state.ts +66 -1
- package/src/state/types.ts +40 -0
- package/src/state/validation.ts +1 -1
- package/src/terminal-manager/manager-client.ts +24 -7
- package/src/ui/components/flash/flash-label-badge.tsx +38 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +2 -0
- package/src/ui/components/layout/sidebar/workspace-list.tsx +4 -0
- package/src/ui/components/layout/sidebar/worktree-row.tsx +2 -0
- package/src/ui/components/layout/top-tab-bar.tsx +2 -0
- package/src/ui/flash/assign-labels.ts +126 -0
- package/src/ui/flash/build-labels.ts +78 -0
- package/src/ui/hooks/use-flash-label.ts +40 -0
- package/src/ui/root.tsx +3 -0
|
@@ -16,6 +16,7 @@ import { IDLE_SESSION_STATUS } from '../../../../state/types'
|
|
|
16
16
|
import { useBusySpinner } from '../../../hooks/use-busy-spinner'
|
|
17
17
|
import { moveIdToIdPosition, orderSessionsForDisplay } from '../../../session-ordering'
|
|
18
18
|
import { useTheme } from '../../../theme'
|
|
19
|
+
import { FlashLabelBadge } from '../../flash/flash-label-badge'
|
|
19
20
|
import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
|
|
20
21
|
import { useSidebarAutoScroll } from './use-sidebar-auto-scroll'
|
|
21
22
|
import { WorktreeRow } from './worktree-row'
|
|
@@ -394,6 +395,9 @@ const WorkspaceRow = memo(function WorkspaceRow({
|
|
|
394
395
|
</text>
|
|
395
396
|
<text fg={t.text} selectable={false} wrapMode="none">
|
|
396
397
|
{' '}
|
|
398
|
+
</text>
|
|
399
|
+
<FlashLabelBadge rowKey={`ws:${session.id}`} />
|
|
400
|
+
<text fg={t.text} selectable={false} wrapMode="none">
|
|
397
401
|
{nameLabel}
|
|
398
402
|
</text>
|
|
399
403
|
</box>
|
|
@@ -8,6 +8,7 @@ import { useAppStore } from '../../../../state/app-store'
|
|
|
8
8
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
9
9
|
import { formatDivergence } from '../../../../state/session-worktrees'
|
|
10
10
|
import { useTheme } from '../../../theme'
|
|
11
|
+
import { FlashLabelBadge } from '../../flash/flash-label-badge'
|
|
11
12
|
import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
|
|
12
13
|
|
|
13
14
|
interface WorktreeRowProps {
|
|
@@ -127,6 +128,7 @@ export const WorktreeRow = memo(function WorktreeRow({
|
|
|
127
128
|
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
128
129
|
{connector}{' '}
|
|
129
130
|
</text>
|
|
131
|
+
<FlashLabelBadge rowKey={`wt:${worktree.id}`} />
|
|
130
132
|
<text fg={t.text} selectable={false} wrapMode="none">
|
|
131
133
|
{worktree.name}
|
|
132
134
|
</text>
|
|
@@ -15,6 +15,7 @@ import { filterTabsForActiveWorktree } from '../../../state/session-worktrees'
|
|
|
15
15
|
import { buildTabEntries, type GroupEntry, type TabEntry } from '../../../state/tab-entries'
|
|
16
16
|
import { moveIdToIdPosition } from '../../session-ordering'
|
|
17
17
|
import { useTheme } from '../../theme'
|
|
18
|
+
import { FlashLabelBadge } from '../flash/flash-label-badge'
|
|
18
19
|
import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
|
|
19
20
|
import { TabItem } from './sidebar/tab-item'
|
|
20
21
|
import { useTopTabBarAutoScroll } from './sidebar/use-top-tab-bar-auto-scroll'
|
|
@@ -161,6 +162,7 @@ function GroupTabItem({
|
|
|
161
162
|
{' | '}
|
|
162
163
|
</text>
|
|
163
164
|
) : null}
|
|
165
|
+
<FlashLabelBadge rowKey={`tab:${tab.id}`} />
|
|
164
166
|
<text fg={isLeafActive ? t.text : t.textMuted} selectable={false} wrapMode="none">
|
|
165
167
|
{tab.title}
|
|
166
168
|
</text>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export interface FlashTarget {
|
|
2
|
+
/** Stable identity (used by callers to pair labels back with their target). */
|
|
3
|
+
key: string
|
|
4
|
+
/** Display name used to pick a nice first-letter label when possible. */
|
|
5
|
+
name: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface AssignedLabel {
|
|
9
|
+
key: string
|
|
10
|
+
label: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pool of letters used as labels — home row first (easiest to reach), then the
|
|
15
|
+
* rest of the alphabet. Lowercase a–z only; the flash-jump mode handler relies
|
|
16
|
+
* on letter input being lowercase ASCII.
|
|
17
|
+
*/
|
|
18
|
+
const POOL: string[] = [
|
|
19
|
+
'a',
|
|
20
|
+
's',
|
|
21
|
+
'd',
|
|
22
|
+
'f',
|
|
23
|
+
'g',
|
|
24
|
+
'h',
|
|
25
|
+
'j',
|
|
26
|
+
'k',
|
|
27
|
+
'l',
|
|
28
|
+
'q',
|
|
29
|
+
'w',
|
|
30
|
+
'e',
|
|
31
|
+
'r',
|
|
32
|
+
't',
|
|
33
|
+
'y',
|
|
34
|
+
'u',
|
|
35
|
+
'i',
|
|
36
|
+
'o',
|
|
37
|
+
'p',
|
|
38
|
+
'z',
|
|
39
|
+
'x',
|
|
40
|
+
'c',
|
|
41
|
+
'v',
|
|
42
|
+
'b',
|
|
43
|
+
'n',
|
|
44
|
+
'm',
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract the first lowercase alphanumeric letter of a name (for the
|
|
49
|
+
* "prefer the actual first letter" heuristic). Returns null when the name has
|
|
50
|
+
* no usable character.
|
|
51
|
+
*/
|
|
52
|
+
function firstAlphaLetter(name: string): string | null {
|
|
53
|
+
for (const raw of name) {
|
|
54
|
+
const ch = raw.toLowerCase()
|
|
55
|
+
if (ch >= 'a' && ch <= 'z') return ch
|
|
56
|
+
}
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Assign 1- or 2-char labels to a list of targets in display order.
|
|
62
|
+
*
|
|
63
|
+
* Algorithm:
|
|
64
|
+
* - Up to (POOL.length - reserved) targets get 1-char labels. We try to give
|
|
65
|
+
* each target its real first letter when it's still free; otherwise pull
|
|
66
|
+
* from the pool.
|
|
67
|
+
* - If the list overflows the single-char budget, the last pool letter is
|
|
68
|
+
* reserved as a 2-char prefix and the overflow gets `<prefix><letter>`
|
|
69
|
+
* labels (also drawn from POOL).
|
|
70
|
+
* - When a target's preferred first letter is needed as the 2-char prefix
|
|
71
|
+
* (or already taken), we fall back to the next available pool letter.
|
|
72
|
+
*
|
|
73
|
+
* The function never returns colliding labels and never returns a label
|
|
74
|
+
* `xy` whose prefix `x` is also a 1-char label — letting the input handler
|
|
75
|
+
* resolve on each keystroke unambiguously.
|
|
76
|
+
*/
|
|
77
|
+
export function assignFlashLabels(targets: FlashTarget[]): AssignedLabel[] {
|
|
78
|
+
if (targets.length === 0) return []
|
|
79
|
+
|
|
80
|
+
const singleBudget = targets.length <= POOL.length ? POOL.length : POOL.length - 1
|
|
81
|
+
const needsPrefix = targets.length > POOL.length
|
|
82
|
+
const prefixLetter = needsPrefix ? (POOL.at(-1) ?? null) : null
|
|
83
|
+
const singlePool = needsPrefix ? POOL.slice(0, POOL.length - 1) : [...POOL]
|
|
84
|
+
|
|
85
|
+
const used = new Set<string>()
|
|
86
|
+
const result: AssignedLabel[] = []
|
|
87
|
+
|
|
88
|
+
const singleSlice = targets.slice(0, singleBudget)
|
|
89
|
+
for (const target of singleSlice) {
|
|
90
|
+
const preferred = firstAlphaLetter(target.name)
|
|
91
|
+
if (preferred !== null && preferred !== prefixLetter && !used.has(preferred)) {
|
|
92
|
+
used.add(preferred)
|
|
93
|
+
result.push({ key: target.key, label: preferred })
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
const next = singlePool.find((letter) => !used.has(letter))
|
|
97
|
+
if (next === undefined) {
|
|
98
|
+
// Single-letter pool exhausted — push remaining targets to the 2-char
|
|
99
|
+
// overflow path below.
|
|
100
|
+
result.push({ key: target.key, label: '' })
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
used.add(next)
|
|
104
|
+
result.push({ key: target.key, label: next })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!needsPrefix) {
|
|
108
|
+
return result.filter((entry) => entry.label !== '')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const prefix = prefixLetter as string
|
|
112
|
+
const overflow = targets.slice(singleBudget)
|
|
113
|
+
const usedSecondary = new Set<string>()
|
|
114
|
+
for (const target of overflow) {
|
|
115
|
+
const preferred = firstAlphaLetter(target.name)
|
|
116
|
+
const second =
|
|
117
|
+
preferred !== null && !usedSecondary.has(preferred)
|
|
118
|
+
? preferred
|
|
119
|
+
: (POOL.find((letter) => !usedSecondary.has(letter)) ?? null)
|
|
120
|
+
if (second === null) break
|
|
121
|
+
usedSecondary.add(second)
|
|
122
|
+
result.push({ key: target.key, label: `${prefix}${second}` })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result.filter((entry) => entry.label !== '')
|
|
126
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AppState, FlashJumpTarget, FlashLabel } from '../../state/types'
|
|
2
|
+
|
|
3
|
+
import { filterTabsForActiveWorktree } from '../../state/session-worktrees'
|
|
4
|
+
import { orderSessionsForDisplay } from '../session-ordering'
|
|
5
|
+
import { assignFlashLabels, type FlashTarget } from './assign-labels'
|
|
6
|
+
|
|
7
|
+
interface PendingTarget {
|
|
8
|
+
key: string
|
|
9
|
+
name: string
|
|
10
|
+
target: FlashJumpTarget
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Collect every visible flash-jump target in the same order the UI renders:
|
|
15
|
+
* workspace rows first (each followed by its non-primary worktrees), then the
|
|
16
|
+
* tabs of the active worktree. Stable ordering keeps the assigned labels
|
|
17
|
+
* predictable across re-opens when nothing changed.
|
|
18
|
+
*/
|
|
19
|
+
function collectTargets(state: AppState): PendingTarget[] {
|
|
20
|
+
const out: PendingTarget[] = []
|
|
21
|
+
const ordered = orderSessionsForDisplay(state.sessions)
|
|
22
|
+
for (const [index, session] of ordered.entries()) {
|
|
23
|
+
const sessionIndex = index + 1
|
|
24
|
+
const worktrees = session.worktrees ?? []
|
|
25
|
+
const primary = worktrees.find((w) => w.source === 'primary') ?? worktrees[0]
|
|
26
|
+
out.push({
|
|
27
|
+
key: `ws:${session.id}`,
|
|
28
|
+
name: session.name,
|
|
29
|
+
target: {
|
|
30
|
+
kind: 'workspace',
|
|
31
|
+
sessionId: session.id,
|
|
32
|
+
sessionIndex,
|
|
33
|
+
worktreeId: primary?.id,
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
for (const worktree of worktrees) {
|
|
37
|
+
if (worktree.id === primary?.id) continue
|
|
38
|
+
out.push({
|
|
39
|
+
key: `wt:${worktree.id}`,
|
|
40
|
+
name: worktree.name,
|
|
41
|
+
target: {
|
|
42
|
+
kind: 'worktree',
|
|
43
|
+
sessionId: session.id,
|
|
44
|
+
sessionIndex,
|
|
45
|
+
worktreeId: worktree.id,
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const currentSession = state.sessions.find((s) => s.id === state.currentSessionId)
|
|
52
|
+
if (currentSession) {
|
|
53
|
+
const tabs = filterTabsForActiveWorktree(state.tabs, currentSession)
|
|
54
|
+
for (const tab of tabs) {
|
|
55
|
+
out.push({
|
|
56
|
+
key: `tab:${tab.id}`,
|
|
57
|
+
name: tab.title,
|
|
58
|
+
target: { kind: 'tab', sessionId: currentSession.id, sessionIndex: 0, tabId: tab.id },
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildFlashJumpLabels(state: AppState): FlashLabel[] {
|
|
67
|
+
const pending = collectTargets(state)
|
|
68
|
+
const flashTargets: FlashTarget[] = pending.map(({ key, name }) => ({ key, name }))
|
|
69
|
+
const assigned = assignFlashLabels(flashTargets)
|
|
70
|
+
const byKey = new Map(pending.map((entry) => [entry.key, entry.target]))
|
|
71
|
+
const out: FlashLabel[] = []
|
|
72
|
+
for (const { key, label } of assigned) {
|
|
73
|
+
const target = byKey.get(key)
|
|
74
|
+
if (!target) continue
|
|
75
|
+
out.push({ key, label, target })
|
|
76
|
+
}
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useAppStore } from '../../state/app-store'
|
|
2
|
+
|
|
3
|
+
export interface FlashLabelView {
|
|
4
|
+
/** Full label, e.g. "a" or "fk". */
|
|
5
|
+
label: string
|
|
6
|
+
/** Already-typed prefix length — the rendered label dims that segment. */
|
|
7
|
+
matchedLen: number
|
|
8
|
+
/** Suffix still to be typed. */
|
|
9
|
+
remaining: string
|
|
10
|
+
/** True when the current buffer still matches this label's prefix. */
|
|
11
|
+
isActive: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Subscribe to the flash-jump modal and return label info for the row keyed
|
|
16
|
+
* by `key` (e.g. `tab:<id>`, `ws:<id>`, `wt:<id>`). Returns null when the
|
|
17
|
+
* modal isn't active or the row isn't a target.
|
|
18
|
+
*
|
|
19
|
+
* Selects primitives (label and buffer) separately so the snapshot React sees
|
|
20
|
+
* stays referentially stable across renders. Returning a freshly-built object
|
|
21
|
+
* from a single selector would tell useSyncExternalStore "state changed" on
|
|
22
|
+
* every render and infinite-loop.
|
|
23
|
+
*/
|
|
24
|
+
export function useFlashLabel(key: string): FlashLabelView | null {
|
|
25
|
+
const label = useAppStore((s) => {
|
|
26
|
+
if (s.modal.type !== 'flash-jump') return null
|
|
27
|
+
const entry = s.modal.labels.find((l) => l.key === key)
|
|
28
|
+
return entry?.label ?? null
|
|
29
|
+
})
|
|
30
|
+
const buffer = useAppStore((s) => (s.modal.type === 'flash-jump' ? s.modal.buffer : ''))
|
|
31
|
+
if (label === null) return null
|
|
32
|
+
const isActive = label.startsWith(buffer)
|
|
33
|
+
const matchedLen = isActive ? buffer.length : 0
|
|
34
|
+
return {
|
|
35
|
+
isActive,
|
|
36
|
+
label,
|
|
37
|
+
matchedLen,
|
|
38
|
+
remaining: isActive ? label.slice(buffer.length) : label,
|
|
39
|
+
}
|
|
40
|
+
}
|