@brimveyn/aimux 1.2.5 → 1.3.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 (59) hide show
  1. package/README.md +29 -8
  2. package/package.json +5 -5
  3. package/src/app-runtime/backend-runtime-events.ts +16 -1
  4. package/src/app-runtime/pty-write.ts +7 -4
  5. package/src/app-runtime/side-effects.ts +62 -4
  6. package/src/app-runtime/snippet-actions.ts +3 -2
  7. package/src/app-runtime/use-backend-runtime.ts +7 -2
  8. package/src/app-runtime/use-renderer-bindings.ts +2 -2
  9. package/src/app-runtime/use-terminal-resize.ts +15 -6
  10. package/src/app.tsx +89 -30
  11. package/src/config/loader.ts +2 -2
  12. package/src/config.ts +14 -3
  13. package/src/daemon/daemon.ts +279 -118
  14. package/src/daemon/runtime-paths.ts +34 -2
  15. package/src/daemon/session-manager.ts +20 -5
  16. package/src/daemon/session-registry.ts +18 -11
  17. package/src/index.tsx +19 -4
  18. package/src/input/keymap/describe-bindings.ts +68 -0
  19. package/src/input/keymap/key-format.ts +67 -0
  20. package/src/input/modes/bridge.ts +1 -0
  21. package/src/input/modes/transitions.ts +2 -0
  22. package/src/input/modes/types.ts +2 -0
  23. package/src/ipc/manager-protocol.ts +396 -0
  24. package/src/ipc/protocol.ts +98 -5
  25. package/src/platform/daemon-control.ts +42 -11
  26. package/src/profile-paths.ts +27 -0
  27. package/src/pty/pty-manager.ts +53 -3
  28. package/src/restart-daemon.ts +10 -10
  29. package/src/session-backend/bootstrap.ts +197 -46
  30. package/src/session-backend/local-session-backend.ts +12 -5
  31. package/src/session-backend/remote-session-backend.ts +143 -63
  32. package/src/session-backend/types.ts +4 -2
  33. package/src/state/reducers/modal-state.ts +18 -1
  34. package/src/state/reducers/tab-state.ts +20 -2
  35. package/src/state/session-catalog.ts +5 -4
  36. package/src/state/session-persistence.ts +9 -2
  37. package/src/state/snippet-catalog.ts +4 -4
  38. package/src/state/types.ts +23 -0
  39. package/src/state/validation.ts +8 -0
  40. package/src/terminal-manager/manager-client.ts +384 -0
  41. package/src/terminal-manager/terminal-manager.ts +288 -0
  42. package/src/ui/components/create-session-modal.tsx +3 -5
  43. package/src/ui/components/git-commit-modal.tsx +3 -5
  44. package/src/ui/components/help-modal.tsx +49 -68
  45. package/src/ui/components/list-item.tsx +24 -5
  46. package/src/ui/components/new-tab-modal.tsx +8 -9
  47. package/src/ui/components/pending-chord-overlay.tsx +28 -0
  48. package/src/ui/components/session-name-modal.tsx +3 -5
  49. package/src/ui/components/session-picker-modal.tsx +3 -1
  50. package/src/ui/components/snippet-editor-modal.tsx +3 -1
  51. package/src/ui/components/snippet-picker-modal.tsx +3 -1
  52. package/src/ui/components/status-bar.tsx +6 -2
  53. package/src/ui/components/theme-picker-modal.tsx +3 -6
  54. package/src/ui/components/update-available-modal.tsx +42 -0
  55. package/src/ui/keymap-context.ts +39 -0
  56. package/src/ui/root.tsx +12 -0
  57. package/src/ui/status-bar-model.ts +67 -39
  58. package/src/update/version-check.ts +67 -0
  59. package/src/update.ts +3 -3
@@ -1,5 +1,8 @@
1
+ import type { ModeId, ResolvedKeymapConfig } from '@brimveyn/aimux-config'
2
+
1
3
  import type { AppState, TabSession } from '../state/types'
2
4
 
5
+ import { buildHintText } from './keymap-context'
3
6
  import { abbreviatePath } from './path-format'
4
7
 
