yzcode-cli 1.0.1 → 1.0.3
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/assistant/sessionHistory.ts +87 -0
- package/bootstrap/state.ts +1769 -0
- package/bridge/bridgeApi.ts +539 -0
- package/bridge/bridgeConfig.ts +48 -0
- package/bridge/bridgeDebug.ts +135 -0
- package/bridge/bridgeEnabled.ts +202 -0
- package/bridge/bridgeMain.ts +2999 -0
- package/bridge/bridgeMessaging.ts +461 -0
- package/bridge/bridgePermissionCallbacks.ts +43 -0
- package/bridge/bridgePointer.ts +210 -0
- package/bridge/bridgeStatusUtil.ts +163 -0
- package/bridge/bridgeUI.ts +530 -0
- package/bridge/capacityWake.ts +56 -0
- package/bridge/codeSessionApi.ts +168 -0
- package/bridge/createSession.ts +384 -0
- package/bridge/debugUtils.ts +141 -0
- package/bridge/envLessBridgeConfig.ts +165 -0
- package/bridge/flushGate.ts +71 -0
- package/bridge/inboundAttachments.ts +175 -0
- package/bridge/inboundMessages.ts +80 -0
- package/bridge/initReplBridge.ts +569 -0
- package/bridge/jwtUtils.ts +256 -0
- package/bridge/pollConfig.ts +110 -0
- package/bridge/pollConfigDefaults.ts +82 -0
- package/bridge/remoteBridgeCore.ts +1008 -0
- package/bridge/replBridge.ts +2406 -0
- package/bridge/replBridgeHandle.ts +36 -0
- package/bridge/replBridgeTransport.ts +370 -0
- package/bridge/sessionIdCompat.ts +57 -0
- package/bridge/sessionRunner.ts +550 -0
- package/bridge/trustedDevice.ts +210 -0
- package/bridge/types.ts +262 -0
- package/bridge/workSecret.ts +127 -0
- package/buddy/CompanionSprite.tsx +371 -0
- package/buddy/companion.ts +133 -0
- package/buddy/prompt.ts +36 -0
- package/buddy/sprites.ts +514 -0
- package/buddy/types.ts +148 -0
- package/buddy/useBuddyNotification.tsx +98 -0
- package/coordinator/coordinatorMode.ts +369 -0
- package/memdir/findRelevantMemories.ts +141 -0
- package/memdir/memdir.ts +507 -0
- package/memdir/memoryAge.ts +53 -0
- package/memdir/memoryScan.ts +94 -0
- package/memdir/memoryTypes.ts +271 -0
- package/memdir/paths.ts +278 -0
- package/memdir/teamMemPaths.ts +292 -0
- package/memdir/teamMemPrompts.ts +100 -0
- package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
- package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
- package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
- package/migrations/migrateFennecToOpus.ts +45 -0
- package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
- package/migrations/migrateOpusToOpus1m.ts +43 -0
- package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
- package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
- package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
- package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
- package/migrations/resetProToOpusDefault.ts +51 -0
- package/native-ts/color-diff/index.ts +999 -0
- package/native-ts/file-index/index.ts +370 -0
- package/native-ts/yoga-layout/enums.ts +134 -0
- package/native-ts/yoga-layout/index.ts +2578 -0
- package/outputStyles/loadOutputStylesDir.ts +98 -0
- package/package.json +22 -5
- package/plugins/builtinPlugins.ts +159 -0
- package/plugins/bundled/index.ts +23 -0
- package/schemas/hooks.ts +222 -0
- package/screens/Doctor.tsx +575 -0
- package/screens/REPL.tsx +5006 -0
- package/screens/ResumeConversation.tsx +399 -0
- package/server/createDirectConnectSession.ts +88 -0
- package/server/directConnectManager.ts +213 -0
- package/server/types.ts +57 -0
- package/skills/bundled/batch.ts +124 -0
- package/skills/bundled/claudeApi.ts +196 -0
- package/skills/bundled/claudeApiContent.ts +75 -0
- package/skills/bundled/claudeInChrome.ts +34 -0
- package/skills/bundled/debug.ts +103 -0
- package/skills/bundled/index.ts +79 -0
- package/skills/bundled/keybindings.ts +339 -0
- package/skills/bundled/loop.ts +92 -0
- package/skills/bundled/loremIpsum.ts +282 -0
- package/skills/bundled/remember.ts +82 -0
- package/skills/bundled/scheduleRemoteAgents.ts +447 -0
- package/skills/bundled/simplify.ts +69 -0
- package/skills/bundled/skillify.ts +197 -0
- package/skills/bundled/stuck.ts +79 -0
- package/skills/bundled/updateConfig.ts +475 -0
- package/skills/bundled/verify/SKILL.md +3 -0
- package/skills/bundled/verify/examples/cli.md +3 -0
- package/skills/bundled/verify/examples/server.md +3 -0
- package/skills/bundled/verify.ts +30 -0
- package/skills/bundled/verifyContent.ts +13 -0
- package/skills/bundledSkills.ts +220 -0
- package/skills/loadSkillsDir.ts +1086 -0
- package/skills/mcpSkillBuilders.ts +44 -0
- package/tasks/DreamTask/DreamTask.ts +157 -0
- package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
- package/tasks/InProcessTeammateTask/types.ts +121 -0
- package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
- package/tasks/LocalMainSessionTask.ts +479 -0
- package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
- package/tasks/LocalShellTask/guards.ts +41 -0
- package/tasks/LocalShellTask/killShellTasks.ts +76 -0
- package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
- package/tasks/pillLabel.ts +82 -0
- package/tasks/stopTask.ts +100 -0
- package/tasks/types.ts +46 -0
- package/upstreamproxy/relay.ts +455 -0
- package/upstreamproxy/upstreamproxy.ts +285 -0
- package/vim/motions.ts +82 -0
- package/vim/operators.ts +556 -0
- package/vim/textObjects.ts +186 -0
- package/vim/transitions.ts +490 -0
- package/vim/types.ts +199 -0
- package/voice/voiceModeEnabled.ts +54 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { toString as qrToString } from 'qrcode'
|
|
3
|
+
import {
|
|
4
|
+
BRIDGE_FAILED_INDICATOR,
|
|
5
|
+
BRIDGE_READY_INDICATOR,
|
|
6
|
+
BRIDGE_SPINNER_FRAMES,
|
|
7
|
+
} from '../constants/figures.js'
|
|
8
|
+
import { stringWidth } from '../ink/stringWidth.js'
|
|
9
|
+
import { logForDebugging } from '../utils/debug.js'
|
|
10
|
+
import {
|
|
11
|
+
buildActiveFooterText,
|
|
12
|
+
buildBridgeConnectUrl,
|
|
13
|
+
buildBridgeSessionUrl,
|
|
14
|
+
buildIdleFooterText,
|
|
15
|
+
FAILED_FOOTER_TEXT,
|
|
16
|
+
formatDuration,
|
|
17
|
+
type StatusState,
|
|
18
|
+
TOOL_DISPLAY_EXPIRY_MS,
|
|
19
|
+
timestamp,
|
|
20
|
+
truncatePrompt,
|
|
21
|
+
wrapWithOsc8Link,
|
|
22
|
+
} from './bridgeStatusUtil.js'
|
|
23
|
+
import type {
|
|
24
|
+
BridgeConfig,
|
|
25
|
+
BridgeLogger,
|
|
26
|
+
SessionActivity,
|
|
27
|
+
SpawnMode,
|
|
28
|
+
} from './types.js'
|
|
29
|
+
|
|
30
|
+
const QR_OPTIONS = {
|
|
31
|
+
type: 'utf8' as const,
|
|
32
|
+
errorCorrectionLevel: 'L' as const,
|
|
33
|
+
small: true,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Generate a QR code and return its lines. */
|
|
37
|
+
async function generateQr(url: string): Promise<string[]> {
|
|
38
|
+
const qr = await qrToString(url, QR_OPTIONS)
|
|
39
|
+
return qr.split('\n').filter((line: string) => line.length > 0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createBridgeLogger(options: {
|
|
43
|
+
verbose: boolean
|
|
44
|
+
write?: (s: string) => void
|
|
45
|
+
}): BridgeLogger {
|
|
46
|
+
const write = options.write ?? ((s: string) => process.stdout.write(s))
|
|
47
|
+
const verbose = options.verbose
|
|
48
|
+
|
|
49
|
+
// Track how many status lines are currently displayed at the bottom
|
|
50
|
+
let statusLineCount = 0
|
|
51
|
+
|
|
52
|
+
// Status state machine
|
|
53
|
+
let currentState: StatusState = 'idle'
|
|
54
|
+
let currentStateText = 'Ready'
|
|
55
|
+
let repoName = ''
|
|
56
|
+
let branch = ''
|
|
57
|
+
let debugLogPath = ''
|
|
58
|
+
|
|
59
|
+
// Connect URL (built in printBanner with correct base for staging/prod)
|
|
60
|
+
let connectUrl = ''
|
|
61
|
+
let cachedIngressUrl = ''
|
|
62
|
+
let cachedEnvironmentId = ''
|
|
63
|
+
let activeSessionUrl: string | null = null
|
|
64
|
+
|
|
65
|
+
// QR code lines for the current URL
|
|
66
|
+
let qrLines: string[] = []
|
|
67
|
+
let qrVisible = false
|
|
68
|
+
|
|
69
|
+
// Tool activity for the second status line
|
|
70
|
+
let lastToolSummary: string | null = null
|
|
71
|
+
let lastToolTime = 0
|
|
72
|
+
|
|
73
|
+
// Session count indicator (shown when multi-session mode is enabled)
|
|
74
|
+
let sessionActive = 0
|
|
75
|
+
let sessionMax = 1
|
|
76
|
+
// Spawn mode shown in the session-count line + gates the `w` hint
|
|
77
|
+
let spawnModeDisplay: 'same-dir' | 'worktree' | null = null
|
|
78
|
+
let spawnMode: SpawnMode = 'single-session'
|
|
79
|
+
|
|
80
|
+
// Per-session display info for the multi-session bullet list (keyed by compat sessionId)
|
|
81
|
+
const sessionDisplayInfo = new Map<
|
|
82
|
+
string,
|
|
83
|
+
{ title?: string; url: string; activity?: SessionActivity }
|
|
84
|
+
>()
|
|
85
|
+
|
|
86
|
+
// Connecting spinner state
|
|
87
|
+
let connectingTimer: ReturnType<typeof setInterval> | null = null
|
|
88
|
+
let connectingTick = 0
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Count how many visual terminal rows a string occupies, accounting for
|
|
92
|
+
* line wrapping. Each `\n` is one row, and content wider than the terminal
|
|
93
|
+
* wraps to additional rows.
|
|
94
|
+
*/
|
|
95
|
+
function countVisualLines(text: string): number {
|
|
96
|
+
// eslint-disable-next-line custom-rules/prefer-use-terminal-size
|
|
97
|
+
const cols = process.stdout.columns || 80 // non-React CLI context
|
|
98
|
+
let count = 0
|
|
99
|
+
// Split on newlines to get logical lines
|
|
100
|
+
for (const logical of text.split('\n')) {
|
|
101
|
+
if (logical.length === 0) {
|
|
102
|
+
// Empty segment between consecutive \n — counts as 1 row
|
|
103
|
+
count++
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
const width = stringWidth(logical)
|
|
107
|
+
count += Math.max(1, Math.ceil(width / cols))
|
|
108
|
+
}
|
|
109
|
+
// The trailing \n in "line\n" produces an empty last element — don't count it
|
|
110
|
+
// because the cursor sits at the start of the next line, not a new visual row.
|
|
111
|
+
if (text.endsWith('\n')) {
|
|
112
|
+
count--
|
|
113
|
+
}
|
|
114
|
+
return count
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Write a status line and track its visual line count. */
|
|
118
|
+
function writeStatus(text: string): void {
|
|
119
|
+
write(text)
|
|
120
|
+
statusLineCount += countVisualLines(text)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Clear any currently displayed status lines. */
|
|
124
|
+
function clearStatusLines(): void {
|
|
125
|
+
if (statusLineCount <= 0) return
|
|
126
|
+
logForDebugging(`[bridge:ui] clearStatusLines count=${statusLineCount}`)
|
|
127
|
+
// Move cursor up to the start of the status block, then erase everything below
|
|
128
|
+
write(`\x1b[${statusLineCount}A`) // cursor up N lines
|
|
129
|
+
write('\x1b[J') // erase from cursor to end of screen
|
|
130
|
+
statusLineCount = 0
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Print a permanent log line, clearing status first and restoring after. */
|
|
134
|
+
function printLog(line: string): void {
|
|
135
|
+
clearStatusLines()
|
|
136
|
+
write(line)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Regenerate the QR code with the given URL. */
|
|
140
|
+
function regenerateQr(url: string): void {
|
|
141
|
+
generateQr(url)
|
|
142
|
+
.then(lines => {
|
|
143
|
+
qrLines = lines
|
|
144
|
+
renderStatusLine()
|
|
145
|
+
})
|
|
146
|
+
.catch(e => {
|
|
147
|
+
logForDebugging(`QR code generation failed: ${e}`, { level: 'error' })
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Render the connecting spinner line (shown before first updateIdleStatus). */
|
|
152
|
+
function renderConnectingLine(): void {
|
|
153
|
+
clearStatusLines()
|
|
154
|
+
|
|
155
|
+
const frame =
|
|
156
|
+
BRIDGE_SPINNER_FRAMES[connectingTick % BRIDGE_SPINNER_FRAMES.length]!
|
|
157
|
+
let suffix = ''
|
|
158
|
+
if (repoName) {
|
|
159
|
+
suffix += chalk.dim(' \u00b7 ') + chalk.dim(repoName)
|
|
160
|
+
}
|
|
161
|
+
if (branch) {
|
|
162
|
+
suffix += chalk.dim(' \u00b7 ') + chalk.dim(branch)
|
|
163
|
+
}
|
|
164
|
+
writeStatus(
|
|
165
|
+
`${chalk.yellow(frame)} ${chalk.yellow('Connecting')}${suffix}\n`,
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Start the connecting spinner. Stopped by first updateIdleStatus(). */
|
|
170
|
+
function startConnecting(): void {
|
|
171
|
+
stopConnecting()
|
|
172
|
+
renderConnectingLine()
|
|
173
|
+
connectingTimer = setInterval(() => {
|
|
174
|
+
connectingTick++
|
|
175
|
+
renderConnectingLine()
|
|
176
|
+
}, 150)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Stop the connecting spinner. */
|
|
180
|
+
function stopConnecting(): void {
|
|
181
|
+
if (connectingTimer) {
|
|
182
|
+
clearInterval(connectingTimer)
|
|
183
|
+
connectingTimer = null
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Render and write the current status lines based on state. */
|
|
188
|
+
function renderStatusLine(): void {
|
|
189
|
+
if (currentState === 'reconnecting' || currentState === 'failed') {
|
|
190
|
+
// These states are handled separately (updateReconnectingStatus /
|
|
191
|
+
// updateFailedStatus). Return before clearing so callers like toggleQr
|
|
192
|
+
// and setSpawnModeDisplay don't blank the display during these states.
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
clearStatusLines()
|
|
197
|
+
|
|
198
|
+
const isIdle = currentState === 'idle'
|
|
199
|
+
|
|
200
|
+
// QR code above the status line
|
|
201
|
+
if (qrVisible) {
|
|
202
|
+
for (const line of qrLines) {
|
|
203
|
+
writeStatus(`${chalk.dim(line)}\n`)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Determine indicator and colors based on state
|
|
208
|
+
const indicator = BRIDGE_READY_INDICATOR
|
|
209
|
+
const indicatorColor = isIdle ? chalk.green : chalk.cyan
|
|
210
|
+
const baseColor = isIdle ? chalk.green : chalk.cyan
|
|
211
|
+
const stateText = baseColor(currentStateText)
|
|
212
|
+
|
|
213
|
+
// Build the suffix with repo and branch
|
|
214
|
+
let suffix = ''
|
|
215
|
+
if (repoName) {
|
|
216
|
+
suffix += chalk.dim(' \u00b7 ') + chalk.dim(repoName)
|
|
217
|
+
}
|
|
218
|
+
// In worktree mode each session gets its own branch, so showing the
|
|
219
|
+
// bridge's branch would be misleading.
|
|
220
|
+
if (branch && spawnMode !== 'worktree') {
|
|
221
|
+
suffix += chalk.dim(' \u00b7 ') + chalk.dim(branch)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (process.env.USER_TYPE === 'ant' && debugLogPath) {
|
|
225
|
+
writeStatus(
|
|
226
|
+
`${chalk.yellow('[ANT-ONLY] Logs:')} ${chalk.dim(debugLogPath)}\n`,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
writeStatus(`${indicatorColor(indicator)} ${stateText}${suffix}\n`)
|
|
230
|
+
|
|
231
|
+
// Session count and per-session list (multi-session mode only)
|
|
232
|
+
if (sessionMax > 1) {
|
|
233
|
+
const modeHint =
|
|
234
|
+
spawnMode === 'worktree'
|
|
235
|
+
? 'New sessions will be created in an isolated worktree'
|
|
236
|
+
: 'New sessions will be created in the current directory'
|
|
237
|
+
writeStatus(
|
|
238
|
+
` ${chalk.dim(`Capacity: ${sessionActive}/${sessionMax} \u00b7 ${modeHint}`)}\n`,
|
|
239
|
+
)
|
|
240
|
+
for (const [, info] of sessionDisplayInfo) {
|
|
241
|
+
const titleText = info.title
|
|
242
|
+
? truncatePrompt(info.title, 35)
|
|
243
|
+
: chalk.dim('Attached')
|
|
244
|
+
const titleLinked = wrapWithOsc8Link(titleText, info.url)
|
|
245
|
+
const act = info.activity
|
|
246
|
+
const showAct = act && act.type !== 'result' && act.type !== 'error'
|
|
247
|
+
const actText = showAct
|
|
248
|
+
? chalk.dim(` ${truncatePrompt(act.summary, 40)}`)
|
|
249
|
+
: ''
|
|
250
|
+
writeStatus(` ${titleLinked}${actText}
|
|
251
|
+
`)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Mode line for spawn modes with a single slot (or true single-session mode)
|
|
256
|
+
if (sessionMax === 1) {
|
|
257
|
+
const modeText =
|
|
258
|
+
spawnMode === 'single-session'
|
|
259
|
+
? 'Single session \u00b7 exits when complete'
|
|
260
|
+
: spawnMode === 'worktree'
|
|
261
|
+
? `Capacity: ${sessionActive}/1 \u00b7 New sessions will be created in an isolated worktree`
|
|
262
|
+
: `Capacity: ${sessionActive}/1 \u00b7 New sessions will be created in the current directory`
|
|
263
|
+
writeStatus(` ${chalk.dim(modeText)}\n`)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Tool activity line for single-session mode
|
|
267
|
+
if (
|
|
268
|
+
sessionMax === 1 &&
|
|
269
|
+
!isIdle &&
|
|
270
|
+
lastToolSummary &&
|
|
271
|
+
Date.now() - lastToolTime < TOOL_DISPLAY_EXPIRY_MS
|
|
272
|
+
) {
|
|
273
|
+
writeStatus(` ${chalk.dim(truncatePrompt(lastToolSummary, 60))}\n`)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Blank line separator before footer
|
|
277
|
+
const url = activeSessionUrl ?? connectUrl
|
|
278
|
+
if (url) {
|
|
279
|
+
writeStatus('\n')
|
|
280
|
+
const footerText = isIdle
|
|
281
|
+
? buildIdleFooterText(url)
|
|
282
|
+
: buildActiveFooterText(url)
|
|
283
|
+
const qrHint = qrVisible
|
|
284
|
+
? chalk.dim.italic('space to hide QR code')
|
|
285
|
+
: chalk.dim.italic('space to show QR code')
|
|
286
|
+
const toggleHint = spawnModeDisplay
|
|
287
|
+
? chalk.dim.italic(' \u00b7 w to toggle spawn mode')
|
|
288
|
+
: ''
|
|
289
|
+
writeStatus(`${chalk.dim(footerText)}\n`)
|
|
290
|
+
writeStatus(`${qrHint}${toggleHint}\n`)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
printBanner(config: BridgeConfig, environmentId: string): void {
|
|
296
|
+
cachedIngressUrl = config.sessionIngressUrl
|
|
297
|
+
cachedEnvironmentId = environmentId
|
|
298
|
+
connectUrl = buildBridgeConnectUrl(environmentId, cachedIngressUrl)
|
|
299
|
+
regenerateQr(connectUrl)
|
|
300
|
+
|
|
301
|
+
if (verbose) {
|
|
302
|
+
write(chalk.dim(`Remote Control`) + ` v${MACRO.VERSION}\n`)
|
|
303
|
+
}
|
|
304
|
+
if (verbose) {
|
|
305
|
+
if (config.spawnMode !== 'single-session') {
|
|
306
|
+
write(chalk.dim(`Spawn mode: `) + `${config.spawnMode}\n`)
|
|
307
|
+
write(
|
|
308
|
+
chalk.dim(`Max concurrent sessions: `) + `${config.maxSessions}\n`,
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
write(chalk.dim(`Environment ID: `) + `${environmentId}\n`)
|
|
312
|
+
}
|
|
313
|
+
if (config.sandbox) {
|
|
314
|
+
write(chalk.dim(`Sandbox: `) + `${chalk.green('Enabled')}\n`)
|
|
315
|
+
}
|
|
316
|
+
write('\n')
|
|
317
|
+
|
|
318
|
+
// Start connecting spinner — first updateIdleStatus() will stop it
|
|
319
|
+
startConnecting()
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
logSessionStart(sessionId: string, prompt: string): void {
|
|
323
|
+
if (verbose) {
|
|
324
|
+
const short = truncatePrompt(prompt, 80)
|
|
325
|
+
printLog(
|
|
326
|
+
chalk.dim(`[${timestamp()}]`) +
|
|
327
|
+
` Session started: ${chalk.white(`"${short}"`)} (${chalk.dim(sessionId)})\n`,
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
logSessionComplete(sessionId: string, durationMs: number): void {
|
|
333
|
+
printLog(
|
|
334
|
+
chalk.dim(`[${timestamp()}]`) +
|
|
335
|
+
` Session ${chalk.green('completed')} (${formatDuration(durationMs)}) ${chalk.dim(sessionId)}\n`,
|
|
336
|
+
)
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
logSessionFailed(sessionId: string, error: string): void {
|
|
340
|
+
printLog(
|
|
341
|
+
chalk.dim(`[${timestamp()}]`) +
|
|
342
|
+
` Session ${chalk.red('failed')}: ${error} ${chalk.dim(sessionId)}\n`,
|
|
343
|
+
)
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
logStatus(message: string): void {
|
|
347
|
+
printLog(chalk.dim(`[${timestamp()}]`) + ` ${message}\n`)
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
logVerbose(message: string): void {
|
|
351
|
+
if (verbose) {
|
|
352
|
+
printLog(chalk.dim(`[${timestamp()}] ${message}`) + '\n')
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
logError(message: string): void {
|
|
357
|
+
printLog(chalk.red(`[${timestamp()}] Error: ${message}`) + '\n')
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
logReconnected(disconnectedMs: number): void {
|
|
361
|
+
printLog(
|
|
362
|
+
chalk.dim(`[${timestamp()}]`) +
|
|
363
|
+
` ${chalk.green('Reconnected')} after ${formatDuration(disconnectedMs)}\n`,
|
|
364
|
+
)
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
setRepoInfo(repo: string, branchName: string): void {
|
|
368
|
+
repoName = repo
|
|
369
|
+
branch = branchName
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
setDebugLogPath(path: string): void {
|
|
373
|
+
debugLogPath = path
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
updateIdleStatus(): void {
|
|
377
|
+
stopConnecting()
|
|
378
|
+
|
|
379
|
+
currentState = 'idle'
|
|
380
|
+
currentStateText = 'Ready'
|
|
381
|
+
lastToolSummary = null
|
|
382
|
+
lastToolTime = 0
|
|
383
|
+
activeSessionUrl = null
|
|
384
|
+
regenerateQr(connectUrl)
|
|
385
|
+
renderStatusLine()
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
setAttached(sessionId: string): void {
|
|
389
|
+
stopConnecting()
|
|
390
|
+
currentState = 'attached'
|
|
391
|
+
currentStateText = 'Connected'
|
|
392
|
+
lastToolSummary = null
|
|
393
|
+
lastToolTime = 0
|
|
394
|
+
// Multi-session: keep footer/QR on the environment connect URL so users
|
|
395
|
+
// can spawn more sessions. Per-session links are in the bullet list.
|
|
396
|
+
if (sessionMax <= 1) {
|
|
397
|
+
activeSessionUrl = buildBridgeSessionUrl(
|
|
398
|
+
sessionId,
|
|
399
|
+
cachedEnvironmentId,
|
|
400
|
+
cachedIngressUrl,
|
|
401
|
+
)
|
|
402
|
+
regenerateQr(activeSessionUrl)
|
|
403
|
+
}
|
|
404
|
+
renderStatusLine()
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
updateReconnectingStatus(delayStr: string, elapsedStr: string): void {
|
|
408
|
+
stopConnecting()
|
|
409
|
+
clearStatusLines()
|
|
410
|
+
currentState = 'reconnecting'
|
|
411
|
+
|
|
412
|
+
// QR code above the status line
|
|
413
|
+
if (qrVisible) {
|
|
414
|
+
for (const line of qrLines) {
|
|
415
|
+
writeStatus(`${chalk.dim(line)}\n`)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const frame =
|
|
420
|
+
BRIDGE_SPINNER_FRAMES[connectingTick % BRIDGE_SPINNER_FRAMES.length]!
|
|
421
|
+
connectingTick++
|
|
422
|
+
writeStatus(
|
|
423
|
+
`${chalk.yellow(frame)} ${chalk.yellow('Reconnecting')} ${chalk.dim('\u00b7')} ${chalk.dim(`retrying in ${delayStr}`)} ${chalk.dim('\u00b7')} ${chalk.dim(`disconnected ${elapsedStr}`)}\n`,
|
|
424
|
+
)
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
updateFailedStatus(error: string): void {
|
|
428
|
+
stopConnecting()
|
|
429
|
+
clearStatusLines()
|
|
430
|
+
currentState = 'failed'
|
|
431
|
+
|
|
432
|
+
let suffix = ''
|
|
433
|
+
if (repoName) {
|
|
434
|
+
suffix += chalk.dim(' \u00b7 ') + chalk.dim(repoName)
|
|
435
|
+
}
|
|
436
|
+
if (branch) {
|
|
437
|
+
suffix += chalk.dim(' \u00b7 ') + chalk.dim(branch)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
writeStatus(
|
|
441
|
+
`${chalk.red(BRIDGE_FAILED_INDICATOR)} ${chalk.red('Remote Control Failed')}${suffix}\n`,
|
|
442
|
+
)
|
|
443
|
+
writeStatus(`${chalk.dim(FAILED_FOOTER_TEXT)}\n`)
|
|
444
|
+
|
|
445
|
+
if (error) {
|
|
446
|
+
writeStatus(`${chalk.red(error)}\n`)
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
updateSessionStatus(
|
|
451
|
+
_sessionId: string,
|
|
452
|
+
_elapsed: string,
|
|
453
|
+
activity: SessionActivity,
|
|
454
|
+
_trail: string[],
|
|
455
|
+
): void {
|
|
456
|
+
// Cache tool activity for the second status line
|
|
457
|
+
if (activity.type === 'tool_start') {
|
|
458
|
+
lastToolSummary = activity.summary
|
|
459
|
+
lastToolTime = Date.now()
|
|
460
|
+
}
|
|
461
|
+
renderStatusLine()
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
clearStatus(): void {
|
|
465
|
+
stopConnecting()
|
|
466
|
+
clearStatusLines()
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
toggleQr(): void {
|
|
470
|
+
qrVisible = !qrVisible
|
|
471
|
+
renderStatusLine()
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
updateSessionCount(active: number, max: number, mode: SpawnMode): void {
|
|
475
|
+
if (sessionActive === active && sessionMax === max && spawnMode === mode)
|
|
476
|
+
return
|
|
477
|
+
sessionActive = active
|
|
478
|
+
sessionMax = max
|
|
479
|
+
spawnMode = mode
|
|
480
|
+
// Don't re-render here — the status ticker calls renderStatusLine
|
|
481
|
+
// on its own cadence, and the next tick will pick up the new values.
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
setSpawnModeDisplay(mode: 'same-dir' | 'worktree' | null): void {
|
|
485
|
+
if (spawnModeDisplay === mode) return
|
|
486
|
+
spawnModeDisplay = mode
|
|
487
|
+
// Also sync the #21118-added spawnMode so the next render shows correct
|
|
488
|
+
// mode hint + branch visibility. Don't render here — matches
|
|
489
|
+
// updateSessionCount: called before printBanner (initial setup) and
|
|
490
|
+
// again from the `w` handler (which follows with refreshDisplay).
|
|
491
|
+
if (mode) spawnMode = mode
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
addSession(sessionId: string, url: string): void {
|
|
495
|
+
sessionDisplayInfo.set(sessionId, { url })
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
updateSessionActivity(sessionId: string, activity: SessionActivity): void {
|
|
499
|
+
const info = sessionDisplayInfo.get(sessionId)
|
|
500
|
+
if (!info) return
|
|
501
|
+
info.activity = activity
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
setSessionTitle(sessionId: string, title: string): void {
|
|
505
|
+
const info = sessionDisplayInfo.get(sessionId)
|
|
506
|
+
if (!info) return
|
|
507
|
+
info.title = title
|
|
508
|
+
// Guard against reconnecting/failed — renderStatusLine clears then returns
|
|
509
|
+
// early for those states, which would erase the spinner/error.
|
|
510
|
+
if (currentState === 'reconnecting' || currentState === 'failed') return
|
|
511
|
+
if (sessionMax === 1) {
|
|
512
|
+
// Single-session: show title in the main status line too.
|
|
513
|
+
currentState = 'titled'
|
|
514
|
+
currentStateText = truncatePrompt(title, 40)
|
|
515
|
+
}
|
|
516
|
+
renderStatusLine()
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
removeSession(sessionId: string): void {
|
|
520
|
+
sessionDisplayInfo.delete(sessionId)
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
refreshDisplay(): void {
|
|
524
|
+
// Skip during reconnecting/failed — renderStatusLine clears then returns
|
|
525
|
+
// early for those states, which would erase the spinner/error.
|
|
526
|
+
if (currentState === 'reconnecting' || currentState === 'failed') return
|
|
527
|
+
renderStatusLine()
|
|
528
|
+
},
|
|
529
|
+
}
|
|
530
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared capacity-wake primitive for bridge poll loops.
|
|
3
|
+
*
|
|
4
|
+
* Both replBridge.ts and bridgeMain.ts need to sleep while "at capacity"
|
|
5
|
+
* but wake early when either (a) the outer loop signal aborts (shutdown),
|
|
6
|
+
* or (b) capacity frees up (session done / transport lost). This module
|
|
7
|
+
* encapsulates the mutable wake-controller + two-signal merger that both
|
|
8
|
+
* poll loops previously duplicated byte-for-byte.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type CapacitySignal = { signal: AbortSignal; cleanup: () => void }
|
|
12
|
+
|
|
13
|
+
export type CapacityWake = {
|
|
14
|
+
/**
|
|
15
|
+
* Create a signal that aborts when either the outer loop signal or the
|
|
16
|
+
* capacity-wake controller fires. Returns the merged signal and a cleanup
|
|
17
|
+
* function that removes listeners when the sleep resolves normally
|
|
18
|
+
* (without abort).
|
|
19
|
+
*/
|
|
20
|
+
signal(): CapacitySignal
|
|
21
|
+
/**
|
|
22
|
+
* Abort the current at-capacity sleep and arm a fresh controller so the
|
|
23
|
+
* poll loop immediately re-checks for new work.
|
|
24
|
+
*/
|
|
25
|
+
wake(): void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createCapacityWake(outerSignal: AbortSignal): CapacityWake {
|
|
29
|
+
let wakeController = new AbortController()
|
|
30
|
+
|
|
31
|
+
function wake(): void {
|
|
32
|
+
wakeController.abort()
|
|
33
|
+
wakeController = new AbortController()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function signal(): CapacitySignal {
|
|
37
|
+
const merged = new AbortController()
|
|
38
|
+
const abort = (): void => merged.abort()
|
|
39
|
+
if (outerSignal.aborted || wakeController.signal.aborted) {
|
|
40
|
+
merged.abort()
|
|
41
|
+
return { signal: merged.signal, cleanup: () => {} }
|
|
42
|
+
}
|
|
43
|
+
outerSignal.addEventListener('abort', abort, { once: true })
|
|
44
|
+
const capSig = wakeController.signal
|
|
45
|
+
capSig.addEventListener('abort', abort, { once: true })
|
|
46
|
+
return {
|
|
47
|
+
signal: merged.signal,
|
|
48
|
+
cleanup: () => {
|
|
49
|
+
outerSignal.removeEventListener('abort', abort)
|
|
50
|
+
capSig.removeEventListener('abort', abort)
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { signal, wake }
|
|
56
|
+
}
|