yzcode-cli 1.0.2 → 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 +19 -2
- 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,210 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises'
|
|
2
|
+
import { dirname, join } from 'path'
|
|
3
|
+
import { z } from 'zod/v4'
|
|
4
|
+
import { logForDebugging } from '../utils/debug.js'
|
|
5
|
+
import { isENOENT } from '../utils/errors.js'
|
|
6
|
+
import { getWorktreePathsPortable } from '../utils/getWorktreePathsPortable.js'
|
|
7
|
+
import { lazySchema } from '../utils/lazySchema.js'
|
|
8
|
+
import {
|
|
9
|
+
getProjectsDir,
|
|
10
|
+
sanitizePath,
|
|
11
|
+
} from '../utils/sessionStoragePortable.js'
|
|
12
|
+
import { jsonParse, jsonStringify } from '../utils/slowOperations.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Upper bound on worktree fanout. git worktree list is naturally bounded
|
|
16
|
+
* (50 is a LOT), but this caps the parallel stat() burst and guards against
|
|
17
|
+
* pathological setups. Above this, --continue falls back to current-dir-only.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_WORKTREE_FANOUT = 50
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Crash-recovery pointer for Remote Control sessions.
|
|
23
|
+
*
|
|
24
|
+
* Written immediately after a bridge session is created, periodically
|
|
25
|
+
* refreshed during the session, and cleared on clean shutdown. If the
|
|
26
|
+
* process dies unclean (crash, kill -9, terminal closed), the pointer
|
|
27
|
+
* persists. On next startup, `claude remote-control` detects it and offers
|
|
28
|
+
* to resume via the --session-id flow from #20460.
|
|
29
|
+
*
|
|
30
|
+
* Staleness is checked against the file's mtime (not an embedded timestamp)
|
|
31
|
+
* so that a periodic re-write with the same content serves as a refresh —
|
|
32
|
+
* matches the backend's rolling BRIDGE_LAST_POLL_TTL (4h) semantics. A
|
|
33
|
+
* bridge that's been polling for 5+ hours and then crashes still has a
|
|
34
|
+
* fresh pointer as long as the refresh ran within the window.
|
|
35
|
+
*
|
|
36
|
+
* Scoped per working directory (alongside transcript JSONL files) so two
|
|
37
|
+
* concurrent bridges in different repos don't clobber each other.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
export const BRIDGE_POINTER_TTL_MS = 4 * 60 * 60 * 1000
|
|
41
|
+
|
|
42
|
+
const BridgePointerSchema = lazySchema(() =>
|
|
43
|
+
z.object({
|
|
44
|
+
sessionId: z.string(),
|
|
45
|
+
environmentId: z.string(),
|
|
46
|
+
source: z.enum(['standalone', 'repl']),
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
export type BridgePointer = z.infer<ReturnType<typeof BridgePointerSchema>>
|
|
51
|
+
|
|
52
|
+
export function getBridgePointerPath(dir: string): string {
|
|
53
|
+
return join(getProjectsDir(), sanitizePath(dir), 'bridge-pointer.json')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Write the pointer. Also used to refresh mtime during long sessions —
|
|
58
|
+
* calling with the same IDs is a cheap no-content-change write that bumps
|
|
59
|
+
* the staleness clock. Best-effort — a crash-recovery file must never
|
|
60
|
+
* itself cause a crash. Logs and swallows on error.
|
|
61
|
+
*/
|
|
62
|
+
export async function writeBridgePointer(
|
|
63
|
+
dir: string,
|
|
64
|
+
pointer: BridgePointer,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
const path = getBridgePointerPath(dir)
|
|
67
|
+
try {
|
|
68
|
+
await mkdir(dirname(path), { recursive: true })
|
|
69
|
+
await writeFile(path, jsonStringify(pointer), 'utf8')
|
|
70
|
+
logForDebugging(`[bridge:pointer] wrote ${path}`)
|
|
71
|
+
} catch (err: unknown) {
|
|
72
|
+
logForDebugging(`[bridge:pointer] write failed: ${err}`, { level: 'warn' })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read the pointer and its age (ms since last write). Operates directly
|
|
78
|
+
* and handles errors — no existence check (CLAUDE.md TOCTOU rule). Returns
|
|
79
|
+
* null on any failure: missing file, corrupted JSON, schema mismatch, or
|
|
80
|
+
* stale (mtime > 4h ago). Stale/invalid pointers are deleted so they don't
|
|
81
|
+
* keep re-prompting after the backend has already GC'd the env.
|
|
82
|
+
*/
|
|
83
|
+
export async function readBridgePointer(
|
|
84
|
+
dir: string,
|
|
85
|
+
): Promise<(BridgePointer & { ageMs: number }) | null> {
|
|
86
|
+
const path = getBridgePointerPath(dir)
|
|
87
|
+
let raw: string
|
|
88
|
+
let mtimeMs: number
|
|
89
|
+
try {
|
|
90
|
+
// stat for mtime (staleness anchor), then read. Two syscalls, but both
|
|
91
|
+
// are needed — mtime IS the data we return, not a TOCTOU guard.
|
|
92
|
+
mtimeMs = (await stat(path)).mtimeMs
|
|
93
|
+
raw = await readFile(path, 'utf8')
|
|
94
|
+
} catch {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parsed = BridgePointerSchema().safeParse(safeJsonParse(raw))
|
|
99
|
+
if (!parsed.success) {
|
|
100
|
+
logForDebugging(`[bridge:pointer] invalid schema, clearing: ${path}`)
|
|
101
|
+
await clearBridgePointer(dir)
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const ageMs = Math.max(0, Date.now() - mtimeMs)
|
|
106
|
+
if (ageMs > BRIDGE_POINTER_TTL_MS) {
|
|
107
|
+
logForDebugging(`[bridge:pointer] stale (>4h mtime), clearing: ${path}`)
|
|
108
|
+
await clearBridgePointer(dir)
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { ...parsed.data, ageMs }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Worktree-aware read for `--continue`. The REPL bridge writes its pointer
|
|
117
|
+
* to `getOriginalCwd()` which EnterWorktreeTool/activeWorktreeSession can
|
|
118
|
+
* mutate to a worktree path — but `claude remote-control --continue` runs
|
|
119
|
+
* with `resolve('.')` = shell CWD. This fans out across git worktree
|
|
120
|
+
* siblings to find the freshest pointer, matching /resume's semantics.
|
|
121
|
+
*
|
|
122
|
+
* Fast path: checks `dir` first. Only shells out to `git worktree list` if
|
|
123
|
+
* that misses — the common case (pointer in launch dir) is one stat, zero
|
|
124
|
+
* exec. Fanout reads run in parallel; capped at MAX_WORKTREE_FANOUT.
|
|
125
|
+
*
|
|
126
|
+
* Returns the pointer AND the dir it was found in, so the caller can clear
|
|
127
|
+
* the right file on resume failure.
|
|
128
|
+
*/
|
|
129
|
+
export async function readBridgePointerAcrossWorktrees(
|
|
130
|
+
dir: string,
|
|
131
|
+
): Promise<{ pointer: BridgePointer & { ageMs: number }; dir: string } | null> {
|
|
132
|
+
// Fast path: current dir. Covers standalone bridge (always matches) and
|
|
133
|
+
// REPL bridge when no worktree mutation happened.
|
|
134
|
+
const here = await readBridgePointer(dir)
|
|
135
|
+
if (here) {
|
|
136
|
+
return { pointer: here, dir }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Fanout: scan worktree siblings. getWorktreePathsPortable has a 5s
|
|
140
|
+
// timeout and returns [] on any error (not a git repo, git not installed).
|
|
141
|
+
const worktrees = await getWorktreePathsPortable(dir)
|
|
142
|
+
if (worktrees.length <= 1) return null
|
|
143
|
+
if (worktrees.length > MAX_WORKTREE_FANOUT) {
|
|
144
|
+
logForDebugging(
|
|
145
|
+
`[bridge:pointer] ${worktrees.length} worktrees exceeds fanout cap ${MAX_WORKTREE_FANOUT}, skipping`,
|
|
146
|
+
)
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Dedupe against `dir` so we don't re-stat it. sanitizePath normalizes
|
|
151
|
+
// case/separators so worktree-list output matches our fast-path key even
|
|
152
|
+
// on Windows where git may emit C:/ vs stored c:/.
|
|
153
|
+
const dirKey = sanitizePath(dir)
|
|
154
|
+
const candidates = worktrees.filter(wt => sanitizePath(wt) !== dirKey)
|
|
155
|
+
|
|
156
|
+
// Parallel stat+read. Each readBridgePointer is a stat() that ENOENTs
|
|
157
|
+
// for worktrees with no pointer (cheap) plus a ~100-byte read for the
|
|
158
|
+
// rare ones that have one. Promise.all → latency ≈ slowest single stat.
|
|
159
|
+
const results = await Promise.all(
|
|
160
|
+
candidates.map(async wt => {
|
|
161
|
+
const p = await readBridgePointer(wt)
|
|
162
|
+
return p ? { pointer: p, dir: wt } : null
|
|
163
|
+
}),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// Pick freshest (lowest ageMs). The pointer stores environmentId so
|
|
167
|
+
// resume reconnects to the right env regardless of which worktree
|
|
168
|
+
// --continue was invoked from.
|
|
169
|
+
let freshest: {
|
|
170
|
+
pointer: BridgePointer & { ageMs: number }
|
|
171
|
+
dir: string
|
|
172
|
+
} | null = null
|
|
173
|
+
for (const r of results) {
|
|
174
|
+
if (r && (!freshest || r.pointer.ageMs < freshest.pointer.ageMs)) {
|
|
175
|
+
freshest = r
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (freshest) {
|
|
179
|
+
logForDebugging(
|
|
180
|
+
`[bridge:pointer] fanout found pointer in worktree ${freshest.dir} (ageMs=${freshest.pointer.ageMs})`,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
return freshest
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Delete the pointer. Idempotent — ENOENT is expected when the process
|
|
188
|
+
* shut down clean previously.
|
|
189
|
+
*/
|
|
190
|
+
export async function clearBridgePointer(dir: string): Promise<void> {
|
|
191
|
+
const path = getBridgePointerPath(dir)
|
|
192
|
+
try {
|
|
193
|
+
await unlink(path)
|
|
194
|
+
logForDebugging(`[bridge:pointer] cleared ${path}`)
|
|
195
|
+
} catch (err: unknown) {
|
|
196
|
+
if (!isENOENT(err)) {
|
|
197
|
+
logForDebugging(`[bridge:pointer] clear failed: ${err}`, {
|
|
198
|
+
level: 'warn',
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function safeJsonParse(raw: string): unknown {
|
|
205
|
+
try {
|
|
206
|
+
return jsonParse(raw)
|
|
207
|
+
} catch {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getClaudeAiBaseUrl,
|
|
3
|
+
getRemoteSessionUrl,
|
|
4
|
+
} from '../constants/product.js'
|
|
5
|
+
import { stringWidth } from '../ink/stringWidth.js'
|
|
6
|
+
import { formatDuration, truncateToWidth } from '../utils/format.js'
|
|
7
|
+
import { getGraphemeSegmenter } from '../utils/intl.js'
|
|
8
|
+
|
|
9
|
+
/** Bridge status state machine states. */
|
|
10
|
+
export type StatusState =
|
|
11
|
+
| 'idle'
|
|
12
|
+
| 'attached'
|
|
13
|
+
| 'titled'
|
|
14
|
+
| 'reconnecting'
|
|
15
|
+
| 'failed'
|
|
16
|
+
|
|
17
|
+
/** How long a tool activity line stays visible after last tool_start (ms). */
|
|
18
|
+
export const TOOL_DISPLAY_EXPIRY_MS = 30_000
|
|
19
|
+
|
|
20
|
+
/** Interval for the shimmer animation tick (ms). */
|
|
21
|
+
export const SHIMMER_INTERVAL_MS = 150
|
|
22
|
+
|
|
23
|
+
export function timestamp(): string {
|
|
24
|
+
const now = new Date()
|
|
25
|
+
const h = String(now.getHours()).padStart(2, '0')
|
|
26
|
+
const m = String(now.getMinutes()).padStart(2, '0')
|
|
27
|
+
const s = String(now.getSeconds()).padStart(2, '0')
|
|
28
|
+
return `${h}:${m}:${s}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { formatDuration, truncateToWidth as truncatePrompt }
|
|
32
|
+
|
|
33
|
+
/** Abbreviate a tool activity summary for the trail display. */
|
|
34
|
+
export function abbreviateActivity(summary: string): string {
|
|
35
|
+
return truncateToWidth(summary, 30)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Build the connect URL shown when the bridge is idle. */
|
|
39
|
+
export function buildBridgeConnectUrl(
|
|
40
|
+
environmentId: string,
|
|
41
|
+
ingressUrl?: string,
|
|
42
|
+
): string {
|
|
43
|
+
const baseUrl = getClaudeAiBaseUrl(undefined, ingressUrl)
|
|
44
|
+
return `${baseUrl}/code?bridge=${environmentId}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build the session URL shown when a session is attached. Delegates to
|
|
49
|
+
* getRemoteSessionUrl for the cse_→session_ prefix translation, then appends
|
|
50
|
+
* the v1-specific ?bridge={environmentId} query.
|
|
51
|
+
*/
|
|
52
|
+
export function buildBridgeSessionUrl(
|
|
53
|
+
sessionId: string,
|
|
54
|
+
environmentId: string,
|
|
55
|
+
ingressUrl?: string,
|
|
56
|
+
): string {
|
|
57
|
+
return `${getRemoteSessionUrl(sessionId, ingressUrl)}?bridge=${environmentId}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Compute the glimmer index for a reverse-sweep shimmer animation. */
|
|
61
|
+
export function computeGlimmerIndex(
|
|
62
|
+
tick: number,
|
|
63
|
+
messageWidth: number,
|
|
64
|
+
): number {
|
|
65
|
+
const cycleLength = messageWidth + 20
|
|
66
|
+
return messageWidth + 10 - (tick % cycleLength)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Split text into three segments by visual column position for shimmer rendering.
|
|
71
|
+
*
|
|
72
|
+
* Uses grapheme segmentation and `stringWidth` so the split is correct for
|
|
73
|
+
* multi-byte characters, emoji, and CJK glyphs.
|
|
74
|
+
*
|
|
75
|
+
* Returns `{ before, shimmer, after }` strings. Both renderers (chalk in
|
|
76
|
+
* bridgeUI.ts and React/Ink in bridge.tsx) apply their own coloring to
|
|
77
|
+
* these segments.
|
|
78
|
+
*/
|
|
79
|
+
export function computeShimmerSegments(
|
|
80
|
+
text: string,
|
|
81
|
+
glimmerIndex: number,
|
|
82
|
+
): { before: string; shimmer: string; after: string } {
|
|
83
|
+
const messageWidth = stringWidth(text)
|
|
84
|
+
const shimmerStart = glimmerIndex - 1
|
|
85
|
+
const shimmerEnd = glimmerIndex + 1
|
|
86
|
+
|
|
87
|
+
// When shimmer is offscreen, return all text as "before"
|
|
88
|
+
if (shimmerStart >= messageWidth || shimmerEnd < 0) {
|
|
89
|
+
return { before: text, shimmer: '', after: '' }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Split into at most 3 segments by visual column position
|
|
93
|
+
const clampedStart = Math.max(0, shimmerStart)
|
|
94
|
+
let colPos = 0
|
|
95
|
+
let before = ''
|
|
96
|
+
let shimmer = ''
|
|
97
|
+
let after = ''
|
|
98
|
+
for (const { segment } of getGraphemeSegmenter().segment(text)) {
|
|
99
|
+
const segWidth = stringWidth(segment)
|
|
100
|
+
if (colPos + segWidth <= clampedStart) {
|
|
101
|
+
before += segment
|
|
102
|
+
} else if (colPos > shimmerEnd) {
|
|
103
|
+
after += segment
|
|
104
|
+
} else {
|
|
105
|
+
shimmer += segment
|
|
106
|
+
}
|
|
107
|
+
colPos += segWidth
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { before, shimmer, after }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Computed bridge status label and color from connection state. */
|
|
114
|
+
export type BridgeStatusInfo = {
|
|
115
|
+
label:
|
|
116
|
+
| 'Remote Control failed'
|
|
117
|
+
| 'Remote Control reconnecting'
|
|
118
|
+
| 'Remote Control active'
|
|
119
|
+
| 'Remote Control connecting\u2026'
|
|
120
|
+
color: 'error' | 'warning' | 'success'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Derive a status label and color from the bridge connection state. */
|
|
124
|
+
export function getBridgeStatus({
|
|
125
|
+
error,
|
|
126
|
+
connected,
|
|
127
|
+
sessionActive,
|
|
128
|
+
reconnecting,
|
|
129
|
+
}: {
|
|
130
|
+
error: string | undefined
|
|
131
|
+
connected: boolean
|
|
132
|
+
sessionActive: boolean
|
|
133
|
+
reconnecting: boolean
|
|
134
|
+
}): BridgeStatusInfo {
|
|
135
|
+
if (error) return { label: 'Remote Control failed', color: 'error' }
|
|
136
|
+
if (reconnecting)
|
|
137
|
+
return { label: 'Remote Control reconnecting', color: 'warning' }
|
|
138
|
+
if (sessionActive || connected)
|
|
139
|
+
return { label: 'Remote Control active', color: 'success' }
|
|
140
|
+
return { label: 'Remote Control connecting\u2026', color: 'warning' }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Footer text shown when bridge is idle (Ready state). */
|
|
144
|
+
export function buildIdleFooterText(url: string): string {
|
|
145
|
+
return `Code everywhere with the Claude app or ${url}`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Footer text shown when a session is active (Connected state). */
|
|
149
|
+
export function buildActiveFooterText(url: string): string {
|
|
150
|
+
return `Continue coding in the Claude app or ${url}`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Footer text shown when the bridge has failed. */
|
|
154
|
+
export const FAILED_FOOTER_TEXT = 'Something went wrong, please try again'
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Wrap text in an OSC 8 terminal hyperlink. Zero visual width for layout purposes.
|
|
158
|
+
* strip-ansi (used by stringWidth) correctly strips these sequences, so
|
|
159
|
+
* countVisualLines in bridgeUI.ts remains accurate.
|
|
160
|
+
*/
|
|
161
|
+
export function wrapWithOsc8Link(text: string, url: string): string {
|
|
162
|
+
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`
|
|
163
|
+
}
|