@brimveyn/aimux 1.12.2 → 1.13.0

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 (133) hide show
  1. package/README.md +3 -0
  2. package/package.json +4 -3
  3. package/src/app-runtime/auto-commit-driver.ts +8 -4
  4. package/src/app-runtime/auto-commit-ref.ts +1 -1
  5. package/src/app-runtime/backend-attach-runtime.ts +18 -2
  6. package/src/app-runtime/backend-runtime-events.ts +1 -1
  7. package/src/app-runtime/pty-write.ts +52 -0
  8. package/src/app-runtime/selection-scroll.ts +2 -2
  9. package/src/app-runtime/session-actions.ts +16 -4
  10. package/src/app-runtime/side-effects.ts +598 -102
  11. package/src/app-runtime/snippet-actions.ts +25 -11
  12. package/src/app-runtime/use-auto-commit-driver.ts +7 -4
  13. package/src/app-runtime/use-backend-runtime.ts +3 -3
  14. package/src/app-runtime/use-directory-search.ts +7 -5
  15. package/src/app-runtime/use-mouse-handlers.ts +11 -6
  16. package/src/app-runtime/use-pane-size-report.ts +1 -1
  17. package/src/app-runtime/use-renderer-bindings.ts +143 -8
  18. package/src/app.tsx +25 -9
  19. package/src/auto-commit/headless-commands.ts +4 -6
  20. package/src/daemon/daemon.ts +3 -3
  21. package/src/daemon/runtime-paths.ts +1 -1
  22. package/src/daemon/session-manager.ts +1 -1
  23. package/src/daemon/session-registry.ts +10 -3
  24. package/src/diff-parser/parse-patch-files.ts +18 -18
  25. package/src/git/command-queue.ts +1 -1
  26. package/src/git/divergence.ts +46 -0
  27. package/src/git/git-diff.ts +12 -5
  28. package/src/git/git-poller.ts +33 -11
  29. package/src/git/git-status.ts +14 -3
  30. package/src/git/move-worktree.ts +96 -0
  31. package/src/git/use-repo-discovery.ts +2 -1
  32. package/src/git/worktree-divergence-poller.ts +59 -0
  33. package/src/git/worktree.ts +99 -0
  34. package/src/index.tsx +1 -1
  35. package/src/input/keymap/describe-bindings.ts +27 -9
  36. package/src/input/keymap/key-chord.ts +4 -4
  37. package/src/input/keymap/key-format.ts +2 -2
  38. package/src/input/keymap/sequence-resolver.ts +1 -1
  39. package/src/input/keymap/trie.ts +1 -1
  40. package/src/input/modes/bridge.ts +7 -0
  41. package/src/input/modes/transitions.ts +2 -1
  42. package/src/input/modes/types.ts +13 -1
  43. package/src/input/raw-input-handler.ts +34 -1
  44. package/src/input/terminal-text-extraction.ts +9 -5
  45. package/src/integrations/claude-syntax-overlay.ts +8 -8
  46. package/src/integrations/claude-theme-sync.ts +12 -12
  47. package/src/ipc/protocol.ts +3 -3
  48. package/src/platform/clipboard.ts +18 -2
  49. package/src/platform/worktree-deps.ts +22 -0
  50. package/src/platform/worktree-paths.ts +67 -0
  51. package/src/profile-paths.ts +3 -3
  52. package/src/pty/assistant-status-detection-loop.ts +2 -2
  53. package/src/pty/assistant-status-detector.ts +15 -6
  54. package/src/pty/command-registry.ts +8 -1
  55. package/src/pty/pty-manager.ts +1 -1
  56. package/src/services/ai-usage/adapters/claude.ts +5 -5
  57. package/src/services/ai-usage/adapters/codex.ts +8 -8
  58. package/src/services/ai-usage/cache.ts +2 -2
  59. package/src/services/ai-usage/pace.ts +3 -6
  60. package/src/services/ai-usage/provider.ts +1 -1
  61. package/src/session-backend/bootstrap.ts +2 -2
  62. package/src/session-backend/local-session-backend.ts +11 -11
  63. package/src/session-backend/remote-session-backend.ts +3 -3
  64. package/src/session-backend/types.ts +2 -2
  65. package/src/snippets/expand-variables.ts +112 -0
  66. package/src/snippets/run-shell-var.ts +93 -0
  67. package/src/snippets/trigger-detector.ts +105 -0
  68. package/src/state/ai-usage-store.ts +1 -1
  69. package/src/state/git-tree.ts +8 -4
  70. package/src/state/layout-tree.ts +8 -5
  71. package/src/state/reducers/git-mode-state.ts +44 -14
  72. package/src/state/reducers/git-panel-state.ts +38 -2
  73. package/src/state/reducers/modal-state.ts +385 -24
  74. package/src/state/reducers/session-state.ts +60 -1
  75. package/src/state/reducers/tab-state.ts +151 -85
  76. package/src/state/selectors.ts +6 -4
  77. package/src/state/session-catalog.ts +20 -5
  78. package/src/state/session-persistence.ts +8 -4
  79. package/src/state/session-worktrees.ts +227 -0
  80. package/src/state/snippet-catalog.ts +65 -4
  81. package/src/state/store.ts +1 -0
  82. package/src/state/toast-store.ts +95 -0
  83. package/src/state/types.ts +108 -16
  84. package/src/state/validation.ts +47 -3
  85. package/src/terminal-manager/manager-client.ts +15 -13
  86. package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
  87. package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
  88. package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
  89. package/src/ui/components/git/diff-renderer/split-view.tsx +6 -4
  90. package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
  91. package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
  92. package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
  93. package/src/ui/components/git/git-panel.tsx +17 -10
  94. package/src/ui/components/git/git-view.tsx +79 -19
  95. package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
  96. package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
  97. package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
  98. package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
  99. package/src/ui/components/layout/session-bar.tsx +3 -3
  100. package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
  101. package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
  102. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
  103. package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
  104. package/src/ui/components/layout/split-layout.tsx +1 -1
  105. package/src/ui/components/layout/terminal-pane.tsx +43 -40
  106. package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
  107. package/src/ui/components/modals/app/help-modal.tsx +4 -2
  108. package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
  109. package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
  110. package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
  111. package/src/ui/components/modals/shared/form.tsx +4 -5
  112. package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
  113. package/src/ui/components/modals/shared/picker.tsx +28 -5
  114. package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +10 -6
  115. package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +25 -6
  116. package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
  117. package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
  118. package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
  119. package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
  120. package/src/ui/components/overlays/toast/toast-position.ts +45 -0
  121. package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
  122. package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
  123. package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
  124. package/src/ui/components/primitives/input-field.tsx +1 -1
  125. package/src/ui/components/primitives/list-item.tsx +3 -3
  126. package/src/ui/filter-themes.ts +1 -1
  127. package/src/ui/root.tsx +65 -14
  128. package/src/ui/session-ordering.ts +2 -2
  129. package/src/ui/status-bar-model.ts +14 -7
  130. package/src/ui/terminal-graphics/capabilities.ts +1 -1
  131. package/src/ui/terminal-graphics/format-fallback.ts +7 -9
  132. package/src/ui/terminal-graphics/kitty.ts +2 -7
  133. package/src/update/version-check.ts +1 -1
