@brimveyn/aimux 1.16.2 → 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.
Files changed (61) hide show
  1. package/package.json +4 -2
  2. package/src/app-runtime/backend-runtime-events.ts +179 -0
  3. package/src/app-runtime/side-effects.ts +3 -1
  4. package/src/app.tsx +26 -0
  5. package/src/cli/chord.ts +77 -0
  6. package/src/cli/client/bootstrap.ts +76 -0
  7. package/src/cli/client/daemon-client.ts +228 -0
  8. package/src/cli/client/workspace-resolver.ts +57 -0
  9. package/src/cli/commands/tab/close.ts +33 -0
  10. package/src/cli/commands/tab/create.ts +109 -0
  11. package/src/cli/commands/tab/focus.ts +33 -0
  12. package/src/cli/commands/tab/list.ts +52 -0
  13. package/src/cli/commands/tab/send.ts +83 -0
  14. package/src/cli/commands/tab/snapshot.ts +127 -0
  15. package/src/cli/commands/tab/tail.ts +171 -0
  16. package/src/cli/commands/tab/wait.ts +86 -0
  17. package/src/cli/commands/workspace/close.ts +32 -0
  18. package/src/cli/commands/workspace/create.ts +92 -0
  19. package/src/cli/commands/workspace/list.ts +25 -0
  20. package/src/cli/commands/workspace/show.ts +32 -0
  21. package/src/cli/commands/workspace/switch.ts +75 -0
  22. package/src/cli/commands/worktree/create.ts +113 -0
  23. package/src/cli/commands/worktree/list.ts +44 -0
  24. package/src/cli/commands/worktree/remove.ts +59 -0
  25. package/src/cli/context.ts +16 -0
  26. package/src/cli/flags.ts +101 -0
  27. package/src/cli/index.ts +113 -0
  28. package/src/cli/output.ts +30 -0
  29. package/src/cli/registry.ts +54 -0
  30. package/src/cli/snapshot-render.ts +54 -0
  31. package/src/daemon/catalog-writer.ts +123 -0
  32. package/src/daemon/daemon.ts +566 -11
  33. package/src/daemon/reexec-client.ts +147 -0
  34. package/src/daemon/runtime-paths.ts +161 -1
  35. package/src/daemon/session-registry.ts +17 -0
  36. package/src/index.tsx +9 -0
  37. package/src/input/modes/bridge.ts +6 -0
  38. package/src/input/modes/transitions.ts +2 -0
  39. package/src/input/modes/types.ts +1 -0
  40. package/src/ipc/README.md +112 -0
  41. package/src/ipc/manager-protocol.ts +54 -4
  42. package/src/ipc/protocol.ts +466 -25
  43. package/src/platform/daemon-control.ts +14 -0
  44. package/src/restart-daemon.ts +36 -4
  45. package/src/session-backend/bootstrap.ts +110 -0
  46. package/src/session-backend/local-session-backend.ts +6 -0
  47. package/src/session-backend/remote-session-backend.ts +63 -0
  48. package/src/session-backend/types.ts +34 -0
  49. package/src/state/reducers/modal-state.ts +66 -1
  50. package/src/state/types.ts +40 -0
  51. package/src/state/validation.ts +1 -1
  52. package/src/terminal-manager/manager-client.ts +24 -7
  53. package/src/ui/components/flash/flash-label-badge.tsx +38 -0
  54. package/src/ui/components/layout/sidebar/tab-item.tsx +2 -0
  55. package/src/ui/components/layout/sidebar/workspace-list.tsx +4 -0
  56. package/src/ui/components/layout/sidebar/worktree-row.tsx +2 -0
  57. package/src/ui/components/layout/top-tab-bar.tsx +2 -0
  58. package/src/ui/flash/assign-labels.ts +126 -0
  59. package/src/ui/flash/build-labels.ts +78 -0
  60. package/src/ui/hooks/use-flash-label.ts +40 -0
  61. 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
+ }
package/src/ui/root.tsx CHANGED
@@ -267,6 +267,9 @@ function renderModal(
267
267
  />
268
268
  )
269
269
  }
270
+ case 'flash-jump':
271
+ // Pure overlay — rendered inline by FlashLabelBadge inside the rows.
272
+ return null
270
273
  case null:
271
274
  return null
272
275
  default: