@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.
- package/package.json +2 -2
- package/src/app-runtime/backend-runtime-events.ts +16 -0
- package/src/app-runtime/side-effects.ts +15 -2
- package/src/auto-rename/coordinator.ts +97 -0
- package/src/auto-rename/prompt-capture.ts +211 -0
- package/src/auto-rename/title-runner.ts +96 -0
- package/src/cli/commands/tab/create.ts +1 -0
- package/src/daemon/daemon.ts +123 -12
- package/src/daemon/session-manager.ts +8 -0
- package/src/daemon/session-registry.ts +17 -1
- package/src/index.tsx +3 -1
- package/src/input/modes/types.ts +1 -0
- package/src/ipc/manager-protocol.ts +38 -1
- package/src/ipc/protocol.ts +49 -1
- package/src/session-backend/bootstrap.ts +3 -1
- package/src/session-backend/local-session-backend.ts +63 -2
- package/src/session-backend/remote-session-backend.ts +17 -0
- package/src/session-backend/types.ts +7 -0
- package/src/state/reducers/tab-state.ts +14 -1
- package/src/state/session-persistence.ts +2 -0
- package/src/state/types.ts +14 -1
- package/src/state/validation.ts +4 -1
- package/src/terminal-manager/manager-client.ts +18 -1
- package/src/terminal-manager/terminal-manager.ts +9 -0
package/src/daemon/daemon.ts
CHANGED
|
@@ -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?: {
|
|
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?: {
|
|
251
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
642
|
-
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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} />)
|
package/src/input/modes/types.ts
CHANGED
|
@@ -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 =
|
|
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')
|
package/src/ipc/protocol.ts
CHANGED
|
@@ -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 =
|
|
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()
|