package/README.md CHANGED
@@ -22,6 +22,9 @@ snippets, themes, and fully configurable keymaps.
22
22
  - configurable git pane (embedded in the sidebar or as a standalone pane) and a
23
23
  dedicated [git mode](docs/guide/git-mode.md) for review / stage / commit /
24
24
  push with a split or stacked diff view and shiki-powered highlighting
25
+ - [git worktrees](docs/guide/worktrees.md) for running agents on parallel
26
+ branches — create per-branch worktrees, review each against its base, and
27
+ squash-move a worktree's work into another
25
28
  - built-in help generated from the resolved keymap
26
29
  - theme picker with 67 built-in themes (shiki catalog + aimux house themes) and a `/` filter
27
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.12.2",
3
+ "version": "1.13.0",
4
4
  "description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -53,14 +53,14 @@
53
53
  "demo:themes": "vhs assets/themes.tape",
54
54
  "restart-daemon": "AIMUX_PROFILE=dev bun run src/index.tsx restart-daemon",
55
55
  "restart-terminal-manager": "AIMUX_PROFILE=dev bun run src/index.tsx restart-terminal-manager",
56
- "lint": "oxlint .",
56
+ "lint": "oxlint --type-aware .",
57
57
  "format": "oxfmt --write .",
58
58
  "format:check": "oxfmt --check .",
59
59
  "knip": "knip-bun",
60
60
  "bump": "bun run scripts/bump.ts"
61
61
  },
62
62
  "dependencies": {
63
- "@brimveyn/aimux-config": "0.5.13",
63
+ "@brimveyn/aimux-config": "0.6.0",
64
64
  "@opentui/core": "^0.1.90",
65
65
  "@opentui/react": "^0.1.90",
66
66
  "@resvg/resvg-wasm": "^2.6.2",
@@ -79,6 +79,7 @@
79
79
  "lefthook": "^2.1.4",
80
80
  "oxfmt": "^0.42.0",
81
81
  "oxlint": "^1.57.0",
82
+ "oxlint-tsgolint": "^0.23.0",
82
83
  "typescript": "^6.0.2"
83
84
  }
