linkshell-cli 0.5.38 → 0.5.39

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.
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs";
1
+ import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync, watch } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, join, relative } from "node:path";
4
4
  import {
@@ -249,11 +249,14 @@ interface AgentConversation {
249
249
  agentSessionId?: string;
250
250
  provider: AgentProvider;
251
251
  cwd: string;
252
+ workspaceRoots?: string[];
253
+ group?: string;
252
254
  title?: string;
253
255
  model?: string;
254
256
  reasoningEffort?: string;
255
257
  permissionMode?: AgentPermissionMode;
256
258
  collaborationMode?: AgentCollaborationMode;
259
+ control?: "owned" | "attached";
257
260
  status: AgentStatus;
258
261
  archived: boolean;
259
262
  lastMessagePreview?: string;
@@ -1675,6 +1678,226 @@ function codexSessionConfig(sessionId: string): {
1675
1678
  return cfg;
1676
1679
  }
1677
1680
 
1681
+ const CODEX_ROLLOUT_NAME =
1682
+ /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-(.+)\.jsonl$/;
1683
+
1684
+ function walkCodexRollouts(
1685
+ root: string,
1686
+ onFile: (path: string, filenameId: string, mtimeMs: number) => void,
1687
+ ): void {
1688
+ if (!existsSync(root)) return;
1689
+ const walk = (dir: string): void => {
1690
+ let entries: string[];
1691
+ try { entries = readdirSync(dir); } catch { return; }
1692
+ for (const name of entries) {
1693
+ const path = join(dir, name);
1694
+ let stat;
1695
+ try { stat = statSync(path); } catch { continue; }
1696
+ if (stat.isDirectory()) { walk(path); continue; }
1697
+ const match = CODEX_ROLLOUT_NAME.exec(name);
1698
+ if (match?.[1]) onFile(path, match[1], stat.mtimeMs);
1699
+ }
1700
+ };
1701
+ walk(root);
1702
+ }
1703
+
1704
+ function findCodexRolloutPath(sessionId: string, root = join(homedir(), ".codex", "sessions")): string | undefined {
1705
+ const byName: { path: string; mt: number }[] = [];
1706
+ const recent: { path: string; mt: number }[] = [];
1707
+ walkCodexRollouts(root, (path, filenameId, mt) => {
1708
+ recent.push({ path, mt });
1709
+ if (filenameId === sessionId) byName.push({ path, mt });
1710
+ });
1711
+ if (byName.length > 0) {
1712
+ byName.sort((a, b) => b.mt - a.mt);
1713
+ return byName[0]?.path;
1714
+ }
1715
+ recent.sort((a, b) => b.mt - a.mt);
1716
+ for (const candidate of recent.slice(0, 80)) {
1717
+ try {
1718
+ const fdText = readFileSync(candidate.path, "utf8");
1719
+ const first = fdText.slice(0, fdText.indexOf("\n") >= 0 ? fdText.indexOf("\n") : fdText.length);
1720
+ const metaId = firstString(asRecord(asRecord(JSON.parse(first))?.payload), ["session_id", "id"]);
1721
+ if (metaId === sessionId) return candidate.path;
1722
+ } catch { /* skip */ }
1723
+ }
1724
+ return undefined;
1725
+ }
1726
+
1727
+ function findLatestCodexRolloutForCwd(cwd: string, root = join(homedir(), ".codex", "sessions")): { path: string; sessionId: string } | undefined {
1728
+ const wanted = cwd.replace(/\/+$/, "");
1729
+ const candidates: { path: string; sessionId: string; mt: number }[] = [];
1730
+ walkCodexRollouts(root, (path, filenameId, mt) => {
1731
+ candidates.push({ path, sessionId: filenameId, mt });
1732
+ });
1733
+ candidates.sort((a, b) => b.mt - a.mt);
1734
+ for (const candidate of candidates.slice(0, 40)) {
1735
+ try {
1736
+ const text = readFileSync(candidate.path, "utf8");
1737
+ const first = text.slice(0, text.indexOf("\n") >= 0 ? text.indexOf("\n") : text.length);
1738
+ const payload = asRecord(asRecord(JSON.parse(first))?.payload);
1739
+ const fileCwd = firstString(payload, ["cwd"])?.replace(/\/+$/, "");
1740
+ const metaId = firstString(payload, ["session_id", "id"]) ?? candidate.sessionId;
1741
+ if (fileCwd === wanted) return { path: candidate.path, sessionId: metaId };
1742
+ } catch { continue; }
1743
+ }
1744
+ return undefined;
1745
+ }
1746
+
1747
+ function publicWorkspaceRoots(roots: unknown): string[] | undefined {
1748
+ if (!Array.isArray(roots)) return undefined;
1749
+ const cleaned = roots
1750
+ .filter((root): root is string => typeof root === "string" && root.length > 0)
1751
+ .filter((root) => !root.includes("/.codex/visualizations/"));
1752
+ return cleaned.length > 0 ? cleaned : undefined;
1753
+ }
1754
+
1755
+ function isInjectedCodexUserText(text: string): boolean {
1756
+ const trimmed = text.trim();
1757
+ return (
1758
+ trimmed.startsWith("<app-context>") ||
1759
+ trimmed.startsWith("<recommended_plugins>") ||
1760
+ trimmed.startsWith("<skills_instructions>") ||
1761
+ trimmed.startsWith("# AGENTS.md") ||
1762
+ trimmed.startsWith("You are `/root`")
1763
+ );
1764
+ }
1765
+
1766
+ function textsFromCodexContent(content: unknown): string[] {
1767
+ if (typeof content === "string" && content.trim()) return [content];
1768
+ if (!Array.isArray(content)) return [];
1769
+ const texts: string[] = [];
1770
+ for (const block of content) {
1771
+ const rec = asRecord(block);
1772
+ const text = typeof rec?.text === "string" ? rec.text : undefined;
1773
+ if (text?.trim()) texts.push(text);
1774
+ }
1775
+ return texts;
1776
+ }
1777
+
1778
+ /** Incremental Codex jsonl parser so live TUI/desktop sessions can tail the file. */
1779
+ export class CodexRolloutParser {
1780
+ private toolNames = new Map<string, string>();
1781
+ private index = 0;
1782
+ workspaceRoots: string[] | undefined;
1783
+ sessionCwd: string | undefined;
1784
+
1785
+ consume(text: string, conversationId: string): AgentTimelineItem[] {
1786
+ const items: AgentTimelineItem[] = [];
1787
+ for (const line of text.split(/\r?\n/)) {
1788
+ if (!line.trim()) continue;
1789
+ const next = this.consumeLine(line, conversationId);
1790
+ if (next) items.push(next);
1791
+ }
1792
+ return items;
1793
+ }
1794
+
1795
+ private consumeLine(line: string, conversationId: string): AgentTimelineItem | undefined {
1796
+ let record: Record<string, unknown> | undefined;
1797
+ try { record = asRecord(JSON.parse(line)); } catch { return undefined; }
1798
+ if (!record) return undefined;
1799
+ const payload = asRecord(record.payload) ?? record;
1800
+ const recordType = firstString(record, ["type"]);
1801
+ const payloadType = firstString(payload, ["type"]);
1802
+ const roots = publicWorkspaceRoots(payload.workspace_roots ?? payload.workspaceRoots);
1803
+ if (roots) this.workspaceRoots = roots;
1804
+ const cwd = firstString(payload, ["cwd"]);
1805
+ if (cwd) this.sessionCwd = cwd;
1806
+ const createdAt = parseTimestamp(record.timestamp ?? payload.timestamp) ?? Date.now();
1807
+ if (recordType === "response_item" && payloadType === "message") {
1808
+ const roleRaw = firstString(payload, ["role"]);
1809
+ if (roleRaw !== "user" && roleRaw !== "assistant") return undefined;
1810
+ const texts = textsFromCodexContent(payload.content).filter((text) =>
1811
+ roleRaw === "assistant" || !isInjectedCodexUserText(text),
1812
+ );
1813
+ if (texts.length === 0) return undefined;
1814
+ const text = texts.join("\n\n");
1815
+ this.index += 1;
1816
+ return {
1817
+ id: firstString(payload, ["id"]) ?? id(`codex-msg-${this.index}`),
1818
+ conversationId,
1819
+ type: "message",
1820
+ kind: "chat",
1821
+ role: roleRaw,
1822
+ text,
1823
+ content: [{ type: "text", text }],
1824
+ createdAt,
1825
+ updatedAt: createdAt,
1826
+ };
1827
+ }
1828
+ if (recordType === "response_item" && payloadType === "reasoning") {
1829
+ const texts = textsFromCodexContent(payload.content ?? payload.summary);
1830
+ if (texts.length === 0) return undefined;
1831
+ this.index += 1;
1832
+ return {
1833
+ id: firstString(payload, ["id"]) ?? id(`codex-think-${this.index}`),
1834
+ conversationId,
1835
+ type: "status",
1836
+ kind: "thinking",
1837
+ text: texts.join("\n"),
1838
+ createdAt,
1839
+ updatedAt: createdAt,
1840
+ };
1841
+ }
1842
+ if (recordType === "response_item" && (payloadType === "custom_tool_call" || payloadType === "function_call")) {
1843
+ const callId = firstString(payload, ["call_id", "id"]) ?? id(`codex-tool-${this.index}`);
1844
+ const name = firstString(payload, ["name"]) ?? "tool";
1845
+ this.toolNames.set(callId, name);
1846
+ const input = firstString(payload, ["input", "arguments"]) ?? stringify(payload.input ?? payload.arguments ?? "");
1847
+ this.index += 1;
1848
+ return {
1849
+ id: `tool:${callId}`,
1850
+ conversationId,
1851
+ type: "tool_call",
1852
+ kind: name === "exec" || name.includes("command") ? "command_execution" : "tool_activity",
1853
+ itemId: callId,
1854
+ toolCall: {
1855
+ id: callId,
1856
+ name,
1857
+ input,
1858
+ createdAt,
1859
+ status: "running",
1860
+ },
1861
+ createdAt,
1862
+ updatedAt: createdAt,
1863
+ };
1864
+ }
1865
+ if (recordType === "response_item" && (payloadType === "custom_tool_call_output" || payloadType === "function_call_output")) {
1866
+ const callId = firstString(payload, ["call_id", "id"]);
1867
+ if (!callId) return undefined;
1868
+ const outputTexts = textsFromCodexContent(payload.output);
1869
+ const output = outputTexts.join("\n") || stringify(payload.output ?? "");
1870
+ this.index += 1;
1871
+ return {
1872
+ id: `tool:${callId}`,
1873
+ conversationId,
1874
+ type: "tool_call",
1875
+ kind: "tool_activity",
1876
+ itemId: callId,
1877
+ toolCall: {
1878
+ id: callId,
1879
+ name: this.toolNames.get(callId) ?? "tool",
1880
+ output,
1881
+ createdAt,
1882
+ status: "completed",
1883
+ },
1884
+ createdAt,
1885
+ updatedAt: createdAt,
1886
+ };
1887
+ }
1888
+ return undefined;
1889
+ }
1890
+ }
1891
+
1892
+ /** Parse a Codex on-disk rollout into timeline items. App-server thread/read
1893
+ * only sees threads it owns; desktop/TUI sessions live in jsonl. */
1894
+ export function timelineItemsFromCodexRolloutText(
1895
+ text: string,
1896
+ conversationId: string,
1897
+ ): AgentTimelineItem[] {
1898
+ return new CodexRolloutParser().consume(text, conversationId).slice(-MAX_TIMELINE_ITEMS);
1899
+ }
1900
+
1678
1901
  function parseRemoteSessions(value: unknown): Array<{
1679
1902
  id: string;
1680
1903
  cwd?: string;
@@ -2204,6 +2427,14 @@ export class AgentWorkspaceProxy {
2204
2427
  const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
2205
2428
  const conversation = this.conversations.get(payload.conversationId);
2206
2429
  if (!conversation) break;
2430
+ if (conversation.control === "attached" || this.attachedExternalCodex.has(conversation.id)) {
2431
+ this.rejectAgentAction(
2432
+ conversation,
2433
+ `${providerLabel(conversation.provider)} 这场会话在本机终端/桌面里运行。Web 无法停止它,请在那边操作。`,
2434
+ conversation.status,
2435
+ );
2436
+ break;
2437
+ }
2207
2438
  const turnId = this.currentTurnIds.get(payload.conversationId);
2208
2439
  if (this.protocolForProvider(conversation.provider) === "codex-app-server" && !turnId) {
2209
2440
  this.rejectAgentAction(
@@ -2271,8 +2502,18 @@ export class AgentWorkspaceProxy {
2271
2502
  private fsSyncRunning = false;
2272
2503
  private lastPushedConversationSignature: string | undefined;
2273
2504
  private lastHydrateAt = new Map<string, number>();
2274
- private static readonly FS_SYNC_THROTTLE_MS = 1500;
2275
- private static readonly EXTERNAL_HYDRATE_THROTTLE_MS = 2000;
2505
+ /** Session ids this LinkShell process created via app-server `newSession`. */
2506
+ private ownedCodexSessionIds = new Set<string>();
2507
+ /** Conversations attached to an external Codex TUI/desktop rollout (same file, not a copy). */
2508
+ private attachedExternalCodex = new Set<string>();
2509
+ private codexFollow = new Map<string, {
2510
+ path: string;
2511
+ offset: number;
2512
+ carry: string;
2513
+ parser: CodexRolloutParser;
2514
+ }>();
2515
+ private static readonly FS_SYNC_THROTTLE_MS = 400;
2516
+ private static readonly EXTERNAL_HYDRATE_THROTTLE_MS = 400;
2276
2517
 
2277
2518
  private startSessionWatchers(): void {
2278
2519
  const roots = [
@@ -2347,7 +2588,9 @@ export class AgentWorkspaceProxy {
2347
2588
  this.lastHydrateAt.set(conversation.id, now);
2348
2589
  try {
2349
2590
  await this.hydrateConversationFromProvider(conversation, undefined, { rememberActiveTurn: false });
2350
- this.sendSnapshot(conversation.id);
2591
+ if (this.followCodexRolloutDisk(conversation, true) > 0) {
2592
+ this.sendSnapshot(conversation.id);
2593
+ }
2351
2594
  } catch (error) {
2352
2595
  if (this.input.verbose) {
2353
2596
  process.stderr.write(`[agent:v2] external hydrate failed for ${conversation.id}: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -2477,11 +2720,17 @@ export class AgentWorkspaceProxy {
2477
2720
  seenKeys.add(key);
2478
2721
  this.processMissCounts.set(key, 0);
2479
2722
 
2480
- const existingId = snapshot.sessionId
2481
- ? this.conversationByAgentSessionId.get(snapshot.sessionId)
2723
+ let sessionId = snapshot.sessionId;
2724
+ if (!sessionId && snapshot.provider === "codex") {
2725
+ const matched = findLatestCodexRolloutForCwd(snapshot.cwd ?? this.input.cwd);
2726
+ if (matched) sessionId = matched.sessionId;
2727
+ }
2728
+ const existingId = sessionId
2729
+ ? this.conversationByAgentSessionId.get(sessionId)
2482
2730
  : undefined;
2483
- const liveId = liveConversationId(snapshot);
2484
- const conversationId = existingId ?? liveId;
2731
+ const liveId = liveConversationId({ ...snapshot, sessionId });
2732
+ const conversationId = existingId
2733
+ ?? (sessionId ? makeAgentV2RemoteConversationId(snapshot.provider, sessionId) : liveId);
2485
2734
  const existing = this.conversations.get(conversationId);
2486
2735
  const cwd = snapshot.cwd ?? existing?.cwd ?? this.input.cwd;
2487
2736
  const driving = this.currentTurnIds.has(conversationId);
@@ -2496,10 +2745,12 @@ export class AgentWorkspaceProxy {
2496
2745
  existing.status = status;
2497
2746
  existing.cwd = cwd;
2498
2747
  if (!existing.title) existing.title = agentProviderLabel(snapshot.provider);
2499
- if (snapshot.sessionId && !existing.agentSessionId) {
2500
- existing.agentSessionId = snapshot.sessionId;
2501
- this.conversationByAgentSessionId.set(snapshot.sessionId, existing.id);
2748
+ if (sessionId && !existing.agentSessionId) {
2749
+ existing.agentSessionId = sessionId;
2750
+ this.conversationByAgentSessionId.set(sessionId, existing.id);
2502
2751
  }
2752
+ this.attachedExternalCodex.add(existing.id);
2753
+ existing.control = "attached";
2503
2754
  if (changed) {
2504
2755
  listChanged = true;
2505
2756
  this.emitConversation(existing);
@@ -2510,7 +2761,7 @@ export class AgentWorkspaceProxy {
2510
2761
  const now = Date.now();
2511
2762
  const conversation: AgentConversation = {
2512
2763
  id: conversationId,
2513
- agentSessionId: snapshot.sessionId,
2764
+ agentSessionId: sessionId,
2514
2765
  provider: snapshot.provider,
2515
2766
  cwd,
2516
2767
  title: agentProviderLabel(snapshot.provider),
@@ -2519,9 +2770,11 @@ export class AgentWorkspaceProxy {
2519
2770
  lastMessagePreview: "本机终端中的 Agent 进程",
2520
2771
  lastActivityAt: now,
2521
2772
  createdAt: now,
2773
+ control: "attached",
2522
2774
  };
2523
2775
  this.conversations.set(conversation.id, conversation);
2524
- if (snapshot.sessionId) this.conversationByAgentSessionId.set(snapshot.sessionId, conversation.id);
2776
+ if (sessionId) this.conversationByAgentSessionId.set(sessionId, conversation.id);
2777
+ this.attachedExternalCodex.add(conversation.id);
2525
2778
  this.emitConversation(conversation);
2526
2779
  listChanged = true;
2527
2780
  }
@@ -2893,6 +3146,8 @@ export class AgentWorkspaceProxy {
2893
3146
  lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
2894
3147
  createdAt: remote.createdAt ?? existing?.createdAt ?? now,
2895
3148
  usage,
3149
+ group: existing?.group ?? (provider === "claude" ? titleFromCwd(cwd) : existing?.group),
3150
+ workspaceRoots: existing?.workspaceRoots,
2896
3151
  };
2897
3152
  this.conversations.set(conversation.id, conversation);
2898
3153
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
@@ -3031,7 +3286,13 @@ export class AgentWorkspaceProxy {
3031
3286
  if (turnCount > 0) this.historyCursors.set(conversation.id, String(turnCount));
3032
3287
  }
3033
3288
 
3034
- const hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
3289
+ let hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
3290
+ if (conversation.provider === "codex") {
3291
+ this.followCodexRolloutDisk(conversation, false);
3292
+ }
3293
+ if (hydratedItems.length === 0) {
3294
+ hydratedItems = this.timelines.get(conversation.id) ?? [];
3295
+ }
3035
3296
  if (hydratedItems.length === 0) return;
3036
3297
  const existing = this.timelines.get(conversation.id) ?? [];
3037
3298
  const merged = new Map<string, AgentTimelineItem>();
@@ -3048,9 +3309,84 @@ export class AgentWorkspaceProxy {
3048
3309
  const lastPreview = [...nextItems].reverse()
3049
3310
  .map((item) => previewText(previewFromTimelineItem(item) ?? ""))
3050
3311
  .find(Boolean);
3051
- if (lastPreview && !conversation.lastMessagePreview) {
3052
- conversation.lastMessagePreview = lastPreview;
3312
+ if (lastPreview) conversation.lastMessagePreview = lastPreview;
3313
+ }
3314
+
3315
+ /** Tail a Codex desktop/TUI rollout. `emit` sends agent.v2.event for new lines
3316
+ * so a phone/web client sees tokens as the jsonl grows, not only on open. */
3317
+ private followCodexRolloutDisk(conversation: AgentConversation, emit: boolean): number {
3318
+ if (conversation.provider !== "codex") return 0;
3319
+ let path = conversation.agentSessionId
3320
+ ? findCodexRolloutPath(conversation.agentSessionId)
3321
+ : undefined;
3322
+ if (!path && conversation.cwd) {
3323
+ const matched = findLatestCodexRolloutForCwd(conversation.cwd);
3324
+ if (matched) {
3325
+ path = matched.path;
3326
+ if (!conversation.agentSessionId) {
3327
+ conversation.agentSessionId = matched.sessionId;
3328
+ this.conversationByAgentSessionId.set(matched.sessionId, conversation.id);
3329
+ }
3330
+ }
3331
+ }
3332
+ if (!path) return 0;
3333
+ let follow = this.codexFollow.get(conversation.id);
3334
+ if (!follow || follow.path !== path) {
3335
+ follow = { path, offset: 0, carry: "", parser: new CodexRolloutParser() };
3336
+ this.codexFollow.set(conversation.id, follow);
3337
+ }
3338
+ let stat;
3339
+ try {
3340
+ stat = statSync(path);
3341
+ } catch {
3342
+ return 0;
3343
+ }
3344
+ if (stat.size < follow.offset) {
3345
+ follow.offset = 0;
3346
+ follow.carry = "";
3347
+ follow.parser = new CodexRolloutParser();
3348
+ }
3349
+ if (stat.size === follow.offset && !follow.carry) return 0;
3350
+ const length = stat.size - follow.offset;
3351
+ const buf = Buffer.alloc(length);
3352
+ const fd = openSync(path, "r");
3353
+ try {
3354
+ readSync(fd, buf, 0, length, follow.offset);
3355
+ } finally {
3356
+ closeSync(fd);
3357
+ }
3358
+ follow.offset = stat.size;
3359
+ const combined = follow.carry + buf.toString("utf8");
3360
+ const lastNl = combined.lastIndexOf("\n");
3361
+ if (lastNl < 0) {
3362
+ follow.carry = combined;
3363
+ return 0;
3364
+ }
3365
+ const complete = combined.slice(0, lastNl + 1);
3366
+ follow.carry = combined.slice(lastNl + 1);
3367
+ const items = follow.parser.consume(complete, conversation.id);
3368
+ if (items.length === 0) return 0;
3369
+ if (emit) {
3370
+ for (const item of items) this.upsertItem(conversation.id, item);
3371
+ } else {
3372
+ const existing = this.timelines.get(conversation.id) ?? [];
3373
+ const merged = new Map(existing.map((item) => [item.id, item] as const));
3374
+ for (const item of items) merged.set(item.id, item);
3375
+ const nextItems = [...merged.values()]
3376
+ .sort((a, b) => a.createdAt - b.createdAt)
3377
+ .slice(-MAX_TIMELINE_ITEMS);
3378
+ this.timelines.set(conversation.id, nextItems);
3379
+ for (const item of nextItems) this.rememberItemConversationId(conversation.id, item);
3053
3380
  }
3381
+ const timeline = this.timelines.get(conversation.id) ?? [];
3382
+ const lastPreview = [...timeline].reverse()
3383
+ .map((item) => previewText(previewFromTimelineItem(item) ?? ""))
3384
+ .find(Boolean);
3385
+ if (lastPreview) conversation.lastMessagePreview = lastPreview;
3386
+ conversation.lastActivityAt = Date.now();
3387
+ if (follow.parser.workspaceRoots?.length) conversation.workspaceRoots = follow.parser.workspaceRoots;
3388
+ if (follow.parser.sessionCwd && !conversation.cwd) conversation.cwd = follow.parser.sessionCwd;
3389
+ return items.length;
3054
3390
  }
3055
3391
 
3056
3392
  private sendCapabilities(): void {
@@ -3299,7 +3635,46 @@ export class AgentWorkspaceProxy {
3299
3635
  }
3300
3636
  }
3301
3637
 
3302
- if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
3638
+ if (
3639
+ existingConversation &&
3640
+ !existingConversation.agentSessionId &&
3641
+ provider === "codex"
3642
+ ) {
3643
+ const matched = findLatestCodexRolloutForCwd(existingConversation.cwd ?? cwd);
3644
+ if (matched) {
3645
+ existingConversation.agentSessionId = matched.sessionId;
3646
+ this.conversationByAgentSessionId.set(matched.sessionId, existingConversation.id);
3647
+ }
3648
+ }
3649
+
3650
+ if (existingConversation && existingConversation.status !== "error") {
3651
+ const owned = Boolean(
3652
+ existingConversation.agentSessionId &&
3653
+ this.ownedCodexSessionIds.has(existingConversation.agentSessionId),
3654
+ );
3655
+ // External Codex (desktop/TUI) is the same session on disk. Attach by
3656
+ // tailing that rollout — never loadSession/newSession (those mint another thread).
3657
+ if (
3658
+ provider === "codex" &&
3659
+ !owned &&
3660
+ this.attachedExternalCodex.has(existingConversation.id) &&
3661
+ !this.currentTurnIds.has(existingConversation.id)
3662
+ ) {
3663
+ existingConversation.control = "attached";
3664
+ this.followCodexRolloutDisk(existingConversation, false);
3665
+ this.activeConversationId = existingConversation.id;
3666
+ this.input.send(createEnvelope({
3667
+ type: "agent.v2.conversation.opened",
3668
+ sessionId: this.input.sessionId,
3669
+ payload: {
3670
+ conversation: existingConversation,
3671
+ snapshot: this.timelines.get(existingConversation.id) ?? [],
3672
+ requestedConversationId: payload.conversationId,
3673
+ },
3674
+ }));
3675
+ return existingConversation;
3676
+ }
3677
+ if (existingConversation.agentSessionId) {
3303
3678
  const requestedCanonicalId = makeAgentV2RemoteConversationId(provider, existingConversation.agentSessionId);
3304
3679
  if (
3305
3680
  payload.conversationId &&
@@ -3342,6 +3717,7 @@ export class AgentWorkspaceProxy {
3342
3717
  if (this.input.verbose) {
3343
3718
  process.stderr.write(`[agent:v2] resume failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
3344
3719
  }
3720
+ await this.hydrateConversationFromProvider(existingConversation, undefined);
3345
3721
  }
3346
3722
  }
3347
3723
  this.activeConversationId = existingConversation.id;
@@ -3355,6 +3731,7 @@ export class AgentWorkspaceProxy {
3355
3731
  },
3356
3732
  }));
3357
3733
  return existingConversation;
3734
+ }
3358
3735
  }
3359
3736
 
3360
3737
  try {
@@ -3372,6 +3749,7 @@ export class AgentWorkspaceProxy {
3372
3749
  : await client.newSession({ cwd });
3373
3750
  }
3374
3751
  agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
3752
+ if (provider === "codex") this.ownedCodexSessionIds.add(agentSessionId);
3375
3753
  const now = Date.now();
3376
3754
  const conversationId = makeAgentV2RemoteConversationId(provider, agentSessionId);
3377
3755
  const conversation: AgentConversation = {
@@ -3381,6 +3759,7 @@ export class AgentWorkspaceProxy {
3381
3759
  provider,
3382
3760
  cwd,
3383
3761
  title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
3762
+ control: "owned",
3384
3763
  model: payload.model ?? existingConversation?.model,
3385
3764
  reasoningEffort: payload.reasoningEffort ?? existingConversation?.reasoningEffort,
3386
3765
  permissionMode: payload.permissionMode ?? existingConversation?.permissionMode,
@@ -3529,11 +3908,19 @@ export class AgentWorkspaceProxy {
3529
3908
  this.conversations.get(payload.conversationId) ??
3530
3909
  await this.openConversation({ conversationId: payload.conversationId });
3531
3910
  if (!conversation) return;
3911
+ if (conversation.control === "attached" || this.attachedExternalCodex.has(conversation.id)) {
3912
+ this.rejectAgentAction(
3913
+ conversation,
3914
+ `${providerLabel(conversation.provider)} 这场会话在本机终端/桌面里运行。Web 只旁观同一份记录,停止和插话请在那边操作。`,
3915
+ conversation.status,
3916
+ );
3917
+ return;
3918
+ }
3532
3919
  if (!conversation.agentSessionId) {
3533
3920
  this.rejectAgentAction(
3534
3921
  conversation,
3535
3922
  isLiveConversationId(conversation.id) || !isProtocolAgentProvider(conversation.provider)
3536
- ? `${providerLabel(conversation.provider)} 正在本机终端中运行。远程对话协议尚未接入,请打开终端面板继续。`
3923
+ ? `${providerLabel(conversation.provider)} 正在本机终端中运行。请打开终端面板继续。`
3537
3924
  : "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
3538
3925
  );
3539
3926
  return;
@@ -80,6 +80,15 @@ export function resolveAgentCommand(input: {
80
80
  };
81
81
  }
82
82
 
83
+ if (input.provider === "grok") {
84
+ return {
85
+ provider: "grok",
86
+ command: "grok agent stdio",
87
+ protocol: "acp",
88
+ framing: "newline",
89
+ };
90
+ }
91
+
83
92
  // copilot / opencode / kimi / custom: PTY-only unless caller passes --agent-command
84
93
  return null;
85
94
  }
@@ -131,6 +140,7 @@ const INSTALL_BINARIES: Array<{ provider: KnownAgentProvider; binaries: string[]
131
140
  { provider: "copilot", binaries: ["copilot"] },
132
141
  { provider: "opencode", binaries: ["opencode", "opencode-ai"] },
133
142
  { provider: "cursor", binaries: ["cursor-agent"] },
143
+ { provider: "grok", binaries: ["grok"] },
134
144
  { provider: "kimi", binaries: ["kimi"] },
135
145
  ];
136
146
 
@@ -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-pnfD2xom.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-DKdJgZ9D.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-pnfD2xom.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-DKdJgZ9D.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};