@brimveyn/aimux 1.19.6 → 1.19.7

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.
@@ -3,6 +3,8 @@ import { connect, createServer, type Socket } from 'node:net'
3
3
 
4
4
  import type { AssistantId, TabSession, TabStatus, TerminalSnapshot } from '../state/types'
5
5
 
6
+ import { AutoRenameCoordinator, initialAutoRenameStatus } from '../auto-rename/coordinator'
7
+ import { loadUserConfig } from '../config/loader'
6
8
  import { logDebug } from '../debug/input-log'
7
9
  import { type ClaudeHookServer, startClaudeHookServer } from '../integrations/claude-hook-server'
8
10
  import {
@@ -70,6 +72,7 @@ export interface DaemonTabEntry {
70
72
  title?: string
71
73
  status?: TabStatus
72
74
  worktreeId?: string
75
+ autoRenameStatus?: 'eligible' | 'attempted'
73
76
  }
74
77
 
75
78
  /**
@@ -91,16 +94,25 @@ export function mergeTabRegistryEntry(
91
94
  command: string,
92
95
  initialViewport: TerminalSnapshot | undefined,
93
96
  allocateSeq: () => number,
94
- metadata?: { title?: string; status?: TabStatus; worktreeId?: string }
97
+ metadata?: {
98
+ title?: string
99
+ status?: TabStatus
100
+ worktreeId?: string
101
+ autoRenameStatus?: 'eligible' | 'attempted'
102
+ }
95
103
  ): DaemonTabEntry {
96
104
  const existing = registry.get(tabId)
97
105
  const viewport = existing?.viewport ?? initialViewport
106
+ const preserveAttemptedMetadata = existing?.autoRenameStatus === 'attempted'
98
107
  const entry: DaemonTabEntry = {
99
108
  assistant,
109
+ autoRenameStatus: preserveAttemptedMetadata
110
+ ? 'attempted'
111
+ : (metadata?.autoRenameStatus ?? existing?.autoRenameStatus),
100
112
  command,
101
113
  sessionId,
102
114
  status: metadata?.status ?? existing?.status,
103
- title: metadata?.title ?? existing?.title,
115
+ title: preserveAttemptedMetadata ? existing.title : (metadata?.title ?? existing?.title),
104
116
  viewport,
105
117
  viewportSeq: existing?.viewportSeq ?? (viewport ? allocateSeq() : 0),
106
118
  worktreeId: metadata?.worktreeId ?? existing?.worktreeId,
@@ -193,6 +205,7 @@ async function ensureTerminalManagerReady(manager: TerminalManagerClient): Promi
193
205
  }
194
206
 
195
207
  export async function runDaemon(): Promise<void> {
208
+ const resolvedConfig = await loadUserConfig()
196
209
  const socketPath = getIpcDaemonSocketPath()
197
210
  // A handoff file means we were spawned to take over from a predecessor
198
211
  // daemon that already drained and renamed its socket away. Consume the
@@ -247,8 +260,13 @@ export async function runDaemon(): Promise<void> {
247
260
  assistant: AssistantId,
248
261
  command: string,
249
262
  initialViewport?: TerminalSnapshot,
250
- metadata?: { title?: string; status?: TabStatus; worktreeId?: string }
251
- ): void => {
263
+ metadata?: {
264
+ title?: string
265
+ status?: TabStatus
266
+ worktreeId?: string
267
+ autoRenameStatus?: 'eligible' | 'attempted'
268
+ }
269
+ ): DaemonTabEntry => {
252
270
  const before = tabRegistry.get(tabId)
253
271
  const entry = mergeTabRegistryEntry(
254
272
  tabRegistry,
@@ -270,6 +288,7 @@ export async function runDaemon(): Promise<void> {
270
288
  sessionId,
271
289
  tabId,
272
290
  })
291
+ return entry
273
292
  }
274
293
 
275
294
  const broadcastAll = (event: ServerEvent): void => {
@@ -315,6 +334,45 @@ export async function runDaemon(): Promise<void> {
315
334
  }
316
335
  }
317
336
 
337
+ const applyTabMetadata = (
338
+ tabId: string,
339
+ patch: { title?: string; autoRenameStatus?: 'eligible' | 'attempted' }
340
+ ): void => {
341
+ const entry = tabRegistry.get(tabId)
342
+ if (!entry) return
343
+ if (patch.title !== undefined) entry.title = patch.title
344
+ if (patch.autoRenameStatus !== undefined) entry.autoRenameStatus = patch.autoRenameStatus
345
+ void (async () => {
346
+ try {
347
+ await manager.updateTabMetadata(entry.sessionId, tabId, patch)
348
+ } catch (error) {
349
+ logDebug('daemon.tabMetadata.managerFailed', {
350
+ error: error instanceof Error ? error.message : String(error),
351
+ tabId,
352
+ })
353
+ }
354
+ })()
355
+ broadcastForSessionVersioned(entry.sessionId, 14, {
356
+ payload: { ...patch, sessionId: entry.sessionId, tabId },
357
+ type: 'tabMetadataUpdated',
358
+ })
359
+ }
360
+
361
+ const autoRename = new AutoRenameCoordinator({
362
+ config: resolvedConfig.autoRename,
363
+ getTab: (tabId) => {
364
+ const entry = tabRegistry.get(tabId)
365
+ if (!entry) return
366
+ return {
367
+ assistant: entry.assistant,
368
+ autoRenameStatus: entry.autoRenameStatus,
369
+ id: tabId,
370
+ title: entry.title ?? '',
371
+ }
372
+ },
373
+ updateTab: applyTabMetadata,
374
+ })
375
+
318
376
  // Count UI attachers so workspace/worktree handlers can decide whether to
319
377
  // relay via broadcast (UI attached → its reducer owns the write) or mutate
320
378
  // the catalog directly. A "UI attacher" here is any non-thin attach — thin
@@ -352,6 +410,7 @@ export async function runDaemon(): Promise<void> {
352
410
  manager.on('exit', (sessionId, tabId, exitCode) => {
353
411
  logDebug('daemon.manager.exit', { exitCode, sessionId, tabId })
354
412
  tabRegistry.delete(tabId)
413
+ autoRename.unregister(tabId)
355
414
  if (sessionActiveTabIds.get(sessionId) === tabId) {
356
415
  sessionActiveTabIds.set(sessionId, null)
357
416
  }
@@ -611,14 +670,25 @@ export async function runDaemon(): Promise<void> {
611
670
  })
612
671
  sessionActiveTabIds.set(message.payload.sessionId, attachResult.activeTabId)
613
672
  for (const tab of attachResult.tabs) {
614
- rememberTab(
673
+ const remembered = rememberTab(
615
674
  message.payload.sessionId,
616
675
  tab.id,
617
676
  tab.assistant,
618
677
  tab.command,
619
678
  tab.viewport,
620
- { status: tab.status, title: tab.title, worktreeId: tab.worktreeId }
679
+ {
680
+ autoRenameStatus: tab.autoRenameStatus,
681
+ status: tab.status,
682
+ title: tab.title,
683
+ worktreeId: tab.worktreeId,
684
+ }
621
685
  )
686
+ autoRename.register({
687
+ assistant: remembered.assistant,
688
+ autoRenameStatus: remembered.autoRenameStatus,
689
+ id: tab.id,
690
+ title: remembered.title ?? tab.title,
691
+ })
622
692
  }
623
693
  // Classify synchronously BEFORE sending attachResult so
624
694
  // each tab's activity and the full session-status snapshot
@@ -637,10 +707,15 @@ export async function runDaemon(): Promise<void> {
637
707
  })),
638
708
  })
639
709
  statusLoop.classifyNow(message.payload.sessionId, tabsForLoop)
640
- const tabsWithActivity = attachResult.tabs.map((tab) => ({
641
- ...tab,
642
- activity: statusLoop.getTabStatus(tab.id) ?? tab.activity,
643
- }))
710
+ const tabsWithActivity = attachResult.tabs.map((tab) => {
711
+ const metadata = tabRegistry.get(tab.id)
712
+ return {
713
+ ...tab,
714
+ activity: statusLoop.getTabStatus(tab.id) ?? tab.activity,
715
+ autoRenameStatus: metadata?.autoRenameStatus ?? tab.autoRenameStatus,
716
+ title: metadata?.title ?? tab.title,
717
+ }
718
+ })
644
719
  const initialSessionStatuses = statusLoop.snapshotSessions()
645
720
  logDebug('daemon.attach.replay', {
646
721
  sessionId: message.payload.sessionId,
@@ -699,6 +774,11 @@ export async function runDaemon(): Promise<void> {
699
774
  message.payload.command,
700
775
  ...(message.payload.args ?? []),
701
776
  ].join(' ')
777
+ const autoRenameStatus = initialAutoRenameStatus(
778
+ resolvedConfig.autoRename,
779
+ message.payload.assistant,
780
+ message.payload.autoRenameCandidate === true
781
+ )
702
782
  rememberTab(
703
783
  sessionId,
704
784
  message.payload.tabId,
@@ -706,6 +786,7 @@ export async function runDaemon(): Promise<void> {
706
786
  fullCommand,
707
787
  undefined,
708
788
  {
789
+ autoRenameStatus,
709
790
  status: 'starting',
710
791
  title: message.payload.title,
711
792
  worktreeId: message.payload.worktreeId,
@@ -719,7 +800,16 @@ export async function runDaemon(): Promise<void> {
719
800
  // even on PTYs that outlive this daemon process.
720
801
  const env: Record<string, string> = { AIMUX_PANE_ID: message.payload.tabId }
721
802
  if (hookServer) env.AIMUX_HOOK_URL_FILE = hookUrlFilePath
722
- await manager.createTab({ ...message.payload, cols, env, rows, sessionId })
803
+ const { autoRenameCandidate: _autoRenameCandidate, ...managerTabPayload } =
804
+ message.payload
805
+ await manager.createTab({
806
+ ...managerTabPayload,
807
+ autoRenameStatus,
808
+ cols,
809
+ env,
810
+ rows,
811
+ sessionId,
812
+ })
723
813
  sendOk(socket, message.id)
724
814
  // Fan a `tabAdded` event only to peers that negotiated at
725
815
  // least v11 — older parsers throw on unknown message types
@@ -728,6 +818,7 @@ export async function runDaemon(): Promise<void> {
728
818
  const synthesizedTab: TabSession = {
729
819
  activity: 'idle',
730
820
  assistant: message.payload.assistant,
821
+ autoRenameStatus,
731
822
  buffer: '',
732
823
  command: fullCommand,
733
824
  id: message.payload.tabId,
@@ -740,6 +831,7 @@ export async function runDaemon(): Promise<void> {
740
831
  payload: { sessionId, tab: synthesizedTab },
741
832
  type: 'tabAdded',
742
833
  })
834
+ autoRename.register(synthesizedTab)
743
835
  logDebug('daemon.request.createTab.success', {
744
836
  sessionId,
745
837
  tabId: message.payload.tabId,
@@ -750,6 +842,21 @@ export async function runDaemon(): Promise<void> {
750
842
  const sessionId = requireSession(socket, attachedSessions)
751
843
  requireNegotiatedVersion(socket, negotiatedVersions)
752
844
  await manager.write(sessionId, message.payload.tabId, message.payload.data)
845
+ autoRename.observeWrite(message.payload.tabId, message.payload.data)
846
+ sendOk(socket, message.id)
847
+ break
848
+ }
849
+ case 'renameTab': {
850
+ const sessionId = requireSession(socket, attachedSessions)
851
+ requireNegotiatedVersion(socket, negotiatedVersions)
852
+ if (tabRegistry.get(message.payload.tabId)?.sessionId !== sessionId) {
853
+ throw new Error(`Tab not found in attached session: ${message.payload.tabId}`)
854
+ }
855
+ autoRename.manualRename(message.payload.tabId)
856
+ applyTabMetadata(message.payload.tabId, {
857
+ autoRenameStatus: 'attempted',
858
+ title: message.payload.title.trim(),
859
+ })
753
860
  sendOk(socket, message.id)
754
861
  break
755
862
  }
@@ -806,6 +913,7 @@ export async function runDaemon(): Promise<void> {
806
913
  const sessionId = requireSession(socket, attachedSessions)
807
914
  requireNegotiatedVersion(socket, negotiatedVersions)
808
915
  tabRegistry.delete(message.payload.tabId)
916
+ autoRename.unregister(message.payload.tabId)
809
917
  if (sessionActiveTabIds.get(sessionId) === message.payload.tabId) {
810
918
  sessionActiveTabIds.set(sessionId, null)
811
919
  }
@@ -851,7 +959,10 @@ export async function runDaemon(): Promise<void> {
851
959
  requireNegotiatedVersion(socket, negotiatedVersions)
852
960
  if (sessionId != null && sessionId !== '') {
853
961
  for (const [tabId, entry] of tabRegistry) {
854
- if (entry.sessionId === sessionId) tabRegistry.delete(tabId)
962
+ if (entry.sessionId === sessionId) {
963
+ autoRename.unregister(tabId)
964
+ tabRegistry.delete(tabId)
965
+ }
855
966
  }
856
967
  sessionActiveTabIds.delete(sessionId)
857
968
  sessionDimensions.delete(sessionId)
@@ -67,6 +67,14 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
67
67
  this.getOrCreateRegistry(sessionId).write(tabId, data)
68
68
  }
69
69
 
70
+ updateTabMetadata(
71
+ sessionId: string,
72
+ tabId: string,
73
+ patch: { title?: string; autoRenameStatus?: 'eligible' | 'attempted' }
74
+ ): void {
75
+ this.getOrCreateRegistry(sessionId).updateTabMetadata(tabId, patch)
76
+ }
77
+
70
78
  resize(sessionId: string, cols: number, rows: number, options?: { sync?: boolean }): void {
71
79
  this.getOrCreateRegistry(sessionId).resizeAll(cols, rows, options)
72
80
  }
@@ -89,7 +89,10 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
89
89
  for (const persisted of snapshot.tabs) {
90
90
  const existing = this.tabs.get(persisted.id)
91
91
  if (existing) {
92
- existing.title = persisted.title
92
+ if (existing.autoRenameStatus !== 'attempted') {
93
+ existing.title = persisted.title
94
+ existing.autoRenameStatus = persisted.autoRenameStatus
95
+ }
93
96
  existing.worktreeId = persisted.worktreeId
94
97
  }
95
98
  }
@@ -145,6 +148,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
145
148
  cwd?: string
146
149
  /** Extra env injected into the spawned shell. Passed through to the PTY. */
147
150
  env?: Record<string, string>
151
+ autoRenameStatus?: 'eligible' | 'attempted'
148
152
  /**
149
153
  * Worktree this tab belongs to (for UI grouping). Stored on the
150
154
  * TabSession so an attach-time replay surfaces it inside the right
@@ -164,6 +168,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
164
168
  this.tabs.set(options.tabId, {
165
169
  activity: 'idle',
166
170
  assistant: options.assistant,
171
+ autoRenameStatus: options.autoRenameStatus,
167
172
  buffer: '',
168
173
  command: [options.command, ...(options.args ?? [])].join(' '),
169
174
  id: options.tabId,
@@ -182,6 +187,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
182
187
  existing.assistant = options.assistant
183
188
  existing.title = options.title
184
189
  existing.command = [options.command, ...(options.args ?? [])].join(' ')
190
+ existing.autoRenameStatus = options.autoRenameStatus
185
191
  if (options.worktreeId !== undefined) existing.worktreeId = options.worktreeId
186
192
  }
187
193
 
@@ -189,6 +195,16 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
189
195
  this.ptyManager.createSession(options)
190
196
  }
191
197
 
198
+ updateTabMetadata(
199
+ tabId: string,
200
+ patch: { title?: string; autoRenameStatus?: 'eligible' | 'attempted' }
201
+ ): void {
202
+ const tab = this.tabs.get(tabId)
203
+ if (!tab) return
204
+ if (patch.title !== undefined) tab.title = patch.title
205
+ if (patch.autoRenameStatus !== undefined) tab.autoRenameStatus = patch.autoRenameStatus
206
+ }
207
+
192
208
  /**
193
209
  * Read the in-memory TabSession for a tab. Returned by reference — used by
194
210
  * the daemon's `tabAdded` broadcast path to publish the same shape the UI
package/src/index.tsx CHANGED
@@ -136,6 +136,8 @@ if (command === '--help' || command === '-h' || command === 'help') {
136
136
  process.exit(0)
137
137
  }
138
138
 
139
+ const resolvedConfig = await loadUserConfig()
140
+
139
141
  const renderer = await createCliRenderer({
140
142
  autoFocus: true,
141
143
  // Transparent clear color so cells untouched by BoxRenderable paints (e.g.
@@ -164,7 +166,6 @@ try {
164
166
 
165
167
  const root = createRoot(renderer)
166
168
 
167
- const resolvedConfig = await loadUserConfig()
168
169
  logDebug('index.userConfigLoaded', {
169
170
  leader: resolvedConfig.keymaps.leader,
170
171
  modeCount: resolvedConfig.keymaps.modes.size,
@@ -179,6 +180,7 @@ if (resolvedConfig.theme?.beta?.experimentalSyntaxHighlight === true) {
179
180
  }
180
181
 
181
182
  const backend = await createSessionBackend({
183
+ autoRenameConfig: resolvedConfig.autoRename,
182
184
  onBreakingUpdateRequired: () =>
183
185
  new Promise<void>((resolve) => {
184
186
  root.render(<BreakingUpdateScreen onConfirm={resolve} />)
@@ -52,6 +52,7 @@ export type SideEffect =
52
52
  | { type: 'apply-theme'; action: 'confirm' }
53
53
  | { type: 'apply-theme'; action: 'preview'; delta: 1 | -1 }
54
54
  | { type: 'rename-session'; sessionId: string; name: string }
55
+ | { type: 'rename-tab'; tabId: string; title: string }
55
56
  | {
56
57
  type: 'split-pane'
57
58
  direction: SplitDirection
@@ -32,8 +32,11 @@ import {
32
32
  // either way (pre-v7 TM → new client never sees the indices; new TM →
33
33
  // pre-v7 client ignores them and falls back to the theme default), so Min
34
34
  // is raised in lockstep to force matching binaries.
35
+ //
36
+ // v9: additive — `tabMetadata` updates titles and auto-rename state without
37
+ // restarting PTYs. Capability-gated; MIN remains at 8.
35
38
  export const MANAGER_PROTOCOL_MIN_VERSION = 8
36
- export const MANAGER_PROTOCOL_VERSION = 8
39
+ export const MANAGER_PROTOCOL_VERSION = 9
37
40
 
38
41
  /**
39
42
  * Capability strings advertised by *this* process in its `helloResult`. New
@@ -44,8 +47,11 @@ export const MANAGER_PROTOCOL_VERSION = 8
44
47
  export const MANAGER_PROTOCOL_CAPABILITIES: readonly string[] = [
45
48
  'setBroadcastEnabled',
46
49
  'createTabWorktreeId',
50
+ 'tabMetadata',
47
51
  ]
48
52
 
53
+ export const MANAGER_CAPABILITY_TAB_METADATA = 'tabMetadata'
54
+
49
55
  /**
50
56
  * Capability name a daemon must observe on the TM's helloResult before
51
57
  * sending `setBroadcastEnabled`.
@@ -119,9 +125,20 @@ export type ManagerRequest =
119
125
  * `worktreeId = undefined`.
120
126
  */
121
127
  worktreeId?: string
128
+ autoRenameStatus?: 'eligible' | 'attempted'
122
129
  }