5
8
  export interface StatusBarModel {
@@ -8,6 +11,7 @@ export interface StatusBarModel {
8
11
  }
9
12
 
10
13
  const MAX_TAB_LABEL_LENGTH = 24
14
+ const HINT_LIMIT = 6
11
15
 
12
16
  function truncateLabel(label: string): string {
13
17
  if (label.length <= MAX_TAB_LABEL_LENGTH) {
@@ -25,39 +29,15 @@ function getActiveTabLabel(tab?: TabSession): string {
25
29
  return `${truncateLabel(tab.title)} (${tab.status})`
26
30
  }
27
31
 
28
- function getNavigationHint(activeTab?: TabSession): string {
29
- if (!activeTab) {
30
- return 'Ctrl+g sessions Ctrl+n new Ctrl+b toggle Ctrl+h/l resize ? help'
31
- }
32
-
33
- if (activeTab.status === 'disconnected') {
34
- return 'Ctrl+r restart restored tab Ctrl+w close i focus'
35
- }
36
-
37
- return 'Ctrl+g sessions j/k move Shift+J/K reorder Ctrl+r restart Ctrl+w close i focus ? help'
38
- }
39
-
40
- function getInputHint(activeTab?: TabSession): string {
41
- if (!activeTab) {
42
- return 'Ctrl+n new no active tab to focus'
43
- }
44
-
45
- if (activeTab.status === 'disconnected') {
46
- return 'Ctrl+z unfocus Ctrl+w layout Ctrl+r restart'
47
- }
48
-
49
- return 'Ctrl+z unfocus Ctrl+w layout typing goes to active tab'
32
+ function hintForMode(config: ResolvedKeymapConfig, modeId: ModeId): string {
33
+ return buildHintText(config, modeId, HINT_LIMIT)
50
34
  }
51
35
 
52
- function getGitHint(modalType: AppState['modal']['type']): string {
53
- if (modalType === 'git-commit') {
54
- return 'Tab switch field Enter newline Ctrl+Enter commit Esc cancel'
55
- }
56
-
57
- return 'j/k file Ctrl+d/u page a stage d unstage/delete c commit p push Esc exit'
58
- }
59
-
60
- export function getStatusBarModel(state: AppState, activeTab?: TabSession): StatusBarModel {
36
+ export function getStatusBarModel(
37
+ state: AppState,
38
+ activeTab: TabSession | undefined,
39
+ config: ResolvedKeymapConfig
40
+ ): StatusBarModel {
61
41
  const currentSession = state.currentSessionId
62
42
  ? state.sessions.find((session) => session.id === state.currentSessionId)
63
43
  : undefined
@@ -73,33 +53,81 @@ export function getStatusBarModel(state: AppState, activeTab?: TabSession): Stat
73
53
  case 'terminal-input':
74
54
  return {
75
55
  left: `${getActiveTabLabel(activeTab)} ${sessionIcon} ${sessionLabel}`,
76
- right: getInputHint(activeTab),
56
+ right: hintForMode(config, 'terminal-input'),
77
57
  }
78
- case 'modal':
58
+ case 'modal': {
59
+ const modalMode = deriveModalModeId(state.modal.type)
79
60
  return {
80
61
  left: `${sessionIcon} ${sessionLabel}`,
81
- right: 'j/k move Enter confirm n/r/d actions Esc cancel',
62
+ right: modalMode ? hintForMode(config, modalMode) : '',
82
63
  }
64
+ }
83
65
  case 'layout':
84
66
  return {
85
67
  left: `${getActiveTabLabel(activeTab)} ${sessionIcon} ${sessionLabel}`,
86
- right: 'h/j/k/l focus |/- split H/L resize q close Esc cancel',
68
+ right: hintForMode(config, 'layout'),
87
69
  }
88
70
  case 'git':
89
71
  return {
90
72
  left: `${sessionIcon} ${sessionLabel}`,
91
- right: getGitHint(state.modal.type),
73
+ right: hintForMode(config, 'git-mode'),
92
74
  }
93
- case 'command-edit':
75
+ case 'command-edit': {
76
+ const commandEditMode = deriveCommandEditModeId(state.modal.type)
94
77
  return {
95
78
  left: `${sessionIcon} ${sessionLabel}`,
96
- right: state.modal.type === 'git-commit' ? getGitHint('git-commit') : 'Esc cancel',
79
+ right: commandEditMode ? hintForMode(config, commandEditMode) : '',
97
80
  }
81
+ }
98
82
  case 'navigation':
99
83
  default:
100
84
  return {
101
85
  left: `${sessionIcon} ${sessionLabel} ${getActiveTabLabel(activeTab)}`,
102
- right: getNavigationHint(activeTab),
86
+ right: hintForMode(config, 'navigation'),
103
87
  }
104
88
  }
105
89
  }
90
+
91
+ function deriveModalModeId(modalType: AppState['modal']['type']): ModeId | null {
92
+ switch (modalType) {
93
+ case 'help':
94
+ return 'modal.help'
95
+ case 'new-tab':
96
+ return 'modal.new-tab'
97
+ case 'session-picker':
98
+ return 'modal.session-picker'
99
+ case 'snippet-picker':
100
+ return 'modal.snippet-picker'
101
+ case 'split-picker':
102
+ return 'modal.split-picker'
103
+ case 'theme-picker':
104
+ return 'modal.theme-picker'
105
+ case 'update-available':
106
+ return 'modal.update-available'
107
+ default:
108
+ return null
109
+ }
110
+ }
111
+
112
+ function deriveCommandEditModeId(modalType: AppState['modal']['type']): ModeId | null {
113
+ switch (modalType) {
114
+ case 'create-session':
115
+ return 'modal.create-session'
116
+ case 'git-commit':
117
+ return 'modal.git-commit'
118
+ case 'new-tab':
119
+ return 'modal.new-tab.command-edit'
120
+ case 'rename-tab':
121
+ return 'modal.rename-tab'
122
+ case 'session-name':
123
+ return 'modal.session-name'
124
+ case 'session-picker':
125
+ return 'modal.session-picker.filtering'
126
+ case 'snippet-editor':
127
+ return 'modal.snippet-editor'
128
+ case 'snippet-picker':
129
+ return 'modal.snippet-picker.filtering'
130
+ default:
131
+ return null
132
+ }
133
+ }
@@ -0,0 +1,67 @@
1
+ import { logDebug } from '../debug/input-log'
2
+
3
+ const NPM_REGISTRY_BASE = 'https://registry.npmjs.org'
4
+
5
+ export async function getCurrentPackageVersion(): Promise<string> {
6
+ const { version } = await import('../../package.json')
7
+ return version
8
+ }
9
+
10
+ export async function fetchLatestNpmVersion(packageName: string): Promise<string | null> {
11
+ const debugOverride = process.env.AIMUX_DEBUG_UPDATE_LATEST
12
+ if (debugOverride) {
13
+ return debugOverride
14
+ }
15
+
16
+ try {
17
+ const url = `${NPM_REGISTRY_BASE}/${encodeURIComponent(packageName).replace('%40', '@')}/latest`
18
+ const controller = new AbortController()
19
+ const timer = setTimeout(() => controller.abort(), 5_000)
20
+ const res = await fetch(url, { signal: controller.signal })
21
+ clearTimeout(timer)
22
+ if (!res.ok) {
23
+ logDebug('update.fetchLatest.nonOk', { packageName, status: res.status })
24
+ return null
25
+ }
26
+ const data = (await res.json()) as { version?: unknown }
27
+ if (typeof data.version !== 'string' || data.version.length === 0) {
28
+ return null
29
+ }
30
+ return data.version
31
+ } catch (error) {
32
+ logDebug('update.fetchLatest.error', {
33
+ error: error instanceof Error ? error.message : String(error),
34
+ packageName,
35
+ })
36
+ return null
37
+ }
38
+ }
39
+
40
+ function parseSemver(version: string): number[] | null {
41
+ const trimmed = version.trim().replace(/^v/, '')
42
+ const core = trimmed.split(/[-+]/)[0] ?? ''
43
+ const parts = core.split('.')
44
+ if (parts.length === 0) return null
45
+ const numeric: number[] = []
46
+ for (const part of parts) {
47
+ const n = Number.parseInt(part, 10)
48
+ if (!Number.isFinite(n) || n < 0) return null
49
+ numeric.push(n)
50
+ }
51
+ return numeric
52
+ }
53
+
54
+ export function isNewerVersion(latest: string, current: string): boolean {
55
+ const a = parseSemver(latest)
56
+ const b = parseSemver(current)
57
+ if (!a || !b) return false
58
+
59
+ const len = Math.max(a.length, b.length)
60
+ for (let i = 0; i < len; i++) {
61
+ const av = a[i] ?? 0
62
+ const bv = b[i] ?? 0
63
+ if (av > bv) return true
64
+ if (av < bv) return false
65
+ }
66
+ return false
67
+ }
package/src/update.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getDaemonSocketPath } from './daemon/runtime-paths'
2
- import { findDaemonPid } from './platform/daemon-control'
2
+ import { findIpcDaemonPid } from './platform/daemon-control'
3
3
  import { runRestartDaemon } from './restart-daemon'
4
4
 
5
5
  const REPO = 'BrimVeyn/aimux'
@@ -52,9 +52,9 @@ export async function runUpdate(): Promise<number> {
52
52
 
53
53
  process.stdout.write(`Updated to ${latest}.\n`)
54
54
 
55
- const pid = await findDaemonPid(getDaemonSocketPath())
55
+ const pid = await findIpcDaemonPid()
56
56
  if (pid !== null) {
57
- process.stdout.write('Restarting daemon...\n')
57
+ process.stdout.write(`Restarting IPC daemon at ${getDaemonSocketPath()}...\n`)
58
58
  await runRestartDaemon()
59
59
  }
60
60