@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.
@@ -8,6 +8,11 @@ import type {
8
8
  } from '../state/types'
9
9
  import type { ResizeOptions, SessionBackend, SessionBackendEvents } from './types'
10
10
 
11
+ import {
12
+ type AutoRenameConfigSnapshot,
13
+ AutoRenameCoordinator,
14
+ initialAutoRenameStatus,
15
+ } from '../auto-rename/coordinator'
11
16
  import { SessionManager } from '../daemon/session-manager'
12
17
  import { logDebug } from '../debug/input-log'
13
18
  import { runStatusDetectionLoop } from '../pty/assistant-status-detection-loop'
@@ -40,9 +45,26 @@ export class LocalSessionBackend
40
45
  string,
41
46
  { viewport: TerminalSnapshot; terminalModes: TerminalModeState }
42
47
  >()
48
+ private readonly autoRename: AutoRenameCoordinator
49
+ private readonly autoRenameConfig: AutoRenameConfigSnapshot
43
50
 
44
- constructor() {
51
+ constructor(
52
+ autoRenameConfig: AutoRenameConfigSnapshot = { enabled: false, models: {}, timeoutMs: 15_000 }
53
+ ) {
45
54
  super()
55
+ this.autoRenameConfig = autoRenameConfig
56
+ this.autoRename = new AutoRenameCoordinator({
57
+ config: autoRenameConfig,
58
+ getTab: (tabId) => this.findTab(tabId)?.tab,
59
+ updateTab: (tabId, patch) => {
60
+ const found = this.findTab(tabId)
61
+ if (!found) return
62
+ this.sessionManager.updateTabMetadata(found.sessionId, tabId, patch)
63
+ if (found.sessionId === this.currentSessionId) {
64
+ this.emit('tabMetadataUpdated', found.sessionId, tabId, patch)
65
+ }
66
+ },
67
+ })
46
68
  this.sessionManager.on('render', (sessionId, tabId, viewport, terminalModes) => {
47
69
  if (sessionId !== this.currentSessionId) return
48
70
  if (this.paneReady.get(tabId) === false) {
@@ -106,6 +128,7 @@ export class LocalSessionBackend
106
128
  )
107
129
  for (const tab of attachResult.tabs) {
108
130
  this.gatePaneRender(tab.id)
131
+ this.autoRename.register(tab)
109
132
  }
110
133
  // Run a synchronous classification pass so every tab's activity and the
111
134
  // session-status snapshot are available to embed in the reply — mirrors
@@ -133,6 +156,7 @@ export class LocalSessionBackend
133
156
  rows: number
134
157
  cwd?: string
135
158
  worktreeId?: string
159
+ autoRenameCandidate?: boolean
136
160
  }): void {
137
161
  if (!(this.currentSessionId != null && this.currentSessionId !== '')) {
138
162
  logDebug('backend.local.skipCreateWithoutSession', { tabId: options.tabId })
@@ -145,7 +169,14 @@ export class LocalSessionBackend
145
169
  worktreeId: options.worktreeId ?? null,
146
170
  })
147
171
  this.gatePaneRender(options.tabId)
148
- this.sessionManager.createTab(this.currentSessionId, options)
172
+ const autoRenameStatus = initialAutoRenameStatus(
173
+ this.getAutoRenameConfig(),
174
+ options.assistant,
175
+ options.autoRenameCandidate === true
176
+ )
177
+ this.sessionManager.createTab(this.currentSessionId, { ...options, autoRenameStatus })
178
+ const tab = this.findTab(options.tabId)?.tab
179
+ if (tab) this.autoRename.register(tab)
149
180
  }
150
181
 
151
182
  /** Suppress render emission for this tab until the frontend acknowledges its
@@ -175,9 +206,21 @@ export class LocalSessionBackend
175
206
  sessionId: this.currentSessionId,
176
207
  tabId,
177
208
  })
209
+ this.autoRename.observeWrite(tabId, input)
178
210
  this.sessionManager.write(this.currentSessionId, tabId, input)
179
211
  }
180
212
 
213
+ renameTab(tabId: string, title: string): void {
214
+ const found = this.findTab(tabId)
215
+ if (!found) return
216
+ this.autoRename.manualRename(tabId)
217
+ const patch = { autoRenameStatus: 'attempted' as const, title }
218
+ this.sessionManager.updateTabMetadata(found.sessionId, tabId, patch)
219
+ if (found.sessionId === this.currentSessionId) {
220
+ this.emit('tabMetadataUpdated', found.sessionId, tabId, patch)
221
+ }
222
+ }
223
+
181
224
  scrollViewport(tabId: string, deltaLines: number): void {
182
225
  if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
183
226
  this.sessionManager.scroll(this.currentSessionId, tabId, deltaLines)
@@ -212,6 +255,7 @@ export class LocalSessionBackend
212
255
  logDebug('backend.local.disposeSession', { sessionId: this.currentSessionId, tabId })
213
256
  this.paneReady.delete(tabId)
214
257
  this.pendingRender.delete(tabId)
258
+ this.autoRename.unregister(tabId)
215
259
  this.sessionManager.closeTab(this.currentSessionId, tabId)
216
260
  }
217
261
 
@@ -220,6 +264,9 @@ export class LocalSessionBackend
220
264
  logDebug('backend.local.disposeAll', { sessionId: this.currentSessionId })
221
265
  this.paneReady.clear()
222
266
  this.pendingRender.clear()
267
+ for (const tab of this.sessionManager.listTabs(this.currentSessionId)) {
268
+ this.autoRename.unregister(tab.id)
269
+ }
223
270
  this.sessionManager.disposeSession(this.currentSessionId)
224
271
  }
225
272
 
@@ -235,4 +282,18 @@ export class LocalSessionBackend
235
282
  announceWorkspaceSwitched(_sessionId: string): void {
236
283
  // No daemon on the local backend, so there's no CLI to notify.
237
284
  }
285
+
286
+ private findTab(
287
+ tabId: string
288
+ ): { sessionId: string; tab: ReturnType<SessionManager['listTabs']>[number] } | null {
289
+ for (const sessionId of this.sessionManager.listSessionIds()) {
290
+ const tab = this.sessionManager.listTabs(sessionId).find((entry) => entry.id === tabId)
291
+ if (tab) return { sessionId, tab }
292
+ }
293
+ return null
294
+ }
295
+
296
+ private getAutoRenameConfig(): AutoRenameConfigSnapshot {
297
+ return this.autoRenameConfig
298
+ }
238
299
  }
@@ -10,6 +10,7 @@ import {
10
10
  type AttachResult,
11
11
  type ClientRequest,
12
12
  encodeMessage,
13
+ IPC_CAPABILITY_TAB_METADATA,
13
14
  IPC_PROTOCOL_MIN_VERSION,
14
15
  IPC_PROTOCOL_VERSION,
15
16
  MessageDecoder,
@@ -211,6 +212,12 @@ export class RemoteSessionBackend
211
212
  })
212
213
  this.emit('tabAdded', message.payload.sessionId, message.payload.tab)
213
214
  break
215
+ case 'tabMetadataUpdated':
216
+ this.emit('tabMetadataUpdated', message.payload.sessionId, message.payload.tabId, {
217
+ autoRenameStatus: message.payload.autoRenameStatus,
218
+ title: message.payload.title,
219
+ })
220
+ break
214
221
  case 'workspaceCreateRequested':
215
222
  this.emit(
216
223
  'workspaceCreateRequested',
@@ -411,6 +418,7 @@ export class RemoteSessionBackend
411
418
  rows: number
412
419
  cwd?: string
413
420
  worktreeId?: string
421
+ autoRenameCandidate?: boolean
414
422
  }): void {
415
423
  if (!this.attached) {
416
424
  logDebug('backend.remote.skipCreateBeforeAttach', { tabId: options.tabId })
@@ -436,6 +444,15 @@ export class RemoteSessionBackend
436
444
  )
437
445
  }
438
446
 
447
+ renameTab(tabId: string, title: string): void {
448
+ if (!this.attached || !this.daemonAdvertises(IPC_CAPABILITY_TAB_METADATA)) return
449
+ this.dispatchCommand(
450
+ { id: crypto.randomUUID(), payload: { tabId, title }, type: 'renameTab' },
451
+ 'renameTab',
452
+ tabId
453
+ )
454
+ }
455
+
439
456
  scrollViewport(tabId: string, deltaLines: number): void {
440
457
  if (!this.attached) {
441
458
  return
@@ -23,6 +23,11 @@ export interface SessionBackendEvents {
23
23
  * `tabRender` event lands.
24
24
  */
25
25
  tabAdded: [sessionId: string, tab: TabSession]
26
+ tabMetadataUpdated: [
27
+ sessionId: string,
28
+ tabId: string,
29
+ patch: { title?: string; autoRenameStatus?: 'eligible' | 'attempted' },
30
+ ]
26
31
  /**
27
32
  * v12 workspace-lifecycle events. Fired when a CLI issued
28
33
  * `createWorkspace` / `switchWorkspace` / `closeWorkspace` and the daemon
@@ -86,8 +91,10 @@ export interface SessionBackend extends EventEmitter<SessionBackendEvents> {
86
91
  * registry surfaces the right grouping in `listTabs` for headless
87
92
  * consumers (CLI control plane). */
88
93
  worktreeId?: string
94
+ autoRenameCandidate?: boolean
89
95
  }): void
