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.
- package/dist/cli/src/runtime/acp/agent-process-discovery.d.ts +1 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.js +8 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +137 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +461 -64
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.js +9 -0
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +164 -8
- package/dist/shared-protocol/src/index.js +6 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/runtime/acp/agent-process-discovery.ts +6 -1
- package/src/runtime/acp/agent-workspace.ts +405 -18
- package/src/runtime/acp/provider-resolver.ts +10 -0
- package/web/assets/{FileBrowser-Db2LlqTw.js → FileBrowser-DtVanRUn.js} +1 -1
- package/web/assets/{PortPreview-Cobi7sn0.js → PortPreview-DC2-UmHo.js} +1 -1
- package/web/assets/{TerminalPanel-_Wve-C8s.js → TerminalPanel-D4gHM4Ta.js} +1 -1
- package/web/assets/{UsageDashboard-DmXct3sk.js → UsageDashboard-ZnvuvqWc.js} +1 -1
- package/web/assets/index-DKdJgZ9D.js +36 -0
- package/web/index.html +1 -1
- package/web/assets/index-pnfD2xom.js +0 -35
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
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
|
-
|
|
2275
|
-
private
|
|
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.
|
|
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
|
-
|
|
2481
|
-
|
|
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
|
|
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 (
|
|
2500
|
-
existing.agentSessionId =
|
|
2501
|
-
this.conversationByAgentSessionId.set(
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
|
3052
|
-
|
|
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 (
|
|
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-
|
|
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-
|
|
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};
|