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.
- package/dist/cli/src/commands/doctor.js +9 -3
- package/dist/cli/src/commands/doctor.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +5 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +318 -119
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/commands/doctor.ts +12 -3
- package/src/runtime/acp/agent-workspace.ts +311 -116
- package/web/assets/{FileBrowser-Daeji5i-.js → FileBrowser-wOAfY-xj.js} +1 -1
- package/web/assets/{PortPreview-6-Cj-cKd.js → PortPreview-Cbvv7yzA.js} +1 -1
- package/web/assets/{TerminalPanel-C3hvv9KU.js → TerminalPanel-CTSklkct.js} +1 -1
- package/web/assets/{UsageDashboard-D9Xlpl3d.js → UsageDashboard-CJ1AsNmd.js} +1 -1
- package/web/assets/index-q85psYZ5.js +36 -0
- package/web/index.html +1 -1
- package/web/assets/index-BahdI_WD.js +0 -36
|
@@ -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
|
|
2229
|
-
//
|
|
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:
|
|
2902
|
-
//
|
|
2903
|
-
//
|
|
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
|
-
|
|
2963
|
-
|
|
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
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
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
|
|
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 =
|
|
3634
|
+
const supportsImages = Boolean(client) && protocolSupportsImages(protocol, advertised);
|
|
3471
3635
|
const isClaudeFallback = protocol === "claude-stream-json";
|
|
3472
|
-
const supportsPermission =
|
|
3473
|
-
const supportsReasoningEffort =
|
|
3474
|
-
const
|
|
3475
|
-
const
|
|
3476
|
-
|
|
3477
|
-
|
|
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 =
|
|
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
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
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 =
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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};
|