90
96
  write(tabId: string, input: string): void
97
+ renameTab(tabId: string, title: string): void
91
98
  scrollViewport(tabId: string, deltaLines: number): void
92
99
  scrollViewportToBottom(tabId: string): void
93
100
  setActiveTab(tabId: string | null): void
@@ -543,7 +543,20 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
543
543
  case 'rename-tab':
544
544
  return {
545
545
  ...state,
546
- tabs: updateTab(state.tabs, action.tabId, (tab) => ({ ...tab, title: action.title })),
546
+ tabs: updateTab(state.tabs, action.tabId, (tab) => ({
547
+ ...tab,
548
+ autoRenameStatus: action.autoRenameStatus ?? tab.autoRenameStatus,
549
+ title: action.title,
550
+ })),
551
+ }
552
+ case 'update-tab-metadata':
553
+ return {
554
+ ...state,
555
+ tabs: updateTab(state.tabs, action.tabId, (tab) => ({
556
+ ...tab,
557
+ autoRenameStatus: action.autoRenameStatus ?? tab.autoRenameStatus,
558
+ title: action.title ?? tab.title,
559
+ })),
547
560
  }
548
561
  case 'split-pane': {
549
562
  if (!(state.activeTabId != null && state.activeTabId !== '')) {
@@ -49,6 +49,7 @@ export function serializeWorkspace(state: AppState): WorkspaceSnapshotV1 {
49
49
  tabGroupMap: Object.keys(state.tabGroupMap).length > 0 ? state.tabGroupMap : undefined,
50
50
  tabs: state.tabs.map((tab) => ({
51
51
  assistant: tab.assistant,
52
+ autoRenameStatus: tab.autoRenameStatus,
52
53
  buffer: tab.buffer,
53
54
  command: tab.command,
54
55
  errorMessage: tab.errorMessage,
@@ -113,6 +114,7 @@ export function restoreTabsFromWorkspace(
113
114
  .map((tab) => ({
114
115
  activity: 'idle',
115
116
  assistant: tab.assistant,
117
+ autoRenameStatus: tab.autoRenameStatus,
116
118
  buffer: tab.buffer,
117
119
  command: tab.command,
118
120
  errorMessage: tab.errorMessage,
@@ -133,6 +133,7 @@ export interface PersistedTabSnapshot {
133
133
  errorMessage?: string
134
134
  exitCode?: number
135
135
  worktreeId?: string
136
+ autoRenameStatus?: 'eligible' | 'attempted'
136
137
  }
137
138
 
138
139
  export interface WorkspaceSnapshotV1 {
@@ -203,6 +204,7 @@ export interface TabSession {
203
204
  errorMessage?: string
204
205
  exitCode?: number
205
206
  worktreeId?: string
207
+ autoRenameStatus?: 'eligible' | 'attempted'
206
208
  }
207
209
 
208
210
  export interface SidebarState {
@@ -747,7 +749,18 @@ export type TabAction =
747
749
  | { type: 'reorder-active-tab'; delta: number }
748
750
  | { type: 'reorder-tabs'; orderedTabIds: string[] }
749
751
  | { type: 'reset-tab-session'; tabId: string }
750
- | { type: 'rename-tab'; tabId: string; title: string }
752
+ | {
753
+ type: 'rename-tab'
754
+ tabId: string
755
+ title: string
756
+ autoRenameStatus?: 'eligible' | 'attempted'
757
+ }
758
+ | {
759
+ type: 'update-tab-metadata'
760
+ tabId: string
761
+ title?: string
762
+ autoRenameStatus?: 'eligible' | 'attempted'
763
+ }
751
764
  | { type: 'append-tab-buffer'; tabId: string; chunk: string }
752
765
  | {
753
766
  type: 'replace-tab-viewport'
@@ -152,7 +152,10 @@ export function isWorkspaceSnapshotV1(value: unknown): value is WorkspaceSnapsho
152
152
  (tab.viewport === undefined || isTerminalSnapshot(tab.viewport)) &&
153
153
  (tab.errorMessage === undefined || isString(tab.errorMessage)) &&
154
154
  (tab.exitCode === undefined || isFiniteNumber(tab.exitCode)) &&
155
- (tab.worktreeId === undefined || isString(tab.worktreeId))
155
+ (tab.worktreeId === undefined || isString(tab.worktreeId)) &&
156
+ (tab.autoRenameStatus === undefined ||
157
+ tab.autoRenameStatus === 'eligible' ||
158
+ tab.autoRenameStatus === 'attempted')
156
159
  ) &&
157
160
  (value.layoutTree === undefined || isLayoutNode(value.layoutTree)) &&
158
161
  (value.layoutTrees === undefined || isLayoutTreesMap(value.layoutTrees)) &&
@@ -8,6 +8,7 @@ import { logDebug } from '../debug/input-log'
8
8
  import {
9
9
  encodeManagerMessage,
10
10
  MANAGER_CAPABILITY_SET_BROADCAST_ENABLED,
11
+ MANAGER_CAPABILITY_TAB_METADATA,
11
12
  MANAGER_PROTOCOL_BROADCAST_GATE_VERSION,
12
13
  MANAGER_PROTOCOL_MIN_VERSION,
13
14
  MANAGER_PROTOCOL_VERSION,
@@ -295,7 +296,10 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
295
296
  tabId: options.tabId,
296
297
  title: options.title,
297
298
  })
298
- return this.sendExpectOk({ id: crypto.randomUUID(), payload: options, type: 'createTab' })
299
+ const payload = this.serverCapabilities.has(MANAGER_CAPABILITY_TAB_METADATA)
300
+ ? options
301
+ : (({ autoRenameStatus: _autoRenameStatus, ...legacy }) => legacy)(options)
302
+ return this.sendExpectOk({ id: crypto.randomUUID(), payload, type: 'createTab' })
299
303
  }
300
304
 
301
305
  async write(sessionId: string, tabId: string, data: string): Promise<void> {
@@ -306,6 +310,19 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
306
310
  })
307
311
  }
308
312
 
313
+ async updateTabMetadata(
314
+ sessionId: string,
315
+ tabId: string,
316
+ patch: { title?: string; autoRenameStatus?: 'eligible' | 'attempted' }
317
+ ): Promise<void> {
318
+ if (!this.serverCapabilities.has(MANAGER_CAPABILITY_TAB_METADATA)) return
319
+ return this.sendExpectOk({
320
+ id: crypto.randomUUID(),
321
+ payload: { ...patch, sessionId, tabId },
322
+ type: 'updateTabMetadata',
323
+ })
324
+ }
325
+
309
326
  async resize(sessionId: string, cols: number, rows: number): Promise<void> {
310
327
  return this.sendExpectOk({
311
328
  id: crypto.randomUUID(),
@@ -209,6 +209,15 @@ export async function runTerminalManager(): Promise<void> {
209
209
  )
210
210
  sendOk(socket, message.id)
211
211
  break
212
+ case 'updateTabMetadata':
213
+ requireNegotiatedVersion(socket, negotiatedVersions)
214
+ sessionManager.updateTabMetadata(
215
+ message.payload.sessionId,
216
+ message.payload.tabId,
217
+ message.payload
218
+ )
219
+ sendOk(socket, message.id)
220
+ break
212
221
  case 'resizeClient': {
213
222
  requireNegotiatedVersion(socket, negotiatedVersions)
214
223
  sessionManager.resize(