@dev.fast/whiteboard 0.0.0-stage → 0.2.1-preview.20261005.98
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/LICENSE +21 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- package/tutorial/trace.json +20 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { type SpawnOptions, spawn } from "node:child_process";
|
|
2
|
+
import { closeSync, fstatSync, mkdtempSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type ReviewDesktopDiscovery,
|
|
8
|
+
parseReviewVerbResponse,
|
|
9
|
+
} from "@dev.fast/review-protocol";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type ReviewInstanceSelection,
|
|
13
|
+
healthyReviewInstance,
|
|
14
|
+
reviewInstanceStartHint,
|
|
15
|
+
reviewInstanceUnavailable,
|
|
16
|
+
selectReviewInstance,
|
|
17
|
+
} from "./desktop-discovery";
|
|
18
|
+
|
|
19
|
+
const RELEASE_APPS = {
|
|
20
|
+
stable: {
|
|
21
|
+
bundleId: "dev.fast.review",
|
|
22
|
+
linuxLauncher: "/usr/bin/review-desktop",
|
|
23
|
+
},
|
|
24
|
+
preview: {
|
|
25
|
+
bundleId: "dev.fast.review.preview",
|
|
26
|
+
linuxLauncher: "/usr/bin/review-preview-desktop",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** "1" on launches without --focus; Desktop opens inactive. */
|
|
31
|
+
export const REVIEW_DESKTOP_BACKGROUND_ENV =
|
|
32
|
+
"DEV_FAST_REVIEW_DESKTOP_BACKGROUND";
|
|
33
|
+
|
|
34
|
+
const DEFAULT_LAUNCH_TIMEOUT_MS = 90_000;
|
|
35
|
+
|
|
36
|
+
const POLL_INTERVAL_MS = 250;
|
|
37
|
+
|
|
38
|
+
const EARLY_EXIT_GRACE_MS = 5_000;
|
|
39
|
+
|
|
40
|
+
interface DesktopLaunchProcess {
|
|
41
|
+
once(event: "error", listener: (error: Error) => void): this;
|
|
42
|
+
once(
|
|
43
|
+
event: "exit",
|
|
44
|
+
listener: (code: number | null, signal: NodeJS.Signals | null) => void,
|
|
45
|
+
): this;
|
|
46
|
+
unref(): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ReviewAppLauncherRuntime {
|
|
50
|
+
selectInstance: () => Promise<ReviewInstanceSelection>;
|
|
51
|
+
fetch: typeof globalThis.fetch;
|
|
52
|
+
focusDesktop: (discovery: ReviewDesktopDiscovery) => Promise<void>;
|
|
53
|
+
launchDesktop: typeof launchDesktopApplication;
|
|
54
|
+
now: () => number;
|
|
55
|
+
wait: (milliseconds: number) => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface RunReviewAppLaunchInput {
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
/** Bring Whiteboard Desktop forward. */
|
|
61
|
+
focus?: boolean;
|
|
62
|
+
/** Explicit Linux container opt-out from the Chromium sandbox. */
|
|
63
|
+
noSandbox?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ReviewAppLaunchEvent {
|
|
67
|
+
event: "app";
|
|
68
|
+
action: "launch";
|
|
69
|
+
state: "launched" | "running";
|
|
70
|
+
instanceId: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface DesktopLaunchAttempt {
|
|
74
|
+
method: string;
|
|
75
|
+
logPath?: string;
|
|
76
|
+
completion: Promise<DesktopLaunchCompletion>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface DesktopLaunchCompletion {
|
|
80
|
+
code: number | null;
|
|
81
|
+
signal: NodeJS.Signals | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface LaunchDesktopApplicationInput {
|
|
85
|
+
platform?: NodeJS.Platform;
|
|
86
|
+
execPath?: string;
|
|
87
|
+
electron?: boolean;
|
|
88
|
+
env?: NodeJS.ProcessEnv;
|
|
89
|
+
focus?: boolean;
|
|
90
|
+
noSandbox?: boolean;
|
|
91
|
+
/** An explicitly selected release instance; absent, the installed app's own. */
|
|
92
|
+
instance?: { key: "stable" | "preview"; appPath?: string };
|
|
93
|
+
spawn?: (
|
|
94
|
+
command: string,
|
|
95
|
+
args: readonly string[],
|
|
96
|
+
options: SpawnOptions,
|
|
97
|
+
) => DesktopLaunchProcess;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function runReviewAppLaunch(
|
|
101
|
+
input: RunReviewAppLaunchInput = {},
|
|
102
|
+
overrides: Partial<ReviewAppLauncherRuntime> = {},
|
|
103
|
+
): Promise<ReviewAppLaunchEvent> {
|
|
104
|
+
const fetch = overrides.fetch ?? globalThis.fetch;
|
|
105
|
+
|
|
106
|
+
const runtime: ReviewAppLauncherRuntime = {
|
|
107
|
+
selectInstance: () => selectReviewInstance({ fetch }),
|
|
108
|
+
fetch,
|
|
109
|
+
focusDesktop: (discovery) => focusReviewDesktop(discovery, fetch),
|
|
110
|
+
launchDesktop: launchDesktopApplication,
|
|
111
|
+
now: Date.now,
|
|
112
|
+
wait: (milliseconds) =>
|
|
113
|
+
new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
114
|
+
...overrides,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Launch recovers from a stale, malformed, or incompatible record, so a
|
|
118
|
+
// selection problem is not a stop here.
|
|
119
|
+
const selection = await runtime.selectInstance();
|
|
120
|
+
const current = healthyReviewInstance(selection);
|
|
121
|
+
|
|
122
|
+
if (current) {
|
|
123
|
+
if (input.focus) await runtime.focusDesktop(current);
|
|
124
|
+
|
|
125
|
+
return launchEvent("running", current.instanceId);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const running = selection.instances.filter((instance) => instance.healthy);
|
|
129
|
+
|
|
130
|
+
if (selection.key !== "stable" && selection.key !== "preview")
|
|
131
|
+
throw new Error(
|
|
132
|
+
`${reviewInstanceStartHint(selection)}; \`whiteboard app launch\` starts only installed apps.`,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (selection.source === "fallback" && running.length > 1)
|
|
136
|
+
throw reviewInstanceUnavailable(selection);
|
|
137
|
+
|
|
138
|
+
const launch: LaunchDesktopApplicationInput = {
|
|
139
|
+
focus: input.focus,
|
|
140
|
+
noSandbox: input.noSandbox,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (selection.source !== "fallback")
|
|
144
|
+
launch.instance = {
|
|
145
|
+
key: selection.key,
|
|
146
|
+
appPath: selection.instance?.discovery.appPath,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const attempt = runtime.launchDesktop(launch);
|
|
150
|
+
|
|
151
|
+
let completion: Promise<DesktopLaunchCompletion> | undefined =
|
|
152
|
+
observedCompletion(attempt);
|
|
153
|
+
|
|
154
|
+
void completion.catch(() => undefined);
|
|
155
|
+
|
|
156
|
+
const deadline =
|
|
157
|
+
runtime.now() + (input.timeoutMs ?? DEFAULT_LAUNCH_TIMEOUT_MS);
|
|
158
|
+
|
|
159
|
+
let unexpectedSuccessfulExitAt: number | undefined;
|
|
160
|
+
|
|
161
|
+
while (runtime.now() < deadline) {
|
|
162
|
+
const ready = healthyReviewInstance(await runtime.selectInstance());
|
|
163
|
+
|
|
164
|
+
if (ready) return launchEvent("launched", ready.instanceId);
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
unexpectedSuccessfulExitAt !== undefined &&
|
|
168
|
+
runtime.now() - unexpectedSuccessfulExitAt >= EARLY_EXIT_GRACE_MS
|
|
169
|
+
) {
|
|
170
|
+
throw launchFailure(
|
|
171
|
+
attempt,
|
|
172
|
+
new Error("the launch process exited before Desktop became ready"),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const remaining = Math.max(0, deadline - runtime.now());
|
|
177
|
+
|
|
178
|
+
const outcome = completion
|
|
179
|
+
? await Promise.race([
|
|
180
|
+
completion.then((result) => ({ completion: result })),
|
|
181
|
+
runtime
|
|
182
|
+
.wait(Math.min(POLL_INTERVAL_MS, remaining))
|
|
183
|
+
.then(() => ({ completion: null })),
|
|
184
|
+
])
|
|
185
|
+
: await runtime
|
|
186
|
+
.wait(Math.min(POLL_INTERVAL_MS, remaining))
|
|
187
|
+
.then(() => ({ completion: null }));
|
|
188
|
+
|
|
189
|
+
if (outcome.completion) {
|
|
190
|
+
assertSuccessfulLaunchCompletion(attempt, outcome.completion);
|
|
191
|
+
|
|
192
|
+
unexpectedSuccessfulExitAt = runtime.now();
|
|
193
|
+
completion = undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (unexpectedSuccessfulExitAt !== undefined) {
|
|
198
|
+
throw launchFailure(
|
|
199
|
+
attempt,
|
|
200
|
+
new Error("the launch process exited before Desktop became ready"),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Whiteboard Desktop did not become ready within ${Math.ceil((input.timeoutMs ?? DEFAULT_LAUNCH_TIMEOUT_MS) / 1_000)} seconds after ${attempt.method}. Open Whiteboard Desktop once, then run \`whiteboard app launch\` again.${launchDiagnostics(attempt.logPath)}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function focusReviewDesktop(
|
|
210
|
+
discovery: ReviewDesktopDiscovery,
|
|
211
|
+
fetch: typeof globalThis.fetch,
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
const response = await fetch(`${discovery.url}/app/focus`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "x-review-token": discovery.token },
|
|
216
|
+
signal: AbortSignal.timeout(5_000),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const result = parseReviewVerbResponse(await response.json());
|
|
220
|
+
|
|
221
|
+
if (!response.ok || !result.ok) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
result.ok
|
|
224
|
+
? `Whiteboard Desktop focus returned ${response.status}.`
|
|
225
|
+
: result.error,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function launchDesktopApplication(
|
|
231
|
+
input: LaunchDesktopApplicationInput = {},
|
|
232
|
+
): DesktopLaunchAttempt {
|
|
233
|
+
const platform = input.platform ?? process.platform;
|
|
234
|
+
|
|
235
|
+
if (platform !== "darwin" && platform !== "linux") {
|
|
236
|
+
return {
|
|
237
|
+
method: `the ${platform} application launcher`,
|
|
238
|
+
completion: Promise.reject(
|
|
239
|
+
new Error("automatic launch is available only on macOS and Linux"),
|
|
240
|
+
),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (input.noSandbox && platform !== "linux")
|
|
245
|
+
return {
|
|
246
|
+
method: `the ${platform} application launcher`,
|
|
247
|
+
completion: Promise.reject(
|
|
248
|
+
new Error("--no-sandbox is only supported for Linux Desktop launches."),
|
|
249
|
+
),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const electron = input.electron ?? Boolean(process.versions.electron);
|
|
253
|
+
const env = { ...(input.env ?? process.env) };
|
|
254
|
+
const execPath = input.execPath ?? process.execPath;
|
|
255
|
+
const focus = input.focus === true;
|
|
256
|
+
|
|
257
|
+
if (focus) delete env[REVIEW_DESKTOP_BACKGROUND_ENV];
|
|
258
|
+
else env[REVIEW_DESKTOP_BACKGROUND_ENV] = "1";
|
|
259
|
+
|
|
260
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
261
|
+
|
|
262
|
+
// An installed app must never inherit a dev Desktop's identity.
|
|
263
|
+
delete env.DEV_FAST_REVIEW_CHECKOUT;
|
|
264
|
+
const release = RELEASE_APPS[input.instance?.key ?? "stable"];
|
|
265
|
+
const stateRoot = env.DEV_FAST_REVIEW_DESKTOP_STATE_ROOT?.trim();
|
|
266
|
+
|
|
267
|
+
const profileArgs = stateRoot
|
|
268
|
+
? [
|
|
269
|
+
`--user-data-dir=${path.resolve(stateRoot, "user-data")}`,
|
|
270
|
+
`--extensions-dir=${path.resolve(stateRoot, "extensions")}`,
|
|
271
|
+
]
|
|
272
|
+
: [];
|
|
273
|
+
|
|
274
|
+
let command = "/usr/bin/open";
|
|
275
|
+
let method: string;
|
|
276
|
+
let args: string[];
|
|
277
|
+
|
|
278
|
+
if (platform === "linux") {
|
|
279
|
+
delete env.VSCODE_DEV;
|
|
280
|
+
delete env.VSCODE_CLI;
|
|
281
|
+
// With no selection, the Fedora CLI wrappers name their own channel's launcher.
|
|
282
|
+
command =
|
|
283
|
+
(input.instance ? "" : env.DEV_FAST_REVIEW_DESKTOP_COMMAND?.trim()) ||
|
|
284
|
+
release.linuxLauncher;
|
|
285
|
+
method = `the installed Linux launcher at "${command}"`;
|
|
286
|
+
|
|
287
|
+
if (electron && !env.DEV_FAST_REVIEW_DESKTOP_COMMAND?.trim()) {
|
|
288
|
+
command = execPath;
|
|
289
|
+
method = `the Desktop-managed bundle at "${command}"`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
args = profileArgs;
|
|
293
|
+
|
|
294
|
+
if (input.noSandbox) args.push("--no-sandbox");
|
|
295
|
+
} else {
|
|
296
|
+
// Direct app execs abort in AppKit under Codex's sandbox.
|
|
297
|
+
const appPath = input.instance
|
|
298
|
+
? input.instance.appPath
|
|
299
|
+
: electron
|
|
300
|
+
? execPath.match(/^(.*?\.app)\/Contents\/MacOS\//)?.[1]
|
|
301
|
+
: undefined;
|
|
302
|
+
|
|
303
|
+
const target = appPath?.endsWith(".app")
|
|
304
|
+
? ["-a", appPath]
|
|
305
|
+
: ["-b", release.bundleId];
|
|
306
|
+
|
|
307
|
+
method = appPath?.endsWith(".app")
|
|
308
|
+
? `the macOS application at "${appPath}"`
|
|
309
|
+
: `the macOS bundle identifier "${release.bundleId}"`;
|
|
310
|
+
|
|
311
|
+
// -n allows parallel profiles; -W exits with Desktop, so a startup crash
|
|
312
|
+
// surfaces early; --env carries Whiteboard's context.
|
|
313
|
+
args = ["-n", "-W", ...(focus ? [] : ["-g"]), ...target];
|
|
314
|
+
|
|
315
|
+
for (const [key, value] of Object.entries(env)) {
|
|
316
|
+
if (value !== undefined && /^DEV_(REVIEW|FAST)_/.test(key))
|
|
317
|
+
args.push("--env", `${key}=${value}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (profileArgs.length > 0) args.push("--args", ...profileArgs);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const {
|
|
324
|
+
promise: completion,
|
|
325
|
+
resolve,
|
|
326
|
+
reject,
|
|
327
|
+
} = Promise.withResolvers<DesktopLaunchCompletion>();
|
|
328
|
+
|
|
329
|
+
let logPath: string | undefined;
|
|
330
|
+
let logFd: number | undefined;
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
if (platform === "linux") {
|
|
334
|
+
logPath = path.join(
|
|
335
|
+
mkdtempSync(path.join(tmpdir(), "whiteboard-launch-")),
|
|
336
|
+
"stderr.log",
|
|
337
|
+
);
|
|
338
|
+
logFd = openSync(logPath, "wx", 0o600);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const spawnProcess = input.spawn ?? spawn;
|
|
342
|
+
|
|
343
|
+
const child = spawnProcess(command, args, {
|
|
344
|
+
detached: true,
|
|
345
|
+
env,
|
|
346
|
+
stdio: logFd === undefined ? "ignore" : ["ignore", "ignore", logFd],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
child.once("error", (error) => reject(error));
|
|
350
|
+
child.once("exit", (code, signal) => {
|
|
351
|
+
resolve({ code, signal });
|
|
352
|
+
});
|
|
353
|
+
child.unref();
|
|
354
|
+
} catch (error) {
|
|
355
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
356
|
+
} finally {
|
|
357
|
+
if (logFd !== undefined) closeSync(logFd);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
method,
|
|
362
|
+
logPath,
|
|
363
|
+
completion,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function launchEvent(
|
|
368
|
+
state: ReviewAppLaunchEvent["state"],
|
|
369
|
+
instanceId: string,
|
|
370
|
+
): ReviewAppLaunchEvent {
|
|
371
|
+
return { event: "app", action: "launch", state, instanceId };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function launchFailure(attempt: DesktopLaunchAttempt, error: Error): Error {
|
|
375
|
+
return new Error(
|
|
376
|
+
`Could not launch Whiteboard with ${attempt.method}: ${error.message}. Open Whiteboard once, then run \`whiteboard app launch\` again. A sandboxed agent must run it outside the sandbox.${launchDiagnostics(attempt.logPath)}`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function assertSuccessfulLaunchCompletion(
|
|
381
|
+
attempt: DesktopLaunchAttempt,
|
|
382
|
+
completion: DesktopLaunchCompletion,
|
|
383
|
+
): void {
|
|
384
|
+
if (completion.code === 0 && !completion.signal) return;
|
|
385
|
+
throw launchFailure(
|
|
386
|
+
attempt,
|
|
387
|
+
new Error(
|
|
388
|
+
completion.signal
|
|
389
|
+
? `the launch process exited on ${completion.signal}`
|
|
390
|
+
: `the launch process exited with code ${completion.code ?? "unknown"}`,
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function observedCompletion(
|
|
396
|
+
attempt: DesktopLaunchAttempt,
|
|
397
|
+
): Promise<DesktopLaunchCompletion> {
|
|
398
|
+
return attempt.completion.catch((error) => {
|
|
399
|
+
throw launchFailure(
|
|
400
|
+
attempt,
|
|
401
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function launchDiagnostics(logPath: string | undefined): string {
|
|
407
|
+
if (!logPath) return "";
|
|
408
|
+
let tail = "";
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const fd = openSync(logPath, "r");
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const size = fstatSync(fd).size;
|
|
415
|
+
const buffer = Buffer.alloc(Math.min(size, 16_384));
|
|
416
|
+
const read = readSync(fd, buffer, 0, buffer.length, size - buffer.length);
|
|
417
|
+
tail = buffer
|
|
418
|
+
.subarray(0, read)
|
|
419
|
+
.toString("utf8")
|
|
420
|
+
.trim()
|
|
421
|
+
.split("\n")
|
|
422
|
+
.slice(-20)
|
|
423
|
+
.join("\n");
|
|
424
|
+
} finally {
|
|
425
|
+
closeSync(fd);
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
// A missing log must not hide the launch failure.
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return `\nDesktop log: ${logPath}${tail ? `\n${tail}` : ""}`;
|
|
432
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { emitKeypressEvents } from "node:readline";
|
|
2
|
+
import type { Writable } from "node:stream";
|
|
3
|
+
|
|
4
|
+
import { fuzzyRank } from "./fuzzy-match";
|
|
5
|
+
|
|
6
|
+
export interface ReviewPickerItem {
|
|
7
|
+
uuid: string;
|
|
8
|
+
title: string;
|
|
9
|
+
status: string;
|
|
10
|
+
lastPublishedAt: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MAX_VISIBLE_ROWS = 10;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimal filter-as-you-type picker over the repository's reviews. Raw-mode
|
|
17
|
+
* stdin, ANSI redraw, no dependencies. Resolves the chosen item, or null when
|
|
18
|
+
* the reviewer cancels (Escape / Ctrl-C).
|
|
19
|
+
*/
|
|
20
|
+
export function pickReview(
|
|
21
|
+
items: readonly ReviewPickerItem[],
|
|
22
|
+
io: { stdin: NodeJS.ReadStream; stdout: Writable },
|
|
23
|
+
): Promise<ReviewPickerItem | null> {
|
|
24
|
+
const { stdin, stdout } = io;
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let query = "";
|
|
28
|
+
let cursor = 0;
|
|
29
|
+
let renderedLines = 0;
|
|
30
|
+
|
|
31
|
+
const filtered = (): ReviewPickerItem[] =>
|
|
32
|
+
fuzzyRank(query, items, (item) => [item.title, item.status, item.uuid]);
|
|
33
|
+
|
|
34
|
+
const clearRendered = () => {
|
|
35
|
+
if (renderedLines === 0) return;
|
|
36
|
+
stdout.write(`[${renderedLines}A[J`);
|
|
37
|
+
renderedLines = 0;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const render = () => {
|
|
41
|
+
clearRendered();
|
|
42
|
+
const matches = filtered();
|
|
43
|
+
|
|
44
|
+
if (cursor >= matches.length) cursor = Math.max(0, matches.length - 1);
|
|
45
|
+
const lines = [`? Switch Whiteboard Desktop to: ${query}█`];
|
|
46
|
+
const visible = matches.slice(0, MAX_VISIBLE_ROWS);
|
|
47
|
+
|
|
48
|
+
for (const [index, item] of visible.entries()) {
|
|
49
|
+
const marker = index === cursor ? "[7m" : "";
|
|
50
|
+
const reset = index === cursor ? "[27m" : "";
|
|
51
|
+
const age = relativeTime(item.lastPublishedAt);
|
|
52
|
+
lines.push(
|
|
53
|
+
`${marker} ${item.title} [2m${item.status}${age ? ` · ${age}` : ""}[22m${reset}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (matches.length === 0) lines.push(" [2mno matches[22m");
|
|
58
|
+
|
|
59
|
+
if (matches.length > visible.length) {
|
|
60
|
+
lines.push(
|
|
61
|
+
` [2m… ${matches.length - visible.length} more; keep typing[22m`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
stdout.write(`${lines.join("\n")}\n`);
|
|
66
|
+
renderedLines = lines.length;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const finish = (result: ReviewPickerItem | null) => {
|
|
70
|
+
stdin.off("keypress", onKeypress);
|
|
71
|
+
stdin.setRawMode?.(false);
|
|
72
|
+
stdin.pause();
|
|
73
|
+
clearRendered();
|
|
74
|
+
resolve(result);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const onKeypress = (
|
|
78
|
+
character: string | undefined,
|
|
79
|
+
key: { name?: string; ctrl?: boolean } | undefined,
|
|
80
|
+
) => {
|
|
81
|
+
try {
|
|
82
|
+
const matches = filtered();
|
|
83
|
+
|
|
84
|
+
/* With no matches the row limit is -1. Clamping keeps the cursor on a
|
|
85
|
+
real row, so a later Enter cannot resolve matches[-1] to undefined
|
|
86
|
+
and cancel the picker instead of opening a review. */
|
|
87
|
+
const stepDown = () =>
|
|
88
|
+
Math.max(
|
|
89
|
+
0,
|
|
90
|
+
Math.min(
|
|
91
|
+
Math.min(matches.length, MAX_VISIBLE_ROWS) - 1,
|
|
92
|
+
cursor + 1,
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (key?.ctrl && key.name === "c") return finish(null);
|
|
97
|
+
|
|
98
|
+
switch (key?.name) {
|
|
99
|
+
case "escape":
|
|
100
|
+
return finish(null);
|
|
101
|
+
case "return":
|
|
102
|
+
return finish(matches[cursor] ?? null);
|
|
103
|
+
case "up":
|
|
104
|
+
cursor = Math.max(0, cursor - 1);
|
|
105
|
+
|
|
106
|
+
return render();
|
|
107
|
+
case "down":
|
|
108
|
+
cursor = stepDown();
|
|
109
|
+
|
|
110
|
+
return render();
|
|
111
|
+
case "backspace":
|
|
112
|
+
query = query.slice(0, -1);
|
|
113
|
+
|
|
114
|
+
return render();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (key?.ctrl) {
|
|
118
|
+
if (key.name === "p") {
|
|
119
|
+
cursor = Math.max(0, cursor - 1);
|
|
120
|
+
|
|
121
|
+
return render();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (key.name === "n") {
|
|
125
|
+
cursor = stepDown();
|
|
126
|
+
|
|
127
|
+
return render();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (character && character >= " " && character !== "") {
|
|
134
|
+
query += character;
|
|
135
|
+
cursor = 0;
|
|
136
|
+
render();
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
stdin.off("keypress", onKeypress);
|
|
140
|
+
stdin.setRawMode?.(false);
|
|
141
|
+
stdin.pause();
|
|
142
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
emitKeypressEvents(stdin);
|
|
147
|
+
stdin.setRawMode?.(true);
|
|
148
|
+
stdin.resume();
|
|
149
|
+
stdin.on("keypress", onKeypress);
|
|
150
|
+
render();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function relativeTime(iso: string | null): string | null {
|
|
155
|
+
if (!iso) return null;
|
|
156
|
+
const then = Date.parse(iso);
|
|
157
|
+
|
|
158
|
+
if (Number.isNaN(then)) return null;
|
|
159
|
+
const seconds = Math.max(0, Math.round((Date.now() - then) / 1000));
|
|
160
|
+
|
|
161
|
+
if (seconds < 60) return "just now";
|
|
162
|
+
|
|
163
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)}m ago`;
|
|
164
|
+
|
|
165
|
+
if (seconds < 86400) return `${Math.round(seconds / 3600)}h ago`;
|
|
166
|
+
|
|
167
|
+
return `${Math.round(seconds / 86400)}d ago`;
|
|
168
|
+
}
|