123
130
  }
124
131
  | { id: string; type: 'write'; payload: { sessionId: string; tabId: string; data: string } }
132
+ | {
133
+ id: string
134
+ type: 'updateTabMetadata'
135
+ payload: {
136
+ sessionId: string
137
+ tabId: string
138
+ title?: string
139
+ autoRenameStatus?: 'eligible' | 'attempted'
140
+ }
141
+ }
125
142
  | {
126
143
  id: string
127
144
  type: 'resizeClient'
@@ -364,12 +381,32 @@ export function parseManagerRequest(value: unknown): ManagerRequest {
364
381
  value.payload.worktreeId === undefined || isString(value.payload.worktreeId),
365
382
  'createTab.worktreeId must be a string when present'
366
383
  )
384
+ assert(
385
+ value.payload.autoRenameStatus === undefined ||
386
+ value.payload.autoRenameStatus === 'eligible' ||
387
+ value.payload.autoRenameStatus === 'attempted',
388
+ 'createTab.autoRenameStatus is invalid'
389
+ )
367
390
  return value as ManagerRequest
368
391
  case 'write':
369
392
  assert(isString(value.payload.sessionId), 'write.sessionId must be a string')
370
393
  assert(isString(value.payload.tabId), 'write.tabId must be a string')
371
394
  assert(isString(value.payload.data), 'write.data must be a string')
372
395
  return value as ManagerRequest
396
+ case 'updateTabMetadata':
397
+ assert(isString(value.payload.sessionId), 'updateTabMetadata.sessionId must be a string')
398
+ assert(isString(value.payload.tabId), 'updateTabMetadata.tabId must be a string')
399
+ assert(
400
+ value.payload.title === undefined || isString(value.payload.title),
401
+ 'updateTabMetadata.title must be a string when present'
402
+ )
403
+ assert(
404
+ value.payload.autoRenameStatus === undefined ||
405
+ value.payload.autoRenameStatus === 'eligible' ||
406
+ value.payload.autoRenameStatus === 'attempted',
407
+ 'updateTabMetadata.autoRenameStatus is invalid'
408
+ )
409
+ return value as ManagerRequest
373
410
  case 'resizeClient':
374
411
  assert(isString(value.payload.sessionId), 'resizeClient.sessionId must be a string')
375
412
  assert(isFiniteNumber(value.payload.cols), 'resizeClient.cols must be a number')
@@ -37,8 +37,11 @@ import { isWorkspaceSnapshotV1, isWorktreeRecord } from '../state/validation'
37
37
  // plus best-effort parsed options), and an additive `lastLine` field on
38
38
  // `TabSessionSummary`. Gated behind `turnLifecycle`, `questionEvents`, and
39
39
  // `listTabsLastLine` respectively; MIN stays at 10.
40
+ //
41
+ // v14: additive — tab metadata synchronization for manual and automatic
42
+ // renames. The new request/event are capability-gated; MIN stays at 10.
40
43
  export const IPC_PROTOCOL_MIN_VERSION = 10
41
- export const IPC_PROTOCOL_VERSION = 13
44
+ export const IPC_PROTOCOL_VERSION = 14
42
45
 
43
46
  /**
44
47
  * Capability advertised by a daemon that knows how to drain + handoff its
@@ -137,6 +140,9 @@ export const IPC_CAPABILITY_QUESTION_EVENTS = 'questionEvents'
137
140
  */
138
141
  export const IPC_CAPABILITY_LIST_TABS_LAST_LINE = 'listTabsLastLine'
139
142
 
143
+ /** Additive tab-title and auto-rename metadata synchronization. */
144
+ export const IPC_CAPABILITY_TAB_METADATA = 'tabMetadata'
145
+
140
146
  /**
141
147
  * Capabilities advertised by *this* process in its `helloResult`. Additive
142
148
  * features should be introduced as new capability strings here rather than
@@ -160,6 +166,7 @@ export const IPC_PROTOCOL_CAPABILITIES: readonly string[] = [
160
166
  IPC_CAPABILITY_TURN_LIFECYCLE,
161
167
  IPC_CAPABILITY_QUESTION_EVENTS,
162
168
  IPC_CAPABILITY_LIST_TABS_LAST_LINE,
169
+ IPC_CAPABILITY_TAB_METADATA,
163
170
  ]
164
171
 
165
172
  export interface ProtocolHelloRequest {
@@ -269,9 +276,12 @@ export type ClientRequest =
269
276
  * only forwards it to the TM when its own capability is in play.
270
277
  */
271
278
  worktreeId?: string
279
+ /** True only when the creator did not provide an explicit title. */
280
+ autoRenameCandidate?: boolean
272
281
  }