84
85
  }
@@ -15,6 +15,7 @@ import { stripAnsi } from '../auto-commit/strip-ansi'
15
15
  import { runSuggestion } from '../auto-commit/suggestion-runner'
16
16
  import { workingTreeHash } from '../auto-commit/working-tree-hash'
17
17
  import { isCommandAvailable } from '../pty/command-registry'
18
+ import { toast } from '../state/toast-store'
18
19
 
19
20
  export interface TriggerInput {
20
21
  enabled: boolean
@@ -63,7 +64,7 @@ const UNTRACKED_FILE_BYTES_CAP = 8_000
63
64
  const SESSION_TAIL_BYTES_CAP = 8_000
64
65
 
65
66
  export function extractSessionTail(buffer: string | undefined): string {
66
- if (!buffer) return '[no session tail available]'
67
+ if (!(buffer != null && buffer !== '')) return '[no session tail available]'
67
68
  const stripped = stripAnsi(buffer).trimEnd()
68
69
  if (stripped.length === 0) return '[no session tail available]'
69
70
  return stripped.length > SESSION_TAIL_BYTES_CAP
@@ -161,7 +162,7 @@ export async function onActivityTransition(
161
162
  currentHash,
162
163
  enabled: config.enabled,
163
164
  git: args.git,
164
- hasProjectPath: !!args.projectPath,
165
+ hasProjectPath: !!(args.projectPath != null && args.projectPath !== ''),
165
166
  sessionId: args.sessionId,
166
167
  state: state.autoCommit,
167
168
  })
@@ -176,12 +177,15 @@ export async function onManualTrigger(
176
177
  ): Promise<void> {
177
178
  const config = deps.getConfig()
178
179
  const fail = (reason: string): void => {
179
- deps.dispatch({ message: `auto-commit: ${reason}`, type: 'git-mode-set-message' })
180
+ // Manual auto-commit triggers can fire outside git mode, so surface the
181
+ // reason as a toast rather than an inline git-pane message that's unseen.
182
+ toast.warning(`Auto-commit: ${reason}`)
180
183
  deps.dispatch({ sessionId: args.sessionId, type: 'auto-commit-clear' })
181
184
  }
182
185
  if (!config.enabled) return fail('disabled in config')
183
186
  if (!args.git || args.git.files.length === 0) return fail('no changes to summarise')
184
- if (!args.projectPath) return fail('no project path for current session')
187
+ if (!(args.projectPath != null && args.projectPath !== ''))
188
+ return fail('no project path for current session')
185
189
 
186
190
  const currentHash = workingTreeHash(args.git)
187
191
  const existing = deps.getState().autoCommit.bySession[args.sessionId]
@@ -14,7 +14,7 @@ export function clearActiveAutoCommitDriverIfMatches(trigger: TriggerFn): void {
14
14
  if (activeTrigger === trigger) activeTrigger = null
15
15
  }
16
16
 
17
- export function triggerAutoCommitNow(args: ActivityTransitionArgs): Promise<void> {
17
+ export async function triggerAutoCommitNow(args: ActivityTransitionArgs): Promise<void> {
18
18
  if (!activeTrigger) return Promise.resolve()
19
19
  return activeTrigger(args)
20
20
  }
@@ -1,7 +1,7 @@
1
1
  import type { MutableRefObject } from 'react'
2
2
 
3
3
  import type { SessionBackend } from '../session-backend/types'
4
- import type { AppAction, LayoutState, WorkspaceSnapshotV1 } from '../state/types'
4
+ import type { AppAction, LayoutState, TabSession, WorkspaceSnapshotV1 } from '../state/types'
5
5
 
6
6
  import { logInputDebug } from '../debug/input-log'
7
7
  import {
@@ -33,6 +33,22 @@ export function resizeSnapshotPanes(
33
33
  })
34
34
  }
35
35
 
36
+ function mergeSnapshotTabMetadata(
37
+ tabs: TabSession[],
38
+ workspaceSnapshot: WorkspaceSnapshotV1 | undefined
39
+ ): TabSession[] {
40
+ if (!workspaceSnapshot) return tabs
41
+ const persistedById = new Map(workspaceSnapshot.tabs.map((tab) => [tab.id, tab]))
42
+ return tabs.map((tab) => {
43
+ const persisted = persistedById.get(tab.id)
44
+ return persisted?.worktreeId != null &&
45
+ persisted?.worktreeId !== '' &&
46
+ !(tab.worktreeId != null && tab.worktreeId !== '')
47
+ ? { ...tab, worktreeId: persisted.worktreeId }
48
+ : tab
49
+ })
50
+ }
51
+
36
52
  function hydrateAttachedSession(
37
53
  dispatch: (action: AppAction) => void,
38
54
  sessionId: string,
@@ -47,7 +63,7 @@ function hydrateAttachedSession(
47
63
  layoutTree: workspaceSnapshot?.layoutTree,
48
64
  layoutTrees: workspaceSnapshot?.layoutTrees,
49
65
  tabGroupMap: workspaceSnapshot?.tabGroupMap,
50
- tabs: result.tabs,
66
+ tabs: mergeSnapshotTabMetadata(result.tabs, workspaceSnapshot),
51
67
  type: 'hydrate-workspace',
52
68
  })
53
69
  // Dispatch the session-status snapshot *after* hydrate-workspace so
@@ -8,10 +8,10 @@ import type {
8
8
  TabSession,
9
9
  TerminalModeState,
10
10
  } from '../state/types'
11
+ import type { TabRuntimeTimeouts } from './tab-runtime-timeouts'
11
12
 
12
13
  import { logInputDebug } from '../debug/input-log'
13
14
  import { clearTabSyntaxState, highlightSnapshot } from '../integrations/claude-syntax-overlay'
14
- import { type TabRuntimeTimeouts } from './tab-runtime-timeouts'
15
15
 
16
16
  interface BindBackendRuntimeEventsOptions {
17
17
  backend: SessionBackend
@@ -45,3 +45,55 @@ export function writePasteToTab(
45
45
  const payload = buildPtyPastePayload(text, tab?.terminalModes.bracketedPasteMode ?? false)
46
46
  writeToTab(backend, tabId, tab, payload, dispatch, { autoBottom: true })
47
47
  }
48
+
49
+ const DEL = '\x7f'
50
+ const CURSOR_LEFT = '\x1b[D'
51
+ const RAW_INLINE_MAX_LEN = 200
52
+
53
+ /**
54
+ * Write a macro expansion to a PTY:
55
+ * 1. Erase `eraseCount` previously echoed characters via DEL (readline-compatible).
56
+ * 2. Inject `expandedText` (raw if short single-line; bracketed-paste payload otherwise).
57
+ * 3. Move cursor left to `cursorOffset` via ANSI left-arrow sequences (outside any
58
+ * bracketed-paste wrapper so the application interprets them as keys, not text).
59
+ */
60
+ export function writeMacroExpansionToTab(
61
+ backend: SessionBackend,
62
+ tabId: string,
63
+ tab: TabSession | undefined,
64
+ eraseCount: number,
65
+ expandedText: string,
66
+ cursorOffset: number,
67
+ dispatch?: (action: AppAction) => void
68
+ ): void {
69
+ if (tab && shouldScrollViewportToBottom(tab)) {
70
+ backend.scrollViewportToBottom(tabId)
71
+ dispatch?.({ intent: { kind: 'bottom' }, tabId, type: 'set-scroll-intent' })
72
+ }
73
+
74
+ const erase = eraseCount > 0 ? DEL.repeat(eraseCount) : ''
75
+ const isShortInline = expandedText.length <= RAW_INLINE_MAX_LEN && !expandedText.includes('\n')
76
+ const bracketed = tab?.terminalModes.bracketedPasteMode ?? false
77
+
78
+ const body = isShortInline ? expandedText : buildPtyPastePayload(expandedText, bracketed)
79
+ const leftArrowCount = Math.max(0, expandedText.length - cursorOffset)
80
+ const leftArrows = leftArrowCount > 0 ? CURSOR_LEFT.repeat(leftArrowCount) : ''
81
+
82
+ logInputDebug('ptyWrite.macroExpansion', {
83
+ bodyLength: body.length,
84
+ cursorOffset,
85
+ eraseCount,
86
+ expandedTextLength: expandedText.length,
87
+ leftArrowCount,
88
+ tabId,
89
+ })
90
+
91
+ if (isShortInline) {
92
+ backend.write(tabId, `${erase}${body}${leftArrows}`)
93
+ return
94
+ }
95
+
96
+ if (erase) backend.write(tabId, erase)
97
+ backend.write(tabId, body)
98
+ if (leftArrows) backend.write(tabId, leftArrows)
99
+ }
@@ -4,7 +4,7 @@ interface SelectableLike {
4
4
  }
5
5
 
6
6
  function isUsableTarget(target: SelectableLike | null | undefined): target is SelectableLike {
7
- return Boolean(target?.selectable) && !target?.isDestroyed
7
+ return Boolean(target?.selectable) && !(target?.isDestroyed === true)
8
8
  }
9
9
 
10
10
  interface SelectionLike {
@@ -69,7 +69,7 @@ export function shiftSelectionByScroll(renderer: RendererSelectionApi, deltaLine
69
69
 
70
70
  const live = renderer.getSelection()
71
71
 
72
- if (!live?.isActive) {
72
+ if (!(live?.isActive === true)) {
73
73
  resetSelectionShiftState(renderer)
74
74
  return
75
75
  }
@@ -5,6 +5,8 @@ import { logInputDebug } from '../debug/input-log'
5
5
  import { createPrefixedId } from '../platform/id'
6
6
  import { saveSessionCatalog } from '../state/session-catalog'
7
7
  import { createEmptyWorkspaceSnapshot, serializeWorkspace } from '../state/session-persistence'
8
+ import { createPrimaryWorktree, ensureSessionWorktrees } from '../state/session-worktrees'
9
+ import { toast } from '../state/toast-store'
8
10
  import { buildSessionsWithCurrentSnapshot } from '../state/workspace-save'
9
11
 
10
12
  export function createSessionFromCurrentState(
@@ -14,10 +16,11 @@ export function createSessionFromCurrentState(
14
16
  ): { session: SessionRecord; sessions: SessionRecord[] } {
15
17
  const now = new Date().toISOString()
16
18
  const workspaceSnapshot =
17
- state.currentSessionId || state.tabs.length === 0
19
+ (state.currentSessionId != null && state.currentSessionId !== '') || state.tabs.length === 0
18
20
  ? createEmptyWorkspaceSnapshot()
19
21
  : serializeWorkspace(state)
20
22
  const session: SessionRecord = {
23
+ activeWorktreeId: undefined,
21
24
  createdAt: now,
22
25
  id: createPrefixedId('session'),
23
26
  lastOpenedAt: now,
@@ -25,10 +28,16 @@ export function createSessionFromCurrentState(
25
28
  projectPath,
26
29
  updatedAt: now,
27
30
  workspaceSnapshot,
31
+ worktrees: undefined,
32
+ }
33
+ if (projectPath != null && projectPath !== '') {
34
+ const worktree = createPrimaryWorktree(projectPath, now)
35
+ session.activeWorktreeId = worktree.id
36
+ session.worktrees = [worktree]
28
37
  }
29
38
 
30
39
  let updatedSessions = state.sessions
31
- if (state.currentSessionId) {
40
+ if (state.currentSessionId != null && state.currentSessionId !== '') {
32
41
  const currentSnapshot = serializeWorkspace(state)
33
42
  updatedSessions = state.sessions.map((entry) =>
34
43
  entry.id === state.currentSessionId
@@ -39,7 +48,7 @@ export function createSessionFromCurrentState(
39
48
 
40
49
  return {
41
50
  session,
42
- sessions: [...updatedSessions, session],
51
+ sessions: [...updatedSessions, ensureSessionWorktrees(session)],
43
52
  }
44
53
  }
45
54
 
@@ -79,7 +88,8 @@ export function handleCreateSessionEffect(
79
88
  ): void {
80
89
  const { session, sessions } = createSessionFromCurrentState(state, name, projectPath)
81
90
  logInputDebug('app.session.create', {
82
- fromCurrentWorkspace: !state.currentSessionId && state.tabs.length > 0,
91
+ fromCurrentWorkspace:
92
+ (state.currentSessionId == null || state.currentSessionId === '') && state.tabs.length > 0,
83
93
  name,
84
94
  sessionId: session.id,
85
95
  tabCount: session.workspaceSnapshot?.tabs.length ?? 0,
@@ -137,6 +147,7 @@ export function handleDeleteSessionEffect(
137
147
  sessionId: string,
138
148
  options?: { openSessionPicker?: boolean }
139
149
  ): void {
150
+ const name = state.sessions.find((entry) => entry.id === sessionId)?.name
140
151
  const remaining = deleteSessionRecords(state.sessions, sessionId)
141
152
  logInputDebug('app.session.delete', {
142
153
  remainingCount: remaining.length,
@@ -152,6 +163,7 @@ export function handleDeleteSessionEffect(
152
163
  sessionId,
153
164
  type: 'delete-session-record',
154
165
  })
166
+ toast.success(name != null && name !== '' ? `Deleted workspace "${name}"` : 'Workspace deleted')
155
167
  }
156
168
 
157
169
  export function restartTabSession(