linkshell-cli 0.5.40 → 0.5.42

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.
@@ -1724,6 +1724,134 @@ function findCodexRolloutPath(sessionId: string, root = join(homedir(), ".codex"
1724
1724
  return undefined;
1725
1725
  }
1726
1726
 
1727
+ type RemoteSessionRecord = {
1728
+ id: string;
1729
+ cwd?: string;
1730
+ title?: string;
1731
+ model?: string;
1732
+ createdAt?: number;
1733
+ lastActivityAt?: number;
1734
+ usage?: unknown;
1735
+ status?: AgentStatus;
1736
+ };
1737
+
1738
+ function readJsonlPrefix(path: string, maxBytes = 8192): string | undefined {
1739
+ let fd: number | undefined;
1740
+ try {
1741
+ const size = statSync(path).size;
1742
+ if (size <= 0) return "";
1743
+ fd = openSync(path, "r");
1744
+ const buf = Buffer.alloc(Math.min(maxBytes, size));
1745
+ readSync(fd, buf, 0, buf.length, 0);
1746
+ return buf.toString("utf8");
1747
+ } catch {
1748
+ return undefined;
1749
+ } finally {
1750
+ if (fd !== undefined) {
1751
+ try { closeSync(fd); } catch { /* ignore */ }
1752
+ }
1753
+ }
1754
+ }
1755
+
1756
+ /** Newest Codex rollouts as catalog metadata. Reads the first line only. */
1757
+ function listCodexSessionsFromDisk(limit = 80): RemoteSessionRecord[] {
1758
+ const files: { path: string; filenameId: string; mt: number }[] = [];
1759
+ walkCodexRollouts(join(homedir(), ".codex", "sessions"), (path, filenameId, mt) => {
1760
+ files.push({ path, filenameId, mt });
1761
+ });
1762
+ files.sort((a, b) => b.mt - a.mt);
1763
+ const out: RemoteSessionRecord[] = [];
1764
+ const seen = new Set<string>();
1765
+ for (const file of files) {
1766
+ if (out.length >= limit) break;
1767
+ const prefix = readJsonlPrefix(file.path);
1768
+ const first = prefix
1769
+ ? prefix.slice(0, prefix.indexOf("\n") >= 0 ? prefix.indexOf("\n") : prefix.length)
1770
+ : "";
1771
+ let sessionId = file.filenameId;
1772
+ let cwd: string | undefined;
1773
+ let createdAt: number | undefined;
1774
+ if (first) {
1775
+ try {
1776
+ const payload = asRecord(asRecord(JSON.parse(first))?.payload) ?? asRecord(JSON.parse(first));
1777
+ sessionId = firstString(payload, ["session_id", "id"]) ?? file.filenameId;
1778
+ cwd = firstString(payload, ["cwd"]);
1779
+ createdAt = parseTimestamp(payload?.timestamp ?? asRecord(JSON.parse(first))?.timestamp);
1780
+ } catch {
1781
+ sessionId = file.filenameId;
1782
+ }
1783
+ }
1784
+ if (seen.has(sessionId)) continue;
1785
+ seen.add(sessionId);
1786
+ out.push({
1787
+ id: sessionId,
1788
+ cwd,
1789
+ title: cwd ? titleFromCwd(cwd) : undefined,
1790
+ createdAt,
1791
+ lastActivityAt: file.mt,
1792
+ });
1793
+ }
1794
+ return out;
1795
+ }
1796
+
1797
+ function listClaudeSessionsFromDisk(limit = 80): RemoteSessionRecord[] {
1798
+ const root = join(homedir(), ".claude", "projects");
1799
+ if (!existsSync(root)) return [];
1800
+ const files: { id: string; mt: number; cwd?: string }[] = [];
1801
+ let projectDirs: string[] = [];
1802
+ try {
1803
+ projectDirs = readdirSync(root).map((name) => join(root, name));
1804
+ } catch {
1805
+ return [];
1806
+ }
1807
+ for (const dir of projectDirs) {
1808
+ let stat;
1809
+ try { stat = statSync(dir); } catch { continue; }
1810
+ if (!stat.isDirectory()) continue;
1811
+ const dirName = basename(dir);
1812
+ const cwd = dirName.startsWith("-") ? `/${dirName.slice(1).replace(/-/g, "/")}` : undefined;
1813
+ let entries: string[];
1814
+ try { entries = readdirSync(dir); } catch { continue; }
1815
+ for (const name of entries) {
1816
+ if (!name.endsWith(".jsonl")) continue;
1817
+ const path = join(dir, name);
1818
+ let fileStat;
1819
+ try { fileStat = statSync(path); } catch { continue; }
1820
+ if (!fileStat.isFile()) continue;
1821
+ files.push({
1822
+ id: name.replace(/\.jsonl$/, ""),
1823
+ mt: fileStat.mtimeMs,
1824
+ cwd,
1825
+ });
1826
+ }
1827
+ }
1828
+ files.sort((a, b) => b.mt - a.mt);
1829
+ const out: RemoteSessionRecord[] = [];
1830
+ const seen = new Set<string>();
1831
+ for (const file of files) {
1832
+ if (out.length >= limit) break;
1833
+ if (seen.has(file.id)) continue;
1834
+ seen.add(file.id);
1835
+ out.push({
1836
+ id: file.id,
1837
+ cwd: file.cwd,
1838
+ title: file.cwd ? titleFromCwd(file.cwd) : undefined,
1839
+ lastActivityAt: file.mt,
1840
+ });
1841
+ }
1842
+ return out;
1843
+ }
1844
+
1845
+ function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
1846
+ return new Promise((resolve, reject) => {
1847
+ const timer = setTimeout(() => reject(new Error(message)), ms);
1848
+ promise.then(
1849
+ (value) => { clearTimeout(timer); resolve(value); },
1850
+ (error) => { clearTimeout(timer); reject(error); },
1851
+ );
1852
+ });
1853
+ }
1854
+
1727
1855
  function findLatestCodexRolloutForCwd(cwd: string, root = join(homedir(), ".codex", "sessions")): { path: string; sessionId: string } | undefined {
1728
1856
  const wanted = cwd.replace(/\/+$/, "");
1729
1857
  const candidates: { path: string; sessionId: string; mt: number }[] = [];
@@ -2225,8 +2353,8 @@ function previewFromTimelineItem(item: AgentTimelineItem): string | undefined {
2225
2353
 
2226
2354
  export class AgentWorkspaceProxy {
2227
2355
  private clients = new Map<AgentProvider, AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient>();
2228
- // In-flight client starts, so concurrent ensureProviderClient calls (eager
2229
- // initialize() racing conversation.open) share one subprocess spawn.
2356
+ // In-flight client starts, so concurrent ensureProviderClient calls
2357
+ // (prompt racing conversation.open) share one subprocess spawn.
2230
2358
  private clientStartPromises = new Map<AgentProvider, Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined>>();
2231
2359
  private agentProtocols = new Map<AgentProvider, AgentProtocol>();
2232
2360
  private providerCapabilities = new Map<AgentProvider, ProviderRuntimeCapabilities>();
@@ -2280,7 +2408,6 @@ export class AgentWorkspaceProxy {
2280
2408
  case "agent.v2.capabilities.request":
2281
2409
  await this.initialize();
2282
2410
  this.sendCapabilities();
2283
- this.sendUsageReport();
2284
2411
  break;
2285
2412
  case "agent.v2.conversation.open": {
2286
2413
  const payload = parseTypedPayload("agent.v2.conversation.open", envelope.payload);
@@ -2517,6 +2644,8 @@ export class AgentWorkspaceProxy {
2517
2644
  private static readonly FS_SYNC_THROTTLE_MS = 1500;
2518
2645
  private static readonly EXTERNAL_HYDRATE_THROTTLE_MS = 400;
2519
2646
  private static readonly LIST_IDLE_CAP = 40;
2647
+ private static readonly LIST_HARD_CAP = 80;
2648
+ private static readonly CLIENT_START_TIMEOUT_MS = 12_000;
2520
2649
 
2521
2650
  private startSessionWatchers(): void {
2522
2651
  const roots = [
@@ -2628,6 +2757,7 @@ export class AgentWorkspaceProxy {
2628
2757
  out.push(conversation);
2629
2758
  };
2630
2759
  for (const conversation of ranked) {
2760
+ if (out.length >= AgentWorkspaceProxy.LIST_HARD_CAP) break;
2631
2761
  if (
2632
2762
  conversation.status === "running" ||
2633
2763
  conversation.status === "waiting_permission" ||
@@ -2721,14 +2851,11 @@ export class AgentWorkspaceProxy {
2721
2851
  private async initialize(): Promise<void> {
2722
2852
  if (this.initialized) return;
2723
2853
  this.initialized = true;
2724
- // Eagerly start all detected providers so capabilities report real status
2725
- const startPromises = this.input.availableProviders.map((p) => this.ensureProviderClient(p));
2726
- await Promise.allSettled(startPromises);
2727
2854
  this.status = "idle";
2728
2855
  this.error = undefined;
2856
+ // Presence only. Spawning every detected CLI here (especially
2857
+ // `grok agent stdio`) blocked the sequential control plane on connect.
2729
2858
  this.sendCapabilities();
2730
- // Start watching the on-disk session stores so externally-started sessions
2731
- // and status changes surface live, not just at connect time.
2732
2859
  this.startSessionWatchers();
2733
2860
  this.startProcessDiscovery();
2734
2861
  }
@@ -2898,10 +3025,9 @@ export class AgentWorkspaceProxy {
2898
3025
  const existing = this.clients.get(provider);
2899
3026
  if (existing) return existing;
2900
3027
 
2901
- // Dedupe concurrent starts: eager initialize() and conversation.open can
2902
- // race here before this.clients is populated (it's only set AFTER the
2903
- // client finishes initializing), which would spawn the provider subprocess
2904
- // twice. Memoize the in-flight promise so both callers share one spawn;
3028
+ // Dedupe concurrent starts: conversation.open and prompt can race here
3029
+ // before this.clients is populated (it's only set AFTER initialize).
3030
+ // Memoize the in-flight promise so both callers share one spawn;
2905
3031
  // drop it once settled so a failed start can be retried.
2906
3032
  const inflight = this.clientStartPromises.get(provider);
2907
3033
  if (inflight) return inflight;
@@ -2959,8 +3085,17 @@ export class AgentWorkspaceProxy {
2959
3085
  onRequest: (method, params) => this.handleRequest(method, params),
2960
3086
  onExit: (message) => this.handleProviderExit(provider, message),
2961
3087
  });
2962
- await client.initialize();
2963
- return client;
3088
+ try {
3089
+ await withTimeout(
3090
+ client.initialize(),
3091
+ AgentWorkspaceProxy.CLIENT_START_TIMEOUT_MS,
3092
+ `${providerLabel(provider)} 启动超时`,
3093
+ );
3094
+ return client;
3095
+ } catch (error) {
3096
+ try { client.stop(); } catch { /* ignore */ }
3097
+ throw error;
3098
+ }
2964
3099
  };
2965
3100
 
2966
3101
  try {
@@ -3136,92 +3271,114 @@ export class AgentWorkspaceProxy {
3136
3271
  }
3137
3272
  }
3138
3273
 
3139
- private async syncProviderSessions(): Promise<void> {
3140
- await this.initialize();
3141
- for (const [provider, client] of this.clients) {
3142
- try {
3143
- let activeClient = client;
3144
- let result: unknown;
3145
- try {
3146
- result = await activeClient.listSessions();
3147
- } catch (error) {
3148
- const recovered = await this.recycleProviderClient(provider, error);
3149
- if (!recovered) throw error;
3150
- activeClient = recovered;
3151
- result = await activeClient.listSessions();
3152
- }
3153
- // Codex's thread/list carries no live status and only knows threads
3154
- // THIS app-server owns — so an externally-run `codex` shows idle. Read
3155
- // running state off disk once per sync (CPU-frugal: stats all rollouts,
3156
- // reads only the fresh few). Claude reports status via parseRemoteSessions.
3157
- const codexRunning = provider === "codex" ? runningCodexSessionIds(Date.now()) : undefined;
3158
- for (const remote of this.pickListedRemotes(parseRemoteSessions(result), provider, codexRunning)) {
3159
- const agentSessionId = remote.id;
3160
- // Skip conversations the user explicitly forgot — don't resurrect them.
3161
- if (this.deletedAgentSessionIds.has(agentSessionId)) continue;
3162
- const existingId = this.conversationByAgentSessionId.get(agentSessionId);
3163
- const now = Date.now();
3164
- const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
3165
- const existing = this.conversations.get(conversationId);
3166
- const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
3167
- const model = remote.model ?? existing?.model;
3168
- // Usage read straight from the on-disk transcript (decoupled from any
3169
- // live LinkShell turn) — so history + externally-started sessions all
3170
- // carry a context meter. Falls back to any existing live-turn usage.
3171
- const usageFields = normalizeUsageFields(remote.usage, model);
3172
- const usage: AgentConversationUsage | undefined = usageFields
3173
- ? { ...usageFields, updatedAt: remote.lastActivityAt ?? now }
3174
- : existing?.usage;
3175
- // Status precedence. While LinkShell is driving a turn for this
3176
- // conversation, the in-memory status is authoritative — the transcript
3177
- // lags (the SDK hasn't flushed the in-flight turn to disk), so trusting
3178
- // it would flicker a live "running" turn back to idle. Otherwise the
3179
- // transcript-derived running signal is authoritative (Claude reports it
3180
- // via remote.status; Codex via the on-disk rollout set): surface
3181
- // "running" while an external process drives it, and let it clear once
3182
- // the external turn finishes. We only fall back to the existing status
3183
- // to keep error/waiting_permission sticky across a re-sync — the
3184
- // transcript signals never emit those, so they live only in `existing`;
3185
- // defaulting everything else to idle avoids a purely-external session
3186
- // getting stuck at "running" forever.
3187
- const remoteRunning = remote.status === "running" || (codexRunning?.has(agentSessionId) ?? false);
3188
- const linkshellDriving = this.currentTurnIds.has(conversationId);
3189
- const status: AgentStatus = linkshellDriving
3190
- ? existing?.status ?? "running"
3191
- : remoteRunning
3192
- ? "running"
3193
- : remote.status === "error"
3194
- ? "error"
3195
- : existing?.status === "error" || existing?.status === "waiting_permission"
3196
- ? existing.status
3197
- : "idle";
3198
- const conversation: AgentConversation = {
3199
- id: conversationId,
3200
- agentSessionId,
3201
- provider,
3202
- cwd,
3203
- title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
3204
- model,
3205
- reasoningEffort: existing?.reasoningEffort,
3206
- permissionMode: existing?.permissionMode,
3207
- collaborationMode: existing?.collaborationMode,
3208
- status,
3209
- archived: existing?.archived ?? false,
3210
- lastMessagePreview: existing?.lastMessagePreview,
3211
- lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
3212
- createdAt: remote.createdAt ?? existing?.createdAt ?? now,
3213
- usage,
3214
- group: existing?.group ?? (provider === "claude" ? titleFromCwd(cwd) : existing?.group),
3215
- workspaceRoots: existing?.workspaceRoots,
3216
- };
3217
- this.conversations.set(conversation.id, conversation);
3218
- this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
3219
- this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
3220
- }
3221
- } catch (error) {
3274
+ private listDiskSessions(provider: AgentProvider): RemoteSessionRecord[] {
3275
+ if (provider === "codex") return listCodexSessionsFromDisk(AgentWorkspaceProxy.LIST_HARD_CAP);
3276
+ if (provider === "claude") return listClaudeSessionsFromDisk(AgentWorkspaceProxy.LIST_HARD_CAP);
3277
+ return [];
3278
+ }
3279
+
3280
+ private async listClientSessions(provider: AgentProvider): Promise<RemoteSessionRecord[]> {
3281
+ const client = this.clients.get(provider);
3282
+ if (!client) return [];
3283
+ try {
3284
+ return parseRemoteSessions(await client.listSessions());
3285
+ } catch (error) {
3286
+ const recovered = await this.recycleProviderClient(provider, error);
3287
+ if (!recovered) {
3222
3288
  if (this.input.verbose) {
3223
3289
  process.stderr.write(`[agent:v2] session list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
3224
3290
  }
3291
+ return [];
3292
+ }
3293
+ try {
3294
+ return parseRemoteSessions(await recovered.listSessions());
3295
+ } catch (retryError) {
3296
+ if (this.input.verbose) {
3297
+ process.stderr.write(`[agent:v2] session list failed for ${provider}: ${retryError instanceof Error ? retryError.message : String(retryError)}\n`);
3298
+ }
3299
+ return [];
3300
+ }
3301
+ }
3302
+ }
3303
+
3304
+ private upsertRemoteSession(
3305
+ provider: AgentProvider,
3306
+ remote: RemoteSessionRecord,
3307
+ now: number,
3308
+ runningIds?: Set<string>,
3309
+ ): void {
3310
+ const agentSessionId = remote.id;
3311
+ if (this.deletedAgentSessionIds.has(agentSessionId)) return;
3312
+ const existingId = this.conversationByAgentSessionId.get(agentSessionId);
3313
+ const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
3314
+ const existing = this.conversations.get(conversationId);
3315
+ const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
3316
+ const model = remote.model ?? existing?.model;
3317
+ const usageFields = normalizeUsageFields(remote.usage, model);
3318
+ const usage: AgentConversationUsage | undefined = usageFields
3319
+ ? { ...usageFields, updatedAt: remote.lastActivityAt ?? now }
3320
+ : existing?.usage;
3321
+ const remoteRunning = remote.status === "running" || (runningIds?.has(agentSessionId) ?? false);
3322
+ const linkshellDriving = this.currentTurnIds.has(conversationId);
3323
+ const owned = Boolean(agentSessionId && this.ownedCodexSessionIds.has(agentSessionId));
3324
+ const attached = !owned && (
3325
+ existing?.control === "attached" ||
3326
+ this.attachedExternalCodex.has(conversationId) ||
3327
+ remoteRunning
3328
+ );
3329
+ if (attached) this.attachedExternalCodex.add(conversationId);
3330
+ const status: AgentStatus = linkshellDriving
3331
+ ? existing?.status ?? "running"
3332
+ : remoteRunning
3333
+ ? "running"
3334
+ : remote.status === "error"
3335
+ ? "error"
3336
+ : existing?.status === "error" || existing?.status === "waiting_permission"
3337
+ ? existing.status
3338
+ : "idle";
3339
+ const conversation: AgentConversation = {
3340
+ id: conversationId,
3341
+ agentSessionId,
3342
+ provider,
3343
+ cwd,
3344
+ title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
3345
+ model,
3346
+ reasoningEffort: existing?.reasoningEffort,
3347
+ permissionMode: existing?.permissionMode,
3348
+ collaborationMode: existing?.collaborationMode,
3349
+ control: owned ? "owned" : attached ? "attached" : existing?.control,
3350
+ status,
3351
+ archived: existing?.archived ?? false,
3352
+ lastMessagePreview: existing?.lastMessagePreview,
3353
+ lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
3354
+ createdAt: remote.createdAt ?? existing?.createdAt ?? now,
3355
+ usage,
3356
+ group: existing?.group ?? (provider === "claude" ? titleFromCwd(cwd) : existing?.group),
3357
+ workspaceRoots: existing?.workspaceRoots,
3358
+ };
3359
+ this.conversations.set(conversation.id, conversation);
3360
+ this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
3361
+ this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
3362
+ }
3363
+
3364
+ private async syncProviderSessions(): Promise<void> {
3365
+ await this.initialize();
3366
+ const now = Date.now();
3367
+ const codexRunning = runningCodexSessionIds(now);
3368
+ const providers = new Set<AgentProvider>([
3369
+ ...this.input.availableProviders,
3370
+ ...this.clients.keys(),
3371
+ ]);
3372
+ for (const provider of providers) {
3373
+ const byId = new Map<string, RemoteSessionRecord>();
3374
+ for (const remote of this.listDiskSessions(provider)) byId.set(remote.id, remote);
3375
+ for (const remote of await this.listClientSessions(provider)) {
3376
+ const prev = byId.get(remote.id);
3377
+ byId.set(remote.id, prev ? { ...prev, ...remote } : remote);
3378
+ }
3379
+ const runningIds = provider === "codex" ? codexRunning : undefined;
3380
+ for (const remote of this.pickListedRemotes([...byId.values()], provider, runningIds)) {
3381
+ this.upsertRemoteSession(provider, remote, now, runningIds);
3225
3382
  }
3226
3383
  }
3227
3384
  }
@@ -3460,26 +3617,40 @@ export class AgentWorkspaceProxy {
3460
3617
  ...this.lastDiscoveredProviders,
3461
3618
  ])];
3462
3619
  const providers = catalog.map((provider) => {
3620
+ const resolved = resolveAgentCommand({
3621
+ provider,
3622
+ command: this.input.command,
3623
+ });
3463
3624
  const client = this.clients.get(provider);
3464
- const protocol = this.agentProtocols.get(provider);
3625
+ const protocol = this.agentProtocols.get(provider) ?? resolved?.protocol;
3465
3626
  const runtimeCapabilities = this.providerCapabilities.get(provider);
3466
3627
  const advertised = runtimeCapabilities?.advertised
3467
3628
  ?? (client instanceof AcpClient ? client.advertised : undefined);
3468
- const enabled = Boolean(client);
3629
+ const canStart = Boolean(resolved) && (
3630
+ this.input.availableProviders.includes(provider) || Boolean(this.input.command?.trim())
3631
+ );
3632
+ const enabled = Boolean(client) || canStart;
3469
3633
  const isAcp = protocol === "acp";
3470
- const supportsImages = enabled && protocolSupportsImages(protocol, advertised);
3634
+ const supportsImages = Boolean(client) && protocolSupportsImages(protocol, advertised);
3471
3635
  const isClaudeFallback = protocol === "claude-stream-json";
3472
- const supportsPermission = enabled && !isClaudeFallback;
3473
- const supportsReasoningEffort = enabled && !isClaudeFallback;
3474
- const supportsSessionList = enabled && (isAcp ? Boolean(advertised?.listSession) : !isClaudeFallback);
3475
- const supportsSessionLoad = enabled && (isAcp ? Boolean(advertised?.loadSession) : !isClaudeFallback);
3476
- const supportsCancel = enabled && (isAcp ? Boolean(advertised?.cancel) : true);
3477
- const supportsFork = enabled && (isAcp
3636
+ const supportsPermission = Boolean(client) && !isClaudeFallback;
3637
+ const supportsReasoningEffort = (Boolean(client) || (canStart && protocol === "codex-app-server")) && !isClaudeFallback;
3638
+ const diskListable = provider === "codex" || provider === "claude";
3639
+ const supportsSessionList = Boolean(client)
3640
+ ? (isAcp ? Boolean(advertised?.listSession) : !isClaudeFallback)
3641
+ : canStart && diskListable;
3642
+ const supportsSessionLoad = Boolean(client)
3643
+ ? (isAcp ? Boolean(advertised?.loadSession) : !isClaudeFallback)
3644
+ : canStart;
3645
+ const supportsCancel = Boolean(client)
3646
+ ? (isAcp ? Boolean(advertised?.cancel) : true)
3647
+ : canStart && !isAcp;
3648
+ const supportsFork = Boolean(client) && (isAcp
3478
3649
  ? Boolean(advertised?.forkSession)
3479
3650
  : protocol === "codex-app-server" || protocol === "claude-agent-sdk");
3480
- const supportsSetModel = enabled && (isAcp
3481
- ? Boolean(advertised?.setModel)
3482
- : protocol === "codex-app-server");
3651
+ const supportsSetModel = Boolean(client)
3652
+ ? (isAcp ? Boolean(advertised?.setModel) : protocol === "codex-app-server")
3653
+ : canStart && protocol === "codex-app-server";
3483
3654
  const protocolBacked = isProtocolAgentProvider(provider);
3484
3655
  const commands = mergeCommands(
3485
3656
  defaultProviderCommands(provider, this.input.cwd, enabled),
@@ -3499,8 +3670,8 @@ export class AgentWorkspaceProxy {
3499
3670
  supportsPermission,
3500
3671
  supportsPlan: enabled && (isAcp ? Boolean(advertised?.embeddedContext) : true),
3501
3672
  supportsCancel,
3502
- models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
3503
- defaultModel: runtimeCapabilities?.defaultModel ?? "default",
3673
+ models: runtimeCapabilities?.models ?? (provider === "codex" ? [...CODEX_FALLBACK_MODELS] : [{ id: "default", label: "默认模型" }]),
3674
+ defaultModel: runtimeCapabilities?.defaultModel ?? (provider === "codex" ? CODEX_FALLBACK_DEFAULT_MODEL : "default"),
3504
3675
  reasoningEfforts: supportsReasoningEffort
3505
3676
  ? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
3506
3677
  : [],
@@ -3554,9 +3725,18 @@ export class AgentWorkspaceProxy {
3554
3725
  }
3555
3726
 
3556
3727
  private shouldOpenAsAttachedView(conversation: AgentConversation): boolean {
3557
- return conversation.control === "attached"
3558
- || this.attachedExternalCodex.has(conversation.id)
3559
- || isLiveConversationId(conversation.id);
3728
+ if (conversation.control === "attached") return true;
3729
+ if (this.attachedExternalCodex.has(conversation.id)) return true;
3730
+ if (isLiveConversationId(conversation.id)) return true;
3731
+ if (
3732
+ conversation.provider === "codex" &&
3733
+ conversation.agentSessionId &&
3734
+ !this.ownedCodexSessionIds.has(conversation.agentSessionId) &&
3735
+ conversation.status === "running"
3736
+ ) {
3737
+ return true;
3738
+ }
3739
+ return false;
3560
3740
  }
3561
3741
 
3562
3742
  private openAttachedView(
@@ -3595,6 +3775,7 @@ export class AgentWorkspaceProxy {
3595
3775
  forkFromConversationId?: string;
3596
3776
  forkFromTurnId?: string;
3597
3777
  }): Promise<AgentConversation | undefined> {
3778
+ await this.initialize();
3598
3779
  const provider = payload.provider ?? this.input.availableProviders[0];
3599
3780
  if (!provider) {
3600
3781
  return this.openFailure(payload, "没有可用的 Agent provider。");
@@ -3642,6 +3823,14 @@ export class AgentWorkspaceProxy {
3642
3823
  if (existingConversation && this.shouldOpenAsAttachedView(existingConversation)) {
3643
3824
  return this.openAttachedView(existingConversation, payload.conversationId);
3644
3825
  }
3826
+ if (
3827
+ existingConversation?.provider === "codex" &&
3828
+ existingConversation.agentSessionId &&
3829
+ !this.ownedCodexSessionIds.has(existingConversation.agentSessionId) &&
3830
+ runningCodexSessionIds(Date.now()).has(existingConversation.agentSessionId)
3831
+ ) {
3832
+ return this.openAttachedView(existingConversation, payload.conversationId);
3833
+ }
3645
3834
 
3646
3835
  let client = await this.ensureProviderClient(provider);
3647
3836
  if (!client) {
@@ -5846,10 +6035,16 @@ export class AgentWorkspaceProxy {
5846
6035
  }
5847
6036
 
5848
6037
  private sendSnapshot(conversationId?: string): void {
5849
- const conversations = [...this.conversations.values()];
6038
+ const conversations = this.listedConversations(
6039
+ [...this.conversations.values()].filter((conversation) => !conversation.archived),
6040
+ );
6041
+ if (conversationId && !conversations.some((conversation) => conversation.id === conversationId)) {
6042
+ const extra = this.conversations.get(conversationId);
6043
+ if (extra) conversations.push(extra);
6044
+ }
5850
6045
  const items = conversationId
5851
6046
  ? this.timelines.get(conversationId) ?? []
5852
- : [...this.timelines.values()].flat();
6047
+ : [...this.watchedConversationIds].flatMap((id) => this.timelines.get(id) ?? []);
5853
6048
  this.input.send(createEnvelope({
5854
6049
  type: "agent.v2.snapshot",
5855
6050
  sessionId: this.input.sessionId,
@@ -1,2 +1,2 @@
1
- import{r as n,j as e}from"./vendor-react-M7xG3qiw.js";import{k as b,a as w,l as k,h as C,m as z}from"./index-BahdI_WD.js";import"./vendor-highlight-NXWAAyIv.js";function I({store:x,initialPath:d,onClose:j}){const[c,u]=n.useState(d||"."),[y,N]=n.useState([]),[o,l]=n.useState(!1),[f,r]=n.useState(null),[i,p]=n.useState(null),a=n.useCallback(async t=>{l(!0),r(null),p(null);try{const s=await x.browse(t,!0);s.error?r(s.error):(u(s.path),N([...s.entries].sort((m,h)=>m.isDirectory!==h.isDirectory?m.isDirectory?-1:1:m.name.localeCompare(h.name))))}catch(s){r(s.message||"浏览失败")}finally{l(!1)}},[x]);n.useEffect(()=>{a(d||".")},[a,d]);const g=async t=>{l(!0),r(null);try{const s=await x.readFile(t.path);s.error?r(s.error):p({path:s.path,content:s.content,truncated:s.truncated})}catch(s){r(s.message||"读取失败")}finally{l(!1)}},v=c.replace(/\/+$/,"").split("/").slice(0,-1).join("/")||"/";return e.jsxs("div",{className:"flex h-full flex-col bg-surface",children:[e.jsxs("div",{className:"flex items-center justify-between border-b border-border px-3 py-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-2xs text-content-secondary",children:[e.jsx(b,{size:13,className:"text-content-faint"})," 文件"]}),e.jsx("button",{onClick:j,className:"codex-btn-ghost px-2 py-1.5","aria-label":"关闭文件面板",children:e.jsx(w,{size:15})})]}),e.jsxs("div",{className:"flex items-center gap-1 border-b border-border px-2 py-1.5",children:[e.jsx("button",{onClick:()=>a(v),disabled:o,className:"cursor-pointer rounded-md p-1.5 text-content-muted transition-colors hover:bg-surface-overlay hover:text-accent",title:"上级目录","aria-label":"上级目录",children:e.jsx(k,{size:14})}),e.jsx("input",{value:c,onChange:t=>u(t.target.value),onKeyDown:t=>t.key==="Enter"&&a(c),className:"flex-1 bg-transparent font-mono text-2xs text-content-primary outline-none"}),e.jsx("button",{onClick:()=>a(c),disabled:o,className:"cursor-pointer rounded-md p-1.5 text-content-muted transition-colors hover:bg-surface-overlay hover:text-accent",title:"刷新","aria-label":"刷新",children:e.jsx(C,{size:14})})]}),f&&e.jsx("p",{className:"px-3 py-1.5 text-2xs text-danger",children:f}),e.jsxs("div",{className:"min-h-0 flex-1 overflow-auto",children:[o&&e.jsx("p",{className:"px-3 py-2 text-2xs text-content-muted",children:"加载中…"}),!o&&i?e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between border-b border-border px-3 py-1.5",children:[e.jsx("span",{className:"truncate font-mono text-2xs text-content-secondary",children:i.path}),e.jsx("button",{onClick:()=>p(null),className:"codex-btn-ghost text-2xs",children:"返回"})]}),e.jsxs("pre",{className:"whitespace-pre-wrap px-3 py-2 font-mono text-2xs leading-relaxed text-content-secondary",children:[i.content,i.truncated&&e.jsx("span",{className:"text-warning",children:`
1
+ import{r as n,j as e}from"./vendor-react-M7xG3qiw.js";import{k as b,a as w,l as k,h as C,m as z}from"./index-q85psYZ5.js";import"./vendor-highlight-NXWAAyIv.js";function I({store:x,initialPath:d,onClose:j}){const[c,u]=n.useState(d||"."),[y,N]=n.useState([]),[o,l]=n.useState(!1),[f,r]=n.useState(null),[i,p]=n.useState(null),a=n.useCallback(async t=>{l(!0),r(null),p(null);try{const s=await x.browse(t,!0);s.error?r(s.error):(u(s.path),N([...s.entries].sort((m,h)=>m.isDirectory!==h.isDirectory?m.isDirectory?-1:1:m.name.localeCompare(h.name))))}catch(s){r(s.message||"浏览失败")}finally{l(!1)}},[x]);n.useEffect(()=>{a(d||".")},[a,d]);const g=async t=>{l(!0),r(null);try{const s=await x.readFile(t.path);s.error?r(s.error):p({path:s.path,content:s.content,truncated:s.truncated})}catch(s){r(s.message||"读取失败")}finally{l(!1)}},v=c.replace(/\/+$/,"").split("/").slice(0,-1).join("/")||"/";return e.jsxs("div",{className:"flex h-full flex-col bg-surface",children:[e.jsxs("div",{className:"flex items-center justify-between border-b border-border px-3 py-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-2xs text-content-secondary",children:[e.jsx(b,{size:13,className:"text-content-faint"})," 文件"]}),e.jsx("button",{onClick:j,className:"codex-btn-ghost px-2 py-1.5","aria-label":"关闭文件面板",children:e.jsx(w,{size:15})})]}),e.jsxs("div",{className:"flex items-center gap-1 border-b border-border px-2 py-1.5",children:[e.jsx("button",{onClick:()=>a(v),disabled:o,className:"cursor-pointer rounded-md p-1.5 text-content-muted transition-colors hover:bg-surface-overlay hover:text-accent",title:"上级目录","aria-label":"上级目录",children:e.jsx(k,{size:14})}),e.jsx("input",{value:c,onChange:t=>u(t.target.value),onKeyDown:t=>t.key==="Enter"&&a(c),className:"flex-1 bg-transparent font-mono text-2xs text-content-primary outline-none"}),e.jsx("button",{onClick:()=>a(c),disabled:o,className:"cursor-pointer rounded-md p-1.5 text-content-muted transition-colors hover:bg-surface-overlay hover:text-accent",title:"刷新","aria-label":"刷新",children:e.jsx(C,{size:14})})]}),f&&e.jsx("p",{className:"px-3 py-1.5 text-2xs text-danger",children:f}),e.jsxs("div",{className:"min-h-0 flex-1 overflow-auto",children:[o&&e.jsx("p",{className:"px-3 py-2 text-2xs text-content-muted",children:"加载中…"}),!o&&i?e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between border-b border-border px-3 py-1.5",children:[e.jsx("span",{className:"truncate font-mono text-2xs text-content-secondary",children:i.path}),e.jsx("button",{onClick:()=>p(null),className:"codex-btn-ghost text-2xs",children:"返回"})]}),e.jsxs("pre",{className:"whitespace-pre-wrap px-3 py-2 font-mono text-2xs leading-relaxed text-content-secondary",children:[i.content,i.truncated&&e.jsx("span",{className:"text-warning",children:`
2
2
  …(已截断)`})]})]}):!o&&y.map(t=>e.jsxs("button",{onClick:()=>t.isDirectory?a(t.path):g(t),className:"flex w-full cursor-pointer items-center gap-2 px-3 py-1 text-left transition-colors hover:bg-surface-overlay",children:[t.isDirectory?e.jsx(b,{size:13,className:"shrink-0 text-content-faint"}):e.jsx(z,{size:13,className:"shrink-0 text-content-faint"}),e.jsx("span",{className:"truncate font-mono text-2xs text-content-primary",children:t.name}),!t.isDirectory&&t.size!=null&&e.jsxs("span",{className:"ml-auto font-mono text-2xs text-content-faint",children:[t.size,"B"]})]},t.path))]})]})}export{I as FileBrowser};
@@ -1,2 +1,2 @@
1
- import{r,j as e}from"./vendor-react-M7xG3qiw.js";import{e as me,g as he,a as be,c as ge,d as ve,f as je,h as ye,i as Ne,j as ke}from"./index-BahdI_WD.js";import"./vendor-highlight-NXWAAyIv.js";const Z=[{cssProp:"color",label:"文本颜色"},{cssProp:"background-color",label:"背景"},{cssProp:"font-size",label:"字号"},{cssProp:"font-weight",label:"字重"},{cssProp:"padding",label:"内边距"},{cssProp:"border-radius",label:"圆角"}];function we(d){var i;const c=(i=d.ownerDocument.defaultView)==null?void 0:i.getComputedStyle(d),o={};for(const{cssProp:f}of Z)o[f]=(c==null?void 0:c.getPropertyValue(f).trim())??"";return o}function $e(d){const c=/^\s*(\d{1,5})(.*)$/.exec(d);if(!c)return null;const o=c[1];if(Number(o)<1||Number(o)>65535)return null;let i=c[2]||"/";return i.startsWith("/")||(i=`/${i}`),{port:o,path:i}}function Ce(d){if(d.id)return`#${CSS.escape(d.id)}`;const c=[];let o=d,i=0;for(;o&&o.nodeType===1&&i<4;){let f=o.tagName.toLowerCase();const P=(o.getAttribute("class")||"").trim().split(/\s+/).filter(Boolean).slice(0,2).map(m=>`.${CSS.escape(m)}`).join("");f+=P;const h=o.parentElement;if(h){const m=[...h.children].filter(y=>y.tagName===o.tagName);m.length>1&&(f+=`:nth-of-type(${m.indexOf(o)+1})`)}if(c.unshift(f),o.id){c[0]=`#${CSS.escape(o.id)}`;break}o=h,i+=1}return c.join(" > ")}function Me({gatewayUrl:d,sessionId:c,initialAuthToken:o,defaultPort:i,isMobile:f,onClose:P,onAnnotate:h}){const[m,y]=r.useState(i||"3000"),[u,ee]=r.useState(null),[R,te]=r.useState(o??null),[E,T]=r.useState(!1),[M,H]=r.useState("desktop"),[ne,U]=r.useState(0),[I,N]=r.useState(!1),[K,z]=r.useState(null),[v,j]=r.useState(null),[l,L]=r.useState(null),[W,k]=r.useState(""),[w,B]=r.useState(!1),[$,D]=r.useState({}),[C,S]=r.useState({}),A=r.useRef(null),V=r.useRef(null),O=r.useMemo(()=>me(),[]);r.useEffect(()=>{let t=!1;return T(!1),he().then(n=>{t||te((n==null?void 0:n.accessToken)??null)}).finally(()=>{t||T(!0)}),()=>{t=!0}},[o]);const b=r.useMemo(()=>{if(!u||o&&!E)return null;const t=`${d.replace(/\/+$/,"")}/tunnel/${encodeURIComponent(c)}/${u.port}${u.path}`,n=[];if(O&&n.push(`token=${encodeURIComponent(O)}`),R&&n.push(`auth_token=${encodeURIComponent(R)}`),n.length===0)return t;const s=u.path.includes("?")?"&":"?";return`${t}${s}${n.join("&")}`},[u,d,c,O,R,E]),_=()=>{const t=$e(m);t&&(ee(t),U(n=>n+1))},se=()=>U(t=>t+1),oe=()=>{var t;if(u)try{const n=(t=A.current)==null?void 0:t.contentWindow;if(!n)return;const s=`/tunnel/${encodeURIComponent(c)}/${u.port}`;let x=n.location.pathname;x.startsWith(s)&&(x=x.slice(s.length)||"/");const p=new URLSearchParams(n.location.search);p.delete("token"),p.delete("auth_token");const g=p.toString();y(`${u.port}${x}${g?`?${g}`:""}`)}catch{}},F=(t,n)=>{const s=A.current,x=s==null?void 0:s.contentDocument;if(!s||!x)return null;const p=s.getBoundingClientRect();return x.elementFromPoint(t-p.left,n-p.top)},X=t=>{const n=t.getBoundingClientRect();return{top:n.top,left:n.left,width:n.width,height:n.height}},re=t=>{try{const n=F(t.clientX,t.clientY);j(n?X(n):null)}catch{j(null),z("此页面跨域,无法读取元素(生产环境同源可用)"),N(!1)}},le=t=>{t.preventDefault();try{const n=F(t.clientX,t.clientY);if(!n||!("style"in n))return;const s=n;L({el:s,selector:Ce(s),text:(s.textContent||"").replace(/\s+/g," ").trim().slice(0,120),rect:X(s)}),D(we(s)),S({}),k("")}catch{z("此页面跨域,无法读取元素(生产环境同源可用)"),N(!1)}},ce=(t,n)=>{if(l){try{l.el.style.setProperty(t,n)}catch{}S(s=>({...s,[t]:n}))}},ae=()=>Object.entries(C).filter(([t,n])=>n!==($[t]??"")).map(([t,n])=>({cssProp:t,from:$[t]??"",to:n})),Y=()=>{if(!l)return;const t=ae(),n=["关于预览页面中的这个元素:",`- 选择器: \`${l.selector}\``,l.text?`- 文本: "${l.text}"`:"",`- 路径: localhost:${m}`,W.trim()?`- 批注: ${W.trim()}`:""];if(t.length>0){n.push("- 请把以下样式改动应用到源码:");for(const s of t)n.push(` - ${s.cssProp}: ${s.from||"(默认)"} → ${s.to}`)}h==null||h(n.filter(Boolean).join(`
1
+ import{r,j as e}from"./vendor-react-M7xG3qiw.js";import{e as me,g as he,a as be,c as ge,d as ve,f as je,h as ye,i as Ne,j as ke}from"./index-q85psYZ5.js";import"./vendor-highlight-NXWAAyIv.js";const Z=[{cssProp:"color",label:"文本颜色"},{cssProp:"background-color",label:"背景"},{cssProp:"font-size",label:"字号"},{cssProp:"font-weight",label:"字重"},{cssProp:"padding",label:"内边距"},{cssProp:"border-radius",label:"圆角"}];function we(d){var i;const c=(i=d.ownerDocument.defaultView)==null?void 0:i.getComputedStyle(d),o={};for(const{cssProp:f}of Z)o[f]=(c==null?void 0:c.getPropertyValue(f).trim())??"";return o}function $e(d){const c=/^\s*(\d{1,5})(.*)$/.exec(d);if(!c)return null;const o=c[1];if(Number(o)<1||Number(o)>65535)return null;let i=c[2]||"/";return i.startsWith("/")||(i=`/${i}`),{port:o,path:i}}function Ce(d){if(d.id)return`#${CSS.escape(d.id)}`;const c=[];let o=d,i=0;for(;o&&o.nodeType===1&&i<4;){let f=o.tagName.toLowerCase();const P=(o.getAttribute("class")||"").trim().split(/\s+/).filter(Boolean).slice(0,2).map(m=>`.${CSS.escape(m)}`).join("");f+=P;const h=o.parentElement;if(h){const m=[...h.children].filter(y=>y.tagName===o.tagName);m.length>1&&(f+=`:nth-of-type(${m.indexOf(o)+1})`)}if(c.unshift(f),o.id){c[0]=`#${CSS.escape(o.id)}`;break}o=h,i+=1}return c.join(" > ")}function Me({gatewayUrl:d,sessionId:c,initialAuthToken:o,defaultPort:i,isMobile:f,onClose:P,onAnnotate:h}){const[m,y]=r.useState(i||"3000"),[u,ee]=r.useState(null),[R,te]=r.useState(o??null),[E,T]=r.useState(!1),[M,H]=r.useState("desktop"),[ne,U]=r.useState(0),[I,N]=r.useState(!1),[K,z]=r.useState(null),[v,j]=r.useState(null),[l,L]=r.useState(null),[W,k]=r.useState(""),[w,B]=r.useState(!1),[$,D]=r.useState({}),[C,S]=r.useState({}),A=r.useRef(null),V=r.useRef(null),O=r.useMemo(()=>me(),[]);r.useEffect(()=>{let t=!1;return T(!1),he().then(n=>{t||te((n==null?void 0:n.accessToken)??null)}).finally(()=>{t||T(!0)}),()=>{t=!0}},[o]);const b=r.useMemo(()=>{if(!u||o&&!E)return null;const t=`${d.replace(/\/+$/,"")}/tunnel/${encodeURIComponent(c)}/${u.port}${u.path}`,n=[];if(O&&n.push(`token=${encodeURIComponent(O)}`),R&&n.push(`auth_token=${encodeURIComponent(R)}`),n.length===0)return t;const s=u.path.includes("?")?"&":"?";return`${t}${s}${n.join("&")}`},[u,d,c,O,R,E]),_=()=>{const t=$e(m);t&&(ee(t),U(n=>n+1))},se=()=>U(t=>t+1),oe=()=>{var t;if(u)try{const n=(t=A.current)==null?void 0:t.contentWindow;if(!n)return;const s=`/tunnel/${encodeURIComponent(c)}/${u.port}`;let x=n.location.pathname;x.startsWith(s)&&(x=x.slice(s.length)||"/");const p=new URLSearchParams(n.location.search);p.delete("token"),p.delete("auth_token");const g=p.toString();y(`${u.port}${x}${g?`?${g}`:""}`)}catch{}},F=(t,n)=>{const s=A.current,x=s==null?void 0:s.contentDocument;if(!s||!x)return null;const p=s.getBoundingClientRect();return x.elementFromPoint(t-p.left,n-p.top)},X=t=>{const n=t.getBoundingClientRect();return{top:n.top,left:n.left,width:n.width,height:n.height}},re=t=>{try{const n=F(t.clientX,t.clientY);j(n?X(n):null)}catch{j(null),z("此页面跨域,无法读取元素(生产环境同源可用)"),N(!1)}},le=t=>{t.preventDefault();try{const n=F(t.clientX,t.clientY);if(!n||!("style"in n))return;const s=n;L({el:s,selector:Ce(s),text:(s.textContent||"").replace(/\s+/g," ").trim().slice(0,120),rect:X(s)}),D(we(s)),S({}),k("")}catch{z("此页面跨域,无法读取元素(生产环境同源可用)"),N(!1)}},ce=(t,n)=>{if(l){try{l.el.style.setProperty(t,n)}catch{}S(s=>({...s,[t]:n}))}},ae=()=>Object.entries(C).filter(([t,n])=>n!==($[t]??"")).map(([t,n])=>({cssProp:t,from:$[t]??"",to:n})),Y=()=>{if(!l)return;const t=ae(),n=["关于预览页面中的这个元素:",`- 选择器: \`${l.selector}\``,l.text?`- 文本: "${l.text}"`:"",`- 路径: localhost:${m}`,W.trim()?`- 批注: ${W.trim()}`:""];if(t.length>0){n.push("- 请把以下样式改动应用到源码:");for(const s of t)n.push(` - ${s.cssProp}: ${s.from||"(默认)"} → ${s.to}`)}h==null||h(n.filter(Boolean).join(`
2
2
  `)),L(null),k(""),D({}),S({}),B(!1),j(null),N(!1)},q=()=>{L(null),k(""),D({}),S({}),B(!1)},ie=()=>{z(null),q(),j(null),N(t=>!t)};return e.jsxs("div",{className:"flex h-full flex-col bg-canvas",children:[e.jsxs("div",{className:"flex items-center gap-1.5 border-b border-border bg-surface px-2 py-1.5",children:[f&&e.jsx("button",{onClick:P,className:"codex-btn-ghost px-2 py-1.5","aria-label":"关闭预览",children:e.jsx(be,{size:15})}),e.jsxs("div",{className:"flex min-w-0 flex-1 items-center gap-1 rounded-lg border border-border bg-surface-raised px-2 py-1",children:[e.jsx("span",{className:"shrink-0 font-mono text-2xs text-content-faint",children:"localhost:"}),e.jsx("input",{value:m,onChange:t=>y(t.target.value),onKeyDown:t=>t.key==="Enter"&&_(),placeholder:"3000",spellCheck:!1,className:"min-w-0 flex-1 bg-transparent font-mono text-2xs text-content-primary outline-none","aria-label":"端口与路径"}),e.jsx("button",{onClick:_,className:"shrink-0 cursor-pointer rounded bg-accent-dim px-2 py-0.5 text-2xs font-medium text-white transition-colors hover:bg-accent",children:"前往"})]}),e.jsx("button",{onClick:ie,disabled:!u,className:`shrink-0 cursor-pointer rounded-md p-1.5 transition-colors disabled:opacity-40 ${I?"bg-accent-dim text-white":"text-content-muted hover:bg-surface-overlay hover:text-accent"}`,title:"标注元素","aria-label":"标注元素",children:e.jsx(ge,{size:14})}),e.jsxs("div",{className:"flex shrink-0 items-center rounded-lg border border-border bg-surface-raised p-0.5",children:[e.jsx("button",{onClick:()=>H("desktop"),className:`cursor-pointer rounded p-1 transition-colors ${M==="desktop"?"bg-surface-overlay text-content-primary":"text-content-muted hover:text-content-primary"}`,title:"桌面视口","aria-label":"桌面视口",children:e.jsx(ve,{size:13})}),e.jsx("button",{onClick:()=>H("mobile"),className:`cursor-pointer rounded p-1 transition-colors ${M==="mobile"?"bg-surface-overlay text-content-primary":"text-content-muted hover:text-content-primary"}`,title:"移动视口","aria-label":"移动视口",children:e.jsx(je,{size:13})})]}),e.jsx("button",{onClick:se,disabled:!u,className:"shrink-0 cursor-pointer rounded-md p-1.5 text-content-muted transition-colors hover:bg-surface-overlay hover:text-accent disabled:opacity-40",title:"刷新","aria-label":"刷新",children:e.jsx(ye,{size:14})}),e.jsx("button",{onClick:()=>b&&window.open(b,"_blank","noopener,noreferrer"),disabled:!b,className:"shrink-0 cursor-pointer rounded-md p-1.5 text-content-muted transition-colors hover:bg-surface-overlay hover:text-accent disabled:opacity-40",title:"在新窗口打开","aria-label":"在新窗口打开",children:e.jsx(Ne,{size:14})})]}),K&&e.jsx("p",{className:"border-b border-border bg-warning/10 px-3 py-1.5 text-2xs text-warning",children:K}),I&&!l&&e.jsx("p",{className:"border-b border-border bg-accent/10 px-3 py-1.5 text-2xs text-accent",children:"标注模式:移动鼠标高亮元素,点击选择并添加批注。"}),e.jsx("div",{className:"min-h-0 flex-1 overflow-auto bg-surface-raised",children:b?e.jsx("div",{className:M==="mobile"?"mx-auto h-full w-[390px] max-w-full border-x border-border":"h-full w-full",children:e.jsxs("div",{className:"relative h-full w-full",children:[e.jsx("iframe",{ref:A,src:b,title:"端口预览",onLoad:oe,className:"h-full w-full bg-white"},`${b}-${ne}`),I&&e.jsx("div",{className:"absolute inset-0 cursor-crosshair",onMouseMove:re,onMouseLeave:()=>j(null),onClick:le,children:v&&!l&&e.jsx("div",{className:"pointer-events-none absolute border-2 border-accent bg-accent/10",style:{top:v.top,left:v.left,width:v.width,height:v.height}})}),l&&(()=>{var J,Q;const s=((J=V.current)==null?void 0:J.clientHeight)??600,x=l.rect.top+l.rect.height+8,p=l.rect.top-8,g=w?340:140,G=x+g>s&&p-g>0?Math.max(4,p-g):x,de=Math.max(4,Math.min(l.rect.left,(((Q=V.current)==null?void 0:Q.clientWidth)??800)-280-4));return e.jsxs("div",{className:"absolute z-20 flex flex-col gap-2 rounded-xl border border-accent-dim/40 bg-surface-raised px-3 py-2.5 shadow-2xl animate-fade-in",style:{top:G,left:de,width:280,maxHeight:s-G-8},children:[e.jsx("p",{className:"truncate font-mono text-2xs text-content-faint",title:l.selector,children:l.selector}),l.text&&e.jsxs("p",{className:"-mt-1 truncate text-2xs text-content-muted",children:['"',l.text,'"']}),e.jsx("input",{value:W,onChange:a=>k(a.target.value),onKeyDown:a=>a.key==="Enter"&&Y(),autoFocus:!0,placeholder:"添加批注…",className:"w-full rounded-lg border border-border bg-surface px-2.5 py-1.5 text-xs text-content-primary placeholder-content-muted outline-none focus:border-accent-dim"}),e.jsxs("button",{onClick:()=>B(a=>!a),className:`flex cursor-pointer items-center gap-1 self-start rounded-md px-1.5 py-0.5 text-2xs transition-colors ${w?"text-accent":"text-content-muted hover:text-content-secondary"}`,children:[e.jsx("svg",{width:10,height:10,viewBox:"0 0 10 10",className:`shrink-0 transition-transform ${w?"rotate-180":""}`,children:e.jsx("path",{d:"M2 3.5 L5 6.5 L8 3.5",fill:"none",stroke:"currentColor",strokeWidth:"1.4",strokeLinecap:"round",strokeLinejoin:"round"})}),"调整样式"]}),w&&e.jsx("div",{className:"grid grid-cols-2 gap-x-2 gap-y-1.5 animate-fade-in",children:Z.map(({cssProp:a,label:ue})=>{const xe=C[a]??$[a]??"",pe=a in C&&C[a]!==($[a]??"");return e.jsxs("label",{className:"flex flex-col gap-0.5",children:[e.jsx("span",{className:`text-2xs ${pe?"text-accent":"text-content-faint"}`,children:ue}),e.jsx("input",{value:xe,onChange:fe=>ce(a,fe.target.value),spellCheck:!1,className:"w-full rounded-md border border-border bg-surface px-2 py-1 font-mono text-2xs text-content-primary outline-none focus:border-accent-dim"})]},a)})}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("button",{onClick:q,className:"codex-btn-ghost flex-1 text-2xs",children:"取消"}),e.jsx("button",{onClick:Y,className:"codex-btn-primary flex-1 text-2xs",children:"发送给 Agent"})]})]})})()]})}):e.jsxs("div",{className:"flex h-full flex-col items-center justify-center gap-3 px-6 text-center",children:[e.jsx(ke,{size:34,className:"text-content-faint"}),e.jsx("p",{className:"text-sm leading-6 text-content-muted",children:u&&!E?"正在准备预览认证...":"输入主机上服务的端口号,预览远程本地服务"})]})})]})}export{Me as PortPreview};
@@ -1 +1 @@
1
- import{r as l,j as i}from"./vendor-react-M7xG3qiw.js";import{x as w,a as C}from"./vendor-xterm-D15THcI4.js";import{I as T,a as A,b as B}from"./index-BahdI_WD.js";import"./vendor-highlight-NXWAAyIv.js";function x(e,t){const r=getComputedStyle(document.documentElement).getPropertyValue(e).trim();return r?`rgb(${r})`:t}function $(e,t,r){const u=getComputedStyle(document.documentElement).getPropertyValue(e).trim();return u?`rgb(${u} / ${t})`:r}function E(){return{background:x("--c-canvas","#0b0d0f"),foreground:x("--c-content-primary","#e6e8eb"),cursor:x("--c-accent","#2dd4bf"),selectionBackground:$("--c-accent",.22,"rgba(45,212,191,0.25)"),black:x("--c-content-faint","#4e535a"),brightBlack:x("--c-content-muted","#70767e"),white:x("--c-content-secondary","#9ea5ad"),brightWhite:x("--c-content-primary","#e9ebee")}}function F(e,t){const r=l.useRef(null),u=l.useRef(null),h=l.useRef(null);return l.useEffect(()=>{if(!r.current||!e)return;const n=new w.Terminal({fontFamily:"JetBrains Mono, ui-monospace, monospace",fontSize:13,cursorBlink:!0,theme:E(),scrollback:5e3}),f=new C.FitAddon;n.loadAddon(f),n.open(r.current),f.fit(),u.current=n,h.current=f;const p=e.terminalBuffer(t);p&&n.write(p);const v=n.onData(c=>e.sendInput(t,c)),b=n.onResize(({cols:c,rows:o})=>e.sendResize(t,c,o)),k=e.onEvent(c=>{if(c.type==="terminal.reset"&&c.terminalId===t){n.reset();return}c.type==="terminal.output"&&c.terminalId===t&&n.write(c.data)}),y=new ResizeObserver(()=>{try{f.fit(),e.sendResize(t,n.cols,n.rows)}catch{}});y.observe(r.current);const s=new MutationObserver(()=>{n.options.theme=E()});return s.observe(document.documentElement,{attributes:!0,attributeFilter:["data-theme"]}),e.sendResize(t,n.cols,n.rows),()=>{v.dispose(),b.dispose(),k(),y.disconnect(),s.disconnect(),n.dispose(),u.current=null,h.current=null}},[e,t]),{containerRef:r}}function I(e,t){return e==="default"?"默认":t||e.slice(0,8)}const z=e=>`linkshell:web:lastTerminal:${e}`;function L(e){if(!e)return null;try{return localStorage.getItem(z(e))}catch{return null}}function P(e,t){if(e)try{localStorage.setItem(z(e),t)}catch{}}function O(e,t){if(e.length===0)return null;if(t&&e.includes(t))return t;const r=e.filter(u=>u!=="default");return r.length>0?r[r.length-1]:e[0]}function V({bridge:e,terminalId:t}){const{containerRef:r}=F(e,t);return i.jsx("div",{ref:r,className:"xterm-host min-h-0 flex-1 bg-canvas p-2"})}function N({bridge:e,onNewTerminal:t}){const r=e==null?void 0:e.sessionId,[u,h]=l.useState([]),[n,f]=l.useState("default"),p=l.useRef(!1),v=l.useRef(!1),b=l.useCallback((s,c)=>{h(o=>o.some(m=>m.id===s)?o:[...o,{id:s,label:I(s,c)}])},[]);l.useEffect(()=>{if(!e)return;p.current=!1,v.current=!1;const s=L(r);return e.requestTerminalList(),e.onEvent(o=>{if(o.type==="terminal.output"){b(o.terminalId);return}if(o.type==="terminal.list"){const m=o.envelope,a=m.payload;if(m.type==="terminal.spawned"){const R=a==null?void 0:a.terminalId;R&&(b(R,a==null?void 0:a.projectName),f(R));return}if(m.type==="terminal.list"){const j=(Array.isArray(a==null?void 0:a.terminals)?a.terminals:[]).filter(d=>typeof d.terminalId=="string"&&d.status!=="exited").map(d=>({id:d.terminalId,label:I(d.terminalId,d.projectName)}));if(h(j),!p.current){p.current=!0;const d=j.map(S=>S.id),g=O(d,s);g?f(g):v.current||(v.current=!0,t==null||t())}}}})},[e,b,t]),l.useEffect(()=>{p.current&&n&&P(r,n)},[r,n]);const k=()=>t==null?void 0:t(),y=s=>{e==null||e.killTerminal(s),h(c=>{var m;const o=c.filter(a=>a.id!==s);return s===n&&f(((m=o[o.length-1])==null?void 0:m.id)??"default"),o})};return i.jsxs("div",{className:"flex h-full flex-col bg-canvas",children:[i.jsxs("div",{className:"flex items-center gap-0.5 border-b border-border bg-surface-raised px-1.5 py-1",children:[i.jsx(T,{size:13,className:"mx-1 shrink-0 text-content-faint"}),i.jsx("div",{className:"flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto",children:u.map(s=>i.jsxs("div",{className:`group flex shrink-0 items-center gap-1 rounded-md px-2 py-1 text-2xs transition-colors ${s.id===n?"bg-surface-overlay text-content-primary":"text-content-muted hover:bg-surface-overlay/60"}`,children:[i.jsx("button",{onClick:()=>f(s.id),className:"cursor-pointer font-mono",children:s.label}),u.length>1&&i.jsx("button",{onClick:()=>y(s.id),className:"cursor-pointer rounded text-content-faint opacity-0 transition hover:text-danger group-hover:opacity-100","aria-label":"关闭终端",children:i.jsx(A,{size:11})})]},s.id))}),i.jsx("button",{onClick:k,className:"ml-1 shrink-0 cursor-pointer rounded-md p-1 text-content-muted transition-colors hover:bg-surface-overlay hover:text-accent",title:"新建终端","aria-label":"新建终端",children:i.jsx(B,{size:14})})]}),i.jsx(V,{bridge:e,terminalId:n},n)]})}export{N as TerminalPanel};
1
+ import{r as l,j as i}from"./vendor-react-M7xG3qiw.js";import{x as w,a as C}from"./vendor-xterm-D15THcI4.js";import{I as T,a as A,b as B}from"./index-q85psYZ5.js";import"./vendor-highlight-NXWAAyIv.js";function x(e,t){const r=getComputedStyle(document.documentElement).getPropertyValue(e).trim();return r?`rgb(${r})`:t}function $(e,t,r){const u=getComputedStyle(document.documentElement).getPropertyValue(e).trim();return u?`rgb(${u} / ${t})`:r}function E(){return{background:x("--c-canvas","#0b0d0f"),foreground:x("--c-content-primary","#e6e8eb"),cursor:x("--c-accent","#2dd4bf"),selectionBackground:$("--c-accent",.22,"rgba(45,212,191,0.25)"),black:x("--c-content-faint","#4e535a"),brightBlack:x("--c-content-muted","#70767e"),white:x("--c-content-secondary","#9ea5ad"),brightWhite:x("--c-content-primary","#e9ebee")}}function F(e,t){const r=l.useRef(null),u=l.useRef(null),h=l.useRef(null);return l.useEffect(()=>{if(!r.current||!e)return;const n=new w.Terminal({fontFamily:"JetBrains Mono, ui-monospace, monospace",fontSize:13,cursorBlink:!0,theme:E(),scrollback:5e3}),f=new C.FitAddon;n.loadAddon(f),n.open(r.current),f.fit(),u.current=n,h.current=f;const p=e.terminalBuffer(t);p&&n.write(p);const v=n.onData(c=>e.sendInput(t,c)),b=n.onResize(({cols:c,rows:o})=>e.sendResize(t,c,o)),k=e.onEvent(c=>{if(c.type==="terminal.reset"&&c.terminalId===t){n.reset();return}c.type==="terminal.output"&&c.terminalId===t&&n.write(c.data)}),y=new ResizeObserver(()=>{try{f.fit(),e.sendResize(t,n.cols,n.rows)}catch{}});y.observe(r.current);const s=new MutationObserver(()=>{n.options.theme=E()});return s.observe(document.documentElement,{attributes:!0,attributeFilter:["data-theme"]}),e.sendResize(t,n.cols,n.rows),()=>{v.dispose(),b.dispose(),k(),y.disconnect(),s.disconnect(),n.dispose(),u.current=null,h.current=null}},[e,t]),{containerRef:r}}function I(e,t){return e==="default"?"默认":t||e.slice(0,8)}const z=e=>`linkshell:web:lastTerminal:${e}`;function L(e){if(!e)return null;try{return localStorage.getItem(z(e))}catch{return null}}function P(e,t){if(e)try{localStorage.setItem(z(e),t)}catch{}}function O(e,t){if(e.length===0)return null;if(t&&e.includes(t))return t;const r=e.filter(u=>u!=="default");return r.length>0?r[r.length-1]:e[0]}function V({bridge:e,terminalId:t}){const{containerRef:r}=F(e,t);return i.jsx("div",{ref:r,className:"xterm-host min-h-0 flex-1 bg-canvas p-2"})}function N({bridge:e,onNewTerminal:t}){const r=e==null?void 0:e.sessionId,[u,h]=l.useState([]),[n,f]=l.useState("default"),p=l.useRef(!1),v=l.useRef(!1),b=l.useCallback((s,c)=>{h(o=>o.some(m=>m.id===s)?o:[...o,{id:s,label:I(s,c)}])},[]);l.useEffect(()=>{if(!e)return;p.current=!1,v.current=!1;const s=L(r);return e.requestTerminalList(),e.onEvent(o=>{if(o.type==="terminal.output"){b(o.terminalId);return}if(o.type==="terminal.list"){const m=o.envelope,a=m.payload;if(m.type==="terminal.spawned"){const R=a==null?void 0:a.terminalId;R&&(b(R,a==null?void 0:a.projectName),f(R));return}if(m.type==="terminal.list"){const j=(Array.isArray(a==null?void 0:a.terminals)?a.terminals:[]).filter(d=>typeof d.terminalId=="string"&&d.status!=="exited").map(d=>({id:d.terminalId,label:I(d.terminalId,d.projectName)}));if(h(j),!p.current){p.current=!0;const d=j.map(S=>S.id),g=O(d,s);g?f(g):v.current||(v.current=!0,t==null||t())}}}})},[e,b,t]),l.useEffect(()=>{p.current&&n&&P(r,n)},[r,n]);const k=()=>t==null?void 0:t(),y=s=>{e==null||e.killTerminal(s),h(c=>{var m;const o=c.filter(a=>a.id!==s);return s===n&&f(((m=o[o.length-1])==null?void 0:m.id)??"default"),o})};return i.jsxs("div",{className:"flex h-full flex-col bg-canvas",children:[i.jsxs("div",{className:"flex items-center gap-0.5 border-b border-border bg-surface-raised px-1.5 py-1",children:[i.jsx(T,{size:13,className:"mx-1 shrink-0 text-content-faint"}),i.jsx("div",{className:"flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto",children:u.map(s=>i.jsxs("div",{className:`group flex shrink-0 items-center gap-1 rounded-md px-2 py-1 text-2xs transition-colors ${s.id===n?"bg-surface-overlay text-content-primary":"text-content-muted hover:bg-surface-overlay/60"}`,children:[i.jsx("button",{onClick:()=>f(s.id),className:"cursor-pointer font-mono",children:s.label}),u.length>1&&i.jsx("button",{onClick:()=>y(s.id),className:"cursor-pointer rounded text-content-faint opacity-0 transition hover:text-danger group-hover:opacity-100","aria-label":"关闭终端",children:i.jsx(A,{size:11})})]},s.id))}),i.jsx("button",{onClick:k,className:"ml-1 shrink-0 cursor-pointer rounded-md p-1 text-content-muted transition-colors hover:bg-surface-overlay hover:text-accent",title:"新建终端","aria-label":"新建终端",children:i.jsx(B,{size:14})})]}),i.jsx(V,{bridge:e,terminalId:n},n)]})}export{N as TerminalPanel};