273
282
  }
274
283
  | { id: string; type: 'write'; payload: { tabId: string; data: string } }
284
+ | { id: string; type: 'renameTab'; payload: { tabId: string; title: string } }
275
285
  | {
276
286
  id: string
277
287
  type: 'resizeClient'
@@ -395,6 +405,15 @@ export type ServerEvent =
395
405
  type: 'tabAdded'
396
406
  payload: { sessionId: string; tab: TabSession }
397
407
  }
408
+ | {
409
+ type: 'tabMetadataUpdated'
410
+ payload: {
411
+ sessionId: string
412
+ tabId: string
413
+ title?: string
414
+ autoRenameStatus?: 'eligible' | 'attempted'
415
+ }
416
+ }
398
417
  // v12 / capability `workspaceLifecycle`. Broadcast to every socket when a
399
418
  // CLI issues `createWorkspace` while a UI is attached — the UI runs its
400
419
  // create-session handler so the live workspace snapshot is preserved.
@@ -608,6 +627,9 @@ function isTabSession(value: unknown): value is TabSession {
608
627
  isString(value.buffer) &&
609
628
  isTerminalModeState(value.terminalModes) &&
610
629
  isString(value.command) &&
630
+ (value.autoRenameStatus === undefined ||
631
+ value.autoRenameStatus === 'eligible' ||
632
+ value.autoRenameStatus === 'attempted') &&
611
633
  (value.viewport === undefined || isTerminalSnapshot(value.viewport)) &&
612
634
  (value.errorMessage === undefined || isString(value.errorMessage)) &&
613
635
  (value.exitCode === undefined || isFiniteNumber(value.exitCode)) &&
@@ -714,11 +736,23 @@ export function parseClientRequest(value: unknown): ClientRequest {
714
736
  value.payload.worktreeId === undefined || isString(value.payload.worktreeId),
715
737
  'createTab.worktreeId must be a string when present'
716
738
  )
739
+ assert(
740
+ value.payload.autoRenameCandidate === undefined ||
741
+ typeof value.payload.autoRenameCandidate === 'boolean',
742
+ 'createTab.autoRenameCandidate must be a boolean when present'
743
+ )
717
744
  return value as ClientRequest
718
745
  case 'write':
719
746
  assert(isString(value.payload.tabId), 'write.tabId must be a string')
720
747
  assert(isString(value.payload.data), 'write.data must be a string')
721
748
  return value as ClientRequest
749
+ case 'renameTab':
750
+ assert(isString(value.payload.tabId), 'renameTab.tabId must be a string')
751
+ assert(
752
+ isString(value.payload.title) && value.payload.title.trim().length > 0,
753
+ 'renameTab.title must be a non-empty string'
754
+ )
755
+ return value as ClientRequest
722
756
  case 'resizeClient':
723
757
  assert(isFiniteNumber(value.payload.cols), 'resizeClient.cols must be a number')
724
758
  assert(isFiniteNumber(value.payload.rows), 'resizeClient.rows must be a number')
@@ -872,6 +906,20 @@ export function parseServerMessage(value: unknown): ServerResponse | ServerEvent
872
906
  assert(isString(value.payload.sessionId), 'tabAdded.sessionId must be a string')
873
907
  assert(isTabSession(value.payload.tab), 'tabAdded.tab is invalid')
874
908
  return value as ServerEvent
909
+ case 'tabMetadataUpdated':
910
+ assert(isString(value.payload.sessionId), 'tabMetadataUpdated.sessionId must be a string')
911
+ assert(isString(value.payload.tabId), 'tabMetadataUpdated.tabId must be a string')
912
+ assert(
913
+ value.payload.title === undefined || isString(value.payload.title),
914
+ 'tabMetadataUpdated.title must be a string when present'
915
+ )
916
+ assert(
917
+ value.payload.autoRenameStatus === undefined ||
918
+ value.payload.autoRenameStatus === 'eligible' ||
919
+ value.payload.autoRenameStatus === 'attempted',
920
+ 'tabMetadataUpdated.autoRenameStatus is invalid'
921
+ )
922
+ return value as ServerEvent
875
923
  case 'sessionStatus':
876
924
  assert(isString(value.payload.sessionId), 'sessionStatus.sessionId must be a string')
877
925
  assert(isSessionStatus(value.payload.status), 'sessionStatus.status is invalid')
@@ -1,5 +1,6 @@
1
1
  import { connect } from 'node:net'
2
2
 
3
+ import type { AutoRenameConfigSnapshot } from '../auto-rename/coordinator'
3
4
  import type { SessionBackend } from './types'
4
5
 
5
6
  import { negotiateDaemonReexec, waitForSocketRemoval } from '../daemon/reexec-client'
@@ -273,10 +274,11 @@ async function stopTerminalManager(): Promise<void> {
273
274
 
274
275
  export async function createSessionBackend(opts?: {
275
276
  onBreakingUpdateRequired?: () => Promise<void>
277
+ autoRenameConfig?: AutoRenameConfigSnapshot
276
278
  }): Promise<SessionBackend> {
277
279
  if (process.env.AIMUX_LOCAL_BACKEND === '1') {
278
280
  logDebug('backend.create.localExplicit')
279
- return new LocalSessionBackend()
281
+ return new LocalSessionBackend(opts?.autoRenameConfig)
280
282
  }
281
283
 
282
284
  const socketPath = getIpcDaemonSocketPath()