@co0ontty/wand 4.72.2 → 4.75.0-beta.g1d477a2
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/build-info.json +4 -4
- package/dist/claude-sdk-runner.d.ts +2 -3
- package/dist/config.d.ts +1 -1
- package/dist/config.js +48 -0
- package/dist/daemon-connection.d.ts +9 -0
- package/dist/daemon-connection.js +86 -0
- package/dist/distribution-manager.d.ts +5 -0
- package/dist/distribution-manager.js +79 -1
- package/dist/git-quick-commit.js +12 -0
- package/dist/message-truncator.js +9 -3
- package/dist/models.d.ts +39 -2
- package/dist/models.js +302 -9
- package/dist/native/darwin-arm64/wand-render +0 -0
- package/dist/native/darwin-arm64/wand-render.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-render.version +1 -0
- package/dist/native/darwin-arm64/wand-structured-renderd +0 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/darwin-x64/wand-render +0 -0
- package/dist/native/darwin-x64/wand-render.sha256 +1 -0
- package/dist/native/darwin-x64/wand-render.version +1 -0
- package/dist/native/darwin-x64/wand-structured-renderd +0 -0
- package/dist/native/darwin-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-x64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-arm64/wand-render +0 -0
- package/dist/native/linux-arm64/wand-render.sha256 +1 -0
- package/dist/native/linux-arm64/wand-render.version +1 -0
- package/dist/native/linux-arm64/wand-structured-renderd +0 -0
- package/dist/native/linux-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-x64/wand-render +0 -0
- package/dist/native/linux-x64/wand-render.sha256 +1 -0
- package/dist/native/linux-x64/wand-render.version +1 -0
- package/dist/native/linux-x64/wand-structured-renderd +0 -0
- package/dist/native/linux-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-x64/wand-structured-renderd.version +1 -0
- package/dist/process-manager.d.ts +12 -1
- package/dist/process-manager.js +88 -7
- package/dist/pty-terminal-state.d.ts +17 -0
- package/dist/pty-terminal-state.js +51 -1
- package/dist/render-binary.d.ts +61 -0
- package/dist/render-binary.js +204 -0
- package/dist/render-daemon-client.d.ts +202 -0
- package/dist/render-daemon-client.js +1164 -0
- package/dist/render-host.d.ts +88 -0
- package/dist/render-host.js +440 -0
- package/dist/render-protocol.d.ts +128 -0
- package/dist/render-protocol.js +99 -0
- package/dist/render-structured-client.d.ts +80 -0
- package/dist/render-structured-client.js +513 -0
- package/dist/render-structured-host.d.ts +31 -0
- package/dist/render-structured-host.js +218 -0
- package/dist/render-structured-protocol.d.ts +101 -0
- package/dist/render-structured-protocol.js +35 -0
- package/dist/server-session-routes.js +114 -12
- package/dist/server-task-routes.js +55 -6
- package/dist/server-update-routes.d.ts +2 -0
- package/dist/server-update-routes.js +44 -0
- package/dist/server.js +43 -13
- package/dist/session-ai-context.d.ts +1 -1
- package/dist/session-ai-context.js +15 -4
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +44 -1
- package/dist/session-transport.js +1 -1
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +14 -5
- package/dist/structured-client-protocol.d.ts +3 -1
- package/dist/structured-client-protocol.js +85 -25
- package/dist/structured-content.d.ts +20 -3
- package/dist/structured-content.js +80 -1
- package/dist/structured-exec-host.js +7 -1
- package/dist/structured-exec-pump.js +6 -2
- package/dist/structured-grok-adapter.js +22 -0
- package/dist/structured-pi-adapter.js +18 -2
- package/dist/structured-provider-common.d.ts +11 -9
- package/dist/structured-provider-common.js +60 -18
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +27 -3
- package/dist/task-types.d.ts +4 -1
- package/dist/terminal-daemon-client.d.ts +9 -2
- package/dist/terminal-daemon-client.js +119 -46
- package/dist/terminal-daemon-server.js +47 -8
- package/dist/terminal-host.d.ts +6 -0
- package/dist/terminal-host.js +3 -0
- package/dist/tui/commands.js +183 -10
- package/dist/tui/ipc-server.js +12 -0
- package/dist/types.d.ts +30 -10
- package/dist/unix-socket-keepalive.d.ts +3 -0
- package/dist/unix-socket-keepalive.js +94 -0
- package/dist/wand-task-sync.js +2 -1
- package/dist/web-ui/content/scripts.js +79 -73
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +21 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/thinking-efforts.d.ts +24 -0
- package/dist/web-ui/thinking-efforts.js +81 -0
- package/dist/ws-broadcast.d.ts +1 -1
- package/dist/ws-broadcast.js +28 -9
- package/package.json +8 -3
- package/scripts/install-render-binary.js +499 -0
- package/scripts/stage-render-binaries.js +626 -0
package/dist/process-manager.js
CHANGED
|
@@ -13,7 +13,7 @@ import { buildChildEnv, isRunningAsRoot } from "./env-utils.js";
|
|
|
13
13
|
import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
|
|
14
14
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
15
15
|
import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
|
|
16
|
-
import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort,
|
|
16
|
+
import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToPiLevel, thinkingEffortToQoderEffort } from "./structured-provider-common.js";
|
|
17
17
|
import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
|
|
18
18
|
import { getErrorMessage } from "./error-utils.js";
|
|
19
19
|
import { recordIterationPrompt } from "./iteration-log.js";
|
|
@@ -150,6 +150,15 @@ export class SessionInputError extends Error {
|
|
|
150
150
|
this.name = "SessionInputError";
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
+
/** Write outcome is unknown to the caller; do not record this as accepted input. */
|
|
154
|
+
export class PtyInputDeliveryError extends Error {
|
|
155
|
+
sessionId;
|
|
156
|
+
constructor(sessionId, cause) {
|
|
157
|
+
super("PTY input delivery could not be confirmed.", { cause });
|
|
158
|
+
this.sessionId = sessionId;
|
|
159
|
+
this.name = "PtyInputDeliveryError";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
153
162
|
const REAL_CONVERSATION_MIN_LINES = 2;
|
|
154
163
|
const DISCOVERY_RECENT_WINDOW_MS = 10 * 60 * 1000;
|
|
155
164
|
const START_TIME_SKEW_MS = 30 * 1000;
|
|
@@ -736,8 +745,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
736
745
|
});
|
|
737
746
|
}
|
|
738
747
|
bindTerminalProcess(record, child, onVisibleChunk) {
|
|
739
|
-
child.
|
|
748
|
+
child.onResync?.((state) => this.handleTerminalResync(record, child, state));
|
|
740
749
|
child.onData((event) => this.handleTerminalData(record, child, event, onVisibleChunk));
|
|
750
|
+
child.onExit(({ exitCode }) => this.handleTerminalExit(record, child, exitCode));
|
|
741
751
|
if (record.ptyBridge) {
|
|
742
752
|
record.ptyBridge.setPtyWrite((input) => {
|
|
743
753
|
if (this.sessions.get(record.id) !== record || record.ptyProcess !== child)
|
|
@@ -746,6 +756,31 @@ export class ProcessManager extends EventEmitter {
|
|
|
746
756
|
});
|
|
747
757
|
}
|
|
748
758
|
}
|
|
759
|
+
handleTerminalResync(record, child, state) {
|
|
760
|
+
const current = this.sessions.get(record.id);
|
|
761
|
+
if (current !== record || current.ptyProcess !== child)
|
|
762
|
+
return;
|
|
763
|
+
current.terminalState?.dispose();
|
|
764
|
+
current.terminalState = restoreTerminalState(state);
|
|
765
|
+
current.output = state.output;
|
|
766
|
+
current.storedOutput = state.output;
|
|
767
|
+
current.ptyOutputSeq = state.seq;
|
|
768
|
+
current.ptyCols = state.cols;
|
|
769
|
+
current.ptyRows = state.rows;
|
|
770
|
+
current.ptyLaunchMarkerToken = state.launchMarkerToken;
|
|
771
|
+
current.providerShellMarker = state.launchMarkerToken
|
|
772
|
+
? new PtyCliExitMarker(state.launchMarkerToken)
|
|
773
|
+
: null;
|
|
774
|
+
if (current.ptyBridge)
|
|
775
|
+
current.messages = current.ptyBridge.getMessages();
|
|
776
|
+
this.initializeClaudeBridge(current, state.output);
|
|
777
|
+
current.ptyBridge?.setPtyWrite((input) => {
|
|
778
|
+
if (this.sessions.get(record.id) === current && current.ptyProcess === child)
|
|
779
|
+
child.write(input);
|
|
780
|
+
});
|
|
781
|
+
this.flushPersist(current, true);
|
|
782
|
+
this.emitEvent({ type: "resync", sessionId: record.id, data: { reason: "render_replay_gap" } });
|
|
783
|
+
}
|
|
749
784
|
handleTerminalExit(record, child, exitCode) {
|
|
750
785
|
const current = this.sessions.get(record.id);
|
|
751
786
|
// A stopped session can be resumed under the same public id before the old
|
|
@@ -1426,7 +1461,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1426
1461
|
return result;
|
|
1427
1462
|
}
|
|
1428
1463
|
getPtyTranscript(id) {
|
|
1429
|
-
return this.logger.
|
|
1464
|
+
return this.logger.readPtyOutputTail(id);
|
|
1430
1465
|
}
|
|
1431
1466
|
/**
|
|
1432
1467
|
* Set the Claude model for an existing PTY session. Persists the selection
|
|
@@ -1483,6 +1518,39 @@ export class ProcessManager extends EventEmitter {
|
|
|
1483
1518
|
return this.snapshot(record);
|
|
1484
1519
|
}
|
|
1485
1520
|
sendInput(id, input, view, shortcutKey, trackUserInput = true) {
|
|
1521
|
+
const record = this.preparePtyInput(id, input, view, shortcutKey, trackUserInput);
|
|
1522
|
+
record.ptyProcess.write(input);
|
|
1523
|
+
this.persist(record);
|
|
1524
|
+
return this.snapshot(record);
|
|
1525
|
+
}
|
|
1526
|
+
async sendInputConfirmed(id, input, view, shortcutKey, trackUserInput = true) {
|
|
1527
|
+
const record = this.validatePtyInput(id);
|
|
1528
|
+
const terminal = record.ptyProcess;
|
|
1529
|
+
try {
|
|
1530
|
+
if (terminal.writeConfirmed)
|
|
1531
|
+
await terminal.writeConfirmed(input);
|
|
1532
|
+
else
|
|
1533
|
+
terminal.write(input);
|
|
1534
|
+
}
|
|
1535
|
+
catch (cause) {
|
|
1536
|
+
throw new PtyInputDeliveryError(id, cause);
|
|
1537
|
+
}
|
|
1538
|
+
const current = this.sessions.get(id);
|
|
1539
|
+
if (current !== record || record.ptyProcess !== terminal) {
|
|
1540
|
+
// The write reached an old PTY, but the caller cannot treat it as input
|
|
1541
|
+
// accepted by the current incarnation or persist the stale record.
|
|
1542
|
+
throw new PtyInputDeliveryError(id, new Error("PTY incarnation changed during write"));
|
|
1543
|
+
}
|
|
1544
|
+
this.trackPtyInput(record, input, view, shortcutKey, trackUserInput);
|
|
1545
|
+
this.persist(record);
|
|
1546
|
+
return this.snapshot(record);
|
|
1547
|
+
}
|
|
1548
|
+
preparePtyInput(id, input, view, shortcutKey, trackUserInput = true) {
|
|
1549
|
+
const record = this.validatePtyInput(id);
|
|
1550
|
+
this.trackPtyInput(record, input, view, shortcutKey, trackUserInput);
|
|
1551
|
+
return record;
|
|
1552
|
+
}
|
|
1553
|
+
validatePtyInput(id) {
|
|
1486
1554
|
if (this.disposed)
|
|
1487
1555
|
throw new Error("ProcessManager has been disposed.");
|
|
1488
1556
|
const record = this.mustGet(id);
|
|
@@ -1495,6 +1563,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
1495
1563
|
console.error(`[ProcessManager] Rejecting input: session ${id} has no PTY`);
|
|
1496
1564
|
throw new SessionInputError("Session is not running.", "SESSION_NO_PTY", id, record.status);
|
|
1497
1565
|
}
|
|
1566
|
+
return record;
|
|
1567
|
+
}
|
|
1568
|
+
trackPtyInput(record, input, view, shortcutKey, trackUserInput = true) {
|
|
1569
|
+
const id = record.id;
|
|
1498
1570
|
if (!record.ptyTopicDraft)
|
|
1499
1571
|
record.ptyTopicDraft = createPtyTopicLineBuffer();
|
|
1500
1572
|
const topicInput = consumePtyInputForTopic(record.ptyTopicDraft, input, view, shortcutKey);
|
|
@@ -1516,9 +1588,6 @@ export class ProcessManager extends EventEmitter {
|
|
|
1516
1588
|
if (record.ptyBridge && trackUserInput) {
|
|
1517
1589
|
record.ptyBridge.onUserInput(input);
|
|
1518
1590
|
}
|
|
1519
|
-
record.ptyProcess.write(input);
|
|
1520
|
-
this.persist(record);
|
|
1521
|
-
return this.snapshot(record);
|
|
1522
1591
|
}
|
|
1523
1592
|
resize(id, cols, rows) {
|
|
1524
1593
|
const record = this.mustGet(id);
|
|
@@ -1553,6 +1622,14 @@ export class ProcessManager extends EventEmitter {
|
|
|
1553
1622
|
}
|
|
1554
1623
|
return record.terminalState.snapshot();
|
|
1555
1624
|
}
|
|
1625
|
+
async getTerminalHistoryPage(id, before, revision) {
|
|
1626
|
+
const record = this.sessions.get(id);
|
|
1627
|
+
if (!record)
|
|
1628
|
+
return null;
|
|
1629
|
+
if (!record.terminalState)
|
|
1630
|
+
this.getTerminalState(id);
|
|
1631
|
+
return record.terminalState?.historyPage(before, revision) ?? null;
|
|
1632
|
+
}
|
|
1556
1633
|
/**
|
|
1557
1634
|
* Finalize provider-only state while leaving the owning PTY shell alive. The
|
|
1558
1635
|
* session itself remains running so terminal input can continue normally.
|
|
@@ -2405,7 +2482,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
2405
2482
|
if (trimmedModel && trimmedModel !== "default" && !/--model(?:\s|=)/.test(result) && !/(?:^|\s)-m(?:\s|$)/.test(result)) {
|
|
2406
2483
|
result += ` --model '${trimmedModel.replace(/'/g, "'\\''")}'`;
|
|
2407
2484
|
}
|
|
2408
|
-
const effort =
|
|
2485
|
+
const effort = thinkingEffortToGrokEffort(thinkingEffort ?? null);
|
|
2409
2486
|
if (effort && !/--(?:reasoning-)?effort(?:\s|=)/.test(result)) {
|
|
2410
2487
|
result += ` --effort '${effort.replace(/'/g, "'\\''")}'`;
|
|
2411
2488
|
}
|
|
@@ -2425,6 +2502,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
2425
2502
|
if (!/--(?:yolo|dangerously-skip-permissions|permission-mode)(?:\s|=|$)/.test(result)) {
|
|
2426
2503
|
result += " --permission-mode bypass_permissions";
|
|
2427
2504
|
}
|
|
2505
|
+
const qoderEffort = thinkingEffortToQoderEffort(thinkingEffort ?? null);
|
|
2506
|
+
if (qoderEffort && !/--reasoning-effort(?:\s|=)/.test(result)) {
|
|
2507
|
+
result += ` --reasoning-effort '${qoderEffort.replace(/'/g, "'\\''")}'`;
|
|
2508
|
+
}
|
|
2428
2509
|
return result;
|
|
2429
2510
|
}
|
|
2430
2511
|
if (provider === "pi") {
|
|
@@ -12,7 +12,19 @@ export interface PtyTerminalSnapshot {
|
|
|
12
12
|
cols: number;
|
|
13
13
|
rows: number;
|
|
14
14
|
pending: PtyTerminalOperation[];
|
|
15
|
+
/** Web scrollback cursor, absent from the Render wire protocol. */
|
|
16
|
+
historyBefore?: number;
|
|
17
|
+
historyRevision?: number;
|
|
15
18
|
}
|
|
19
|
+
export interface PtyHistoryPage {
|
|
20
|
+
data: string;
|
|
21
|
+
separator: string;
|
|
22
|
+
start: number;
|
|
23
|
+
before: number;
|
|
24
|
+
revision: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const PTY_INITIAL_HISTORY_ROWS = 80;
|
|
27
|
+
export declare const PTY_HISTORY_PAGE_ROWS = 80;
|
|
16
28
|
/**
|
|
17
29
|
* Server-side xterm screen model used only for reconnect snapshots. Live PTY
|
|
18
30
|
* bytes still travel unchanged from node-pty to the browser terminal.
|
|
@@ -31,11 +43,16 @@ export declare class PtyTerminalState {
|
|
|
31
43
|
private committedData;
|
|
32
44
|
private committedCols;
|
|
33
45
|
private committedRows;
|
|
46
|
+
private committedHistoryBefore;
|
|
47
|
+
private committedRevision;
|
|
48
|
+
private revision;
|
|
34
49
|
private disposed;
|
|
35
50
|
constructor(cols: number, rows: number, initialData?: string);
|
|
36
51
|
write(data: string): void;
|
|
37
52
|
resize(cols: number, rows: number): void;
|
|
38
53
|
snapshot(): PtyTerminalSnapshot;
|
|
54
|
+
/** A bounded ANSI range preceding the committed baseline, never a cumulative snapshot. */
|
|
55
|
+
historyPage(before: number, revision: number, limit?: number): Promise<PtyHistoryPage | null>;
|
|
39
56
|
dispose(): void;
|
|
40
57
|
private addPending;
|
|
41
58
|
private scheduleFlush;
|
|
@@ -2,6 +2,10 @@ import headless from "@xterm/headless";
|
|
|
2
2
|
import { SerializeAddon } from "@xterm/addon-serialize";
|
|
3
3
|
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
|
4
4
|
const { Terminal } = headless;
|
|
5
|
+
export const PTY_INITIAL_HISTORY_ROWS = 80;
|
|
6
|
+
const PTY_INITIAL_HISTORY_MAX_BYTES = 256 * 1024;
|
|
7
|
+
export const PTY_HISTORY_PAGE_ROWS = 80;
|
|
8
|
+
const PTY_HISTORY_PAGE_MAX_BYTES = 192 * 1024;
|
|
5
9
|
/**
|
|
6
10
|
* Incoming PTY chunks are coalesced into one xterm write per flush. xterm's
|
|
7
11
|
* write callback costs a timer turn per call, so one write per node-pty chunk
|
|
@@ -63,6 +67,9 @@ export class PtyTerminalState {
|
|
|
63
67
|
committedData = "";
|
|
64
68
|
committedCols;
|
|
65
69
|
committedRows;
|
|
70
|
+
committedHistoryBefore = 0;
|
|
71
|
+
committedRevision = 0;
|
|
72
|
+
revision = 0;
|
|
66
73
|
disposed = false;
|
|
67
74
|
constructor(cols, rows, initialData = "") {
|
|
68
75
|
this.committedCols = cols;
|
|
@@ -77,6 +84,7 @@ export class PtyTerminalState {
|
|
|
77
84
|
write(data) {
|
|
78
85
|
if (this.disposed || !data)
|
|
79
86
|
return;
|
|
87
|
+
this.revision++;
|
|
80
88
|
this.buffered += data;
|
|
81
89
|
if (this.buffered.length >= WRITE_BATCH_MAX_CHARS)
|
|
82
90
|
this.flushBuffered();
|
|
@@ -86,6 +94,7 @@ export class PtyTerminalState {
|
|
|
86
94
|
resize(cols, rows) {
|
|
87
95
|
if (this.disposed)
|
|
88
96
|
return;
|
|
97
|
+
this.revision++;
|
|
89
98
|
// Bytes that arrived before the resize must land before it.
|
|
90
99
|
this.flushBuffered();
|
|
91
100
|
this.addPending({ type: "resize", cols, rows });
|
|
@@ -105,6 +114,37 @@ export class PtyTerminalState {
|
|
|
105
114
|
cols: this.committedCols,
|
|
106
115
|
rows: this.committedRows,
|
|
107
116
|
pending: coalesceOperations(this.pending),
|
|
117
|
+
historyBefore: this.committedHistoryBefore,
|
|
118
|
+
historyRevision: this.committedRevision,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** A bounded ANSI range preceding the committed baseline, never a cumulative snapshot. */
|
|
122
|
+
async historyPage(before, revision, limit = PTY_HISTORY_PAGE_ROWS) {
|
|
123
|
+
if (this.disposed || !Number.isSafeInteger(before) || !Number.isSafeInteger(revision)
|
|
124
|
+
|| !Number.isSafeInteger(limit) || limit < 1 || limit > PTY_HISTORY_PAGE_ROWS)
|
|
125
|
+
return null;
|
|
126
|
+
// Wait for buffered writes/checkpoints. If output or resize changed the row indices,
|
|
127
|
+
// the caller must resync rather than concatenate unrelated lines.
|
|
128
|
+
await this.tail;
|
|
129
|
+
if (this.revision !== revision || before > this.committedHistoryBefore || before <= 0)
|
|
130
|
+
return null;
|
|
131
|
+
const count = Math.min(before, limit);
|
|
132
|
+
let start = before - count;
|
|
133
|
+
let data = "";
|
|
134
|
+
while (start < before) {
|
|
135
|
+
data = this.serializer.serialize({ range: { start, end: before - 1 } });
|
|
136
|
+
if (Buffer.byteLength(data, "utf8") <= PTY_HISTORY_PAGE_MAX_BYTES)
|
|
137
|
+
break;
|
|
138
|
+
if (start === before - 1)
|
|
139
|
+
return null;
|
|
140
|
+
start = Math.min(before - 1, start + Math.max(1, Math.ceil((before - start) / 2)));
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
data,
|
|
144
|
+
separator: this.terminal.buffer.normal.getLine(before)?.isWrapped ? "" : "\r\n",
|
|
145
|
+
start,
|
|
146
|
+
before,
|
|
147
|
+
revision,
|
|
108
148
|
};
|
|
109
149
|
}
|
|
110
150
|
dispose() {
|
|
@@ -186,14 +226,24 @@ export class PtyTerminalState {
|
|
|
186
226
|
const cutoff = this.pending.at(-1)?.id ?? 0;
|
|
187
227
|
if (!cutoff)
|
|
188
228
|
return;
|
|
229
|
+
const revision = this.revision;
|
|
189
230
|
this.checkpointQueued = true;
|
|
190
231
|
this.tail = this.tail.then(() => {
|
|
191
232
|
if (this.disposed)
|
|
192
233
|
return;
|
|
193
234
|
try {
|
|
194
|
-
|
|
235
|
+
let historyRows = PTY_INITIAL_HISTORY_ROWS;
|
|
236
|
+
this.committedData = this.serializer.serialize({ scrollback: historyRows });
|
|
237
|
+
while (historyRows > 0 && Buffer.byteLength(this.committedData) > PTY_INITIAL_HISTORY_MAX_BYTES) {
|
|
238
|
+
historyRows = Math.floor(historyRows / 2);
|
|
239
|
+
this.committedData = this.serializer.serialize({ scrollback: historyRows });
|
|
240
|
+
}
|
|
195
241
|
this.committedCols = this.terminal.cols;
|
|
196
242
|
this.committedRows = this.terminal.rows;
|
|
243
|
+
this.committedHistoryBefore = Math.max(0, this.terminal.buffer.normal.baseY - historyRows);
|
|
244
|
+
// A newer write can arrive while the queued checkpoint drains. Only the
|
|
245
|
+
// operations through cutoff are represented by this baseline.
|
|
246
|
+
this.committedRevision = revision;
|
|
197
247
|
this.pending = this.pending.filter((entry) => entry.id > cutoff);
|
|
198
248
|
}
|
|
199
249
|
catch {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 支持的分发三元组。
|
|
3
|
+
*
|
|
4
|
+
* 与 `render-bin/manifest.json` 的版本条目键、`render/release.json` 的
|
|
5
|
+
* `supportedTriples` 必须一致;新增平台要同时改这三处与 CI 矩阵。
|
|
6
|
+
*/
|
|
7
|
+
export declare const SUPPORTED_RENDER_TRIPLES: readonly ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"];
|
|
8
|
+
/** `uname -m` 的输出归一成 Node 的 arch 名;认不出返回 null(调用方回退 process.arch)。 */
|
|
9
|
+
export declare function normalizeMachineArch(machine: string | null | undefined): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* 平台三元组(`darwin-arm64` 等)。
|
|
12
|
+
*
|
|
13
|
+
* 为什么不直接用 `process.arch`:macOS 上 Node 跑在 Rosetta 里时它报 `x64`,
|
|
14
|
+
* 而机器其实是 arm64 —— 于是会去找一个不存在(或架构不符)的产物,最后表现成
|
|
15
|
+
* 「自动模式下 Rust 引擎莫名其妙不生效」。`uname -m` 才反映真实硬件;只有
|
|
16
|
+
* `uname` 不可用(PATH 被清空、Windows)时才回退 `process.arch`。
|
|
17
|
+
*
|
|
18
|
+
* `rosettaTranslated` 是反向兜底:翻译态下 `uname -m` 同样报 `x86_64`,
|
|
19
|
+
* 只有 `sysctl.proc_translated` 能说明「这个 x64 进程跑在 arm64 机器上」。
|
|
20
|
+
* 少了它,上面那条规则在它最该生效的场景里形同虚设。
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveRenderTriple(platform: string, nodeArch: string, unameMachine: string | null, rosettaTranslated?: boolean): string;
|
|
23
|
+
interface PlatformArchProbe {
|
|
24
|
+
unameMachine: string | null;
|
|
25
|
+
rosettaTranslated: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 真实架构探测(进程级缓存):`resolveRenderBinaryPath` 在一次启动里会被调用多次,
|
|
29
|
+
* 不该每次都 spawn 两个进程。
|
|
30
|
+
*/
|
|
31
|
+
export declare function probePlatformArch(): PlatformArchProbe;
|
|
32
|
+
/** 当前进程应使用的三元组。 */
|
|
33
|
+
export declare function currentRenderTriple(): string;
|
|
34
|
+
/**
|
|
35
|
+
* 约定候选路径(不含 `WAND_RENDER_BIN` 与 `PATH`,调用方单独处理)。
|
|
36
|
+
*
|
|
37
|
+
* 顺序刻意把「刚 cargo build 出来的」排在分发位置之前:开发机上调 Rust 引擎时
|
|
38
|
+
* 期望立刻生效,而不是被上一次安装的旧二进制盖住。
|
|
39
|
+
*
|
|
40
|
+
* 旧布局(`wand-rs/`、仓库根 `native/`)已不存在,候选里不留兼容分支:
|
|
41
|
+
* 留着只会让「引擎没生效」这类问题多一个静默的假来源。
|
|
42
|
+
*/
|
|
43
|
+
export declare function renderBinaryCandidates(configDir: string, triple: string): string[];
|
|
44
|
+
/**
|
|
45
|
+
* 解析 wand-render 可执行文件。
|
|
46
|
+
*
|
|
47
|
+
* 顺序:`WAND_RENDER_BIN` → `<repo>/render/target/{release,debug}` → `<configDir>/bin`
|
|
48
|
+
* → `<repo>/dist/native/<triple>` → `PATH`。返回 null 表示「这台机器没有」,由调用方
|
|
49
|
+
* 按 engine 决定告警还是报错。
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveRenderBinaryPath(configPath: string): string | null;
|
|
52
|
+
/**
|
|
53
|
+
* 读取 wand-render 的版本。
|
|
54
|
+
*
|
|
55
|
+
* 先读二进制旁边的 `wand-render.version` sidecar(就位脚本会写),读不到才去跑
|
|
56
|
+
* `wand-render --version` —— 启动路径上不该无谓地执行一个进程。
|
|
57
|
+
* 两种方式都失败时返回 null:调用方只要一个展示用版本号,拿不到不能影响启动。
|
|
58
|
+
*/
|
|
59
|
+
export declare function readRenderBinaryVersion(binaryPath: string): Promise<string | null>;
|
|
60
|
+
export declare function isExecutableFile(candidate: string): boolean;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
/** 仓库根:src/ 与 dist/ 都在根下一层,所以两种运行方式解析结果一致。 */
|
|
7
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const VERSION_PROBE_TIMEOUT_MS = 2_000;
|
|
9
|
+
const ARCH_PROBE_TIMEOUT_MS = 1_000;
|
|
10
|
+
/**
|
|
11
|
+
* 支持的分发三元组。
|
|
12
|
+
*
|
|
13
|
+
* 与 `render-bin/manifest.json` 的版本条目键、`render/release.json` 的
|
|
14
|
+
* `supportedTriples` 必须一致;新增平台要同时改这三处与 CI 矩阵。
|
|
15
|
+
*/
|
|
16
|
+
export const SUPPORTED_RENDER_TRIPLES = ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"];
|
|
17
|
+
/** `uname -m` 的架构别名 → Node 的 `process.arch` 命名(两个生态叫法不同)。 */
|
|
18
|
+
const MACHINE_ARCH_ALIASES = {
|
|
19
|
+
arm64: "arm64",
|
|
20
|
+
aarch64: "arm64",
|
|
21
|
+
x86_64: "x64",
|
|
22
|
+
amd64: "x64",
|
|
23
|
+
x64: "x64",
|
|
24
|
+
};
|
|
25
|
+
/** `uname -m` 的输出归一成 Node 的 arch 名;认不出返回 null(调用方回退 process.arch)。 */
|
|
26
|
+
export function normalizeMachineArch(machine) {
|
|
27
|
+
if (typeof machine !== "string")
|
|
28
|
+
return null;
|
|
29
|
+
return MACHINE_ARCH_ALIASES[machine.trim().toLowerCase()] ?? null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 平台三元组(`darwin-arm64` 等)。
|
|
33
|
+
*
|
|
34
|
+
* 为什么不直接用 `process.arch`:macOS 上 Node 跑在 Rosetta 里时它报 `x64`,
|
|
35
|
+
* 而机器其实是 arm64 —— 于是会去找一个不存在(或架构不符)的产物,最后表现成
|
|
36
|
+
* 「自动模式下 Rust 引擎莫名其妙不生效」。`uname -m` 才反映真实硬件;只有
|
|
37
|
+
* `uname` 不可用(PATH 被清空、Windows)时才回退 `process.arch`。
|
|
38
|
+
*
|
|
39
|
+
* `rosettaTranslated` 是反向兜底:翻译态下 `uname -m` 同样报 `x86_64`,
|
|
40
|
+
* 只有 `sysctl.proc_translated` 能说明「这个 x64 进程跑在 arm64 机器上」。
|
|
41
|
+
* 少了它,上面那条规则在它最该生效的场景里形同虚设。
|
|
42
|
+
*/
|
|
43
|
+
export function resolveRenderTriple(platform, nodeArch, unameMachine, rosettaTranslated = false) {
|
|
44
|
+
let arch = normalizeMachineArch(unameMachine) ?? nodeArch;
|
|
45
|
+
if (platform === "darwin" && arch === "x64" && rosettaTranslated)
|
|
46
|
+
arch = "arm64";
|
|
47
|
+
return `${platform}-${arch}`;
|
|
48
|
+
}
|
|
49
|
+
let cachedPlatformArchProbe = null;
|
|
50
|
+
/**
|
|
51
|
+
* 真实架构探测(进程级缓存):`resolveRenderBinaryPath` 在一次启动里会被调用多次,
|
|
52
|
+
* 不该每次都 spawn 两个进程。
|
|
53
|
+
*/
|
|
54
|
+
export function probePlatformArch() {
|
|
55
|
+
if (!cachedPlatformArchProbe) {
|
|
56
|
+
cachedPlatformArchProbe = {
|
|
57
|
+
unameMachine: runUnameMachine(),
|
|
58
|
+
rosettaTranslated: isRosettaTranslated(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return cachedPlatformArchProbe;
|
|
62
|
+
}
|
|
63
|
+
/** 当前进程应使用的三元组。 */
|
|
64
|
+
export function currentRenderTriple() {
|
|
65
|
+
const probe = probePlatformArch();
|
|
66
|
+
return resolveRenderTriple(process.platform, process.arch, probe.unameMachine, probe.rosettaTranslated);
|
|
67
|
+
}
|
|
68
|
+
function runUnameMachine() {
|
|
69
|
+
if (process.platform === "win32")
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
const result = spawnSync("uname", ["-m"], {
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
timeout: ARCH_PROBE_TIMEOUT_MS,
|
|
75
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
76
|
+
});
|
|
77
|
+
if (result.error || result.status !== 0)
|
|
78
|
+
return null;
|
|
79
|
+
return (result.stdout ?? "").trim() || null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** `sysctl.proc_translated` == 1 表示当前进程被 Rosetta 翻译(只在 darwin 上有意义)。 */
|
|
86
|
+
function isRosettaTranslated() {
|
|
87
|
+
if (process.platform !== "darwin")
|
|
88
|
+
return false;
|
|
89
|
+
try {
|
|
90
|
+
const result = spawnSync("sysctl", ["-n", "sysctl.proc_translated"], {
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
timeout: ARCH_PROBE_TIMEOUT_MS,
|
|
93
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
94
|
+
});
|
|
95
|
+
if (result.error || result.status !== 0)
|
|
96
|
+
return false;
|
|
97
|
+
return (result.stdout ?? "").trim() === "1";
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 约定候选路径(不含 `WAND_RENDER_BIN` 与 `PATH`,调用方单独处理)。
|
|
105
|
+
*
|
|
106
|
+
* 顺序刻意把「刚 cargo build 出来的」排在分发位置之前:开发机上调 Rust 引擎时
|
|
107
|
+
* 期望立刻生效,而不是被上一次安装的旧二进制盖住。
|
|
108
|
+
*
|
|
109
|
+
* 旧布局(`wand-rs/`、仓库根 `native/`)已不存在,候选里不留兼容分支:
|
|
110
|
+
* 留着只会让「引擎没生效」这类问题多一个静默的假来源。
|
|
111
|
+
*/
|
|
112
|
+
export function renderBinaryCandidates(configDir, triple) {
|
|
113
|
+
const binaryName = process.platform === "win32" ? "wand-render.exe" : "wand-render";
|
|
114
|
+
return [
|
|
115
|
+
path.join(REPO_ROOT, "render", "target", "release", binaryName),
|
|
116
|
+
path.join(REPO_ROOT, "render", "target", "debug", binaryName),
|
|
117
|
+
path.join(configDir, "bin", binaryName),
|
|
118
|
+
// npm 包内嵌的正式位置:scripts/stage-render-binaries.js 把 render-bin 的产物写在这里,
|
|
119
|
+
// dist/ 在 package.json 的 files 里,所以全局安装后也有二进制。
|
|
120
|
+
path.join(REPO_ROOT, "dist", "native", triple, binaryName),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 解析 wand-render 可执行文件。
|
|
125
|
+
*
|
|
126
|
+
* 顺序:`WAND_RENDER_BIN` → `<repo>/render/target/{release,debug}` → `<configDir>/bin`
|
|
127
|
+
* → `<repo>/dist/native/<triple>` → `PATH`。返回 null 表示「这台机器没有」,由调用方
|
|
128
|
+
* 按 engine 决定告警还是报错。
|
|
129
|
+
*/
|
|
130
|
+
export function resolveRenderBinaryPath(configPath) {
|
|
131
|
+
const binaryName = process.platform === "win32" ? "wand-render.exe" : "wand-render";
|
|
132
|
+
const fromEnv = process.env.WAND_RENDER_BIN?.trim();
|
|
133
|
+
if (fromEnv) {
|
|
134
|
+
if (isExecutableFile(fromEnv))
|
|
135
|
+
return fromEnv;
|
|
136
|
+
// 显式配置但不存在属于配置错误,必须可见,不能静默滑到仓库构建产物。
|
|
137
|
+
process.stderr.write(`[wand] WAND_RENDER_BIN points at ${fromEnv}, which is not an executable file; falling back to discovery.\n`);
|
|
138
|
+
}
|
|
139
|
+
const configDir = path.dirname(path.resolve(configPath));
|
|
140
|
+
const triple = currentRenderTriple();
|
|
141
|
+
for (const candidate of renderBinaryCandidates(configDir, triple)) {
|
|
142
|
+
if (isExecutableFile(candidate))
|
|
143
|
+
return candidate;
|
|
144
|
+
}
|
|
145
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
146
|
+
if (!dir)
|
|
147
|
+
continue;
|
|
148
|
+
const candidate = path.join(dir, binaryName);
|
|
149
|
+
if (isExecutableFile(candidate))
|
|
150
|
+
return candidate;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 读取 wand-render 的版本。
|
|
156
|
+
*
|
|
157
|
+
* 先读二进制旁边的 `wand-render.version` sidecar(就位脚本会写),读不到才去跑
|
|
158
|
+
* `wand-render --version` —— 启动路径上不该无谓地执行一个进程。
|
|
159
|
+
* 两种方式都失败时返回 null:调用方只要一个展示用版本号,拿不到不能影响启动。
|
|
160
|
+
*/
|
|
161
|
+
export function readRenderBinaryVersion(binaryPath) {
|
|
162
|
+
const sidecar = readVersionSidecar(binaryPath);
|
|
163
|
+
if (sidecar)
|
|
164
|
+
return Promise.resolve(sidecar);
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
execFile(binaryPath, ["--version"], { timeout: VERSION_PROBE_TIMEOUT_MS }, (error, stdout) => {
|
|
167
|
+
if (error) {
|
|
168
|
+
resolve(null);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const firstLine = stdout.split("\n").map((line) => line.trim()).find((line) => line.length > 0) ?? "";
|
|
172
|
+
const version = /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.]+)?)/.exec(firstLine);
|
|
173
|
+
resolve(version ? version[1] : null);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function readVersionSidecar(binaryPath) {
|
|
178
|
+
try {
|
|
179
|
+
const raw = readFileSync(path.join(path.dirname(binaryPath), "wand-render.version"), "utf8").trim();
|
|
180
|
+
const version = /^(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.]+)?)$/.exec(raw);
|
|
181
|
+
return version ? version[1] : null;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export function isExecutableFile(candidate) {
|
|
188
|
+
try {
|
|
189
|
+
if (!existsSync(candidate) || !statSync(candidate).isFile())
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
if (process.platform === "win32")
|
|
196
|
+
return true;
|
|
197
|
+
try {
|
|
198
|
+
accessSync(candidate, constants.X_OK);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|