pi-studio 0.9.53 → 0.9.55
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/CHANGELOG.md +23 -0
- package/README.md +8 -3
- package/ROADMAP.md +12 -4
- package/client/studio-client.js +890 -166
- package/client/studio.css +191 -5
- package/index.ts +517 -147
- package/package.json +1 -1
- package/shared/studio-disk-revisions.js +558 -0
- package/shared/studio-file-watcher.js +181 -0
- package/shared/studio-side-question.js +32 -3
- package/shared/studio-workspace-state.js +4 -0
package/index.ts
CHANGED
|
@@ -23,6 +23,12 @@ import {
|
|
|
23
23
|
transformStudioMarkdownOutsideFences,
|
|
24
24
|
} from "./shared/studio-annotation-scanner.js";
|
|
25
25
|
import { stripStudioMarkdownHtmlComments } from "./shared/studio-markdown-html-comments.js";
|
|
26
|
+
import { createStudioFileWatcher } from "./shared/studio-file-watcher.js";
|
|
27
|
+
import {
|
|
28
|
+
readStudioDiskFileSnapshot,
|
|
29
|
+
saveStudioDiskFileAs,
|
|
30
|
+
saveStudioDiskFileIfRevision,
|
|
31
|
+
} from "./shared/studio-disk-revisions.js";
|
|
26
32
|
import { normalizeStudioMarkdownSmartFences } from "./shared/studio-markdown-fences.js";
|
|
27
33
|
import {
|
|
28
34
|
extractStandaloneLatexDefinitionsFromMarkdown,
|
|
@@ -68,9 +74,11 @@ import {
|
|
|
68
74
|
import {
|
|
69
75
|
buildStudioSideQuestionFollowUpPrompt,
|
|
70
76
|
buildStudioSideQuestionPrompt,
|
|
77
|
+
getStudioSideQuestionThinkingLevels,
|
|
71
78
|
normalizeStudioSideQuestionFocusKind,
|
|
72
79
|
normalizeStudioSideQuestionGatherScope,
|
|
73
80
|
normalizeStudioSideQuestionThinking,
|
|
81
|
+
resolveStudioSideQuestionThinking,
|
|
74
82
|
STUDIO_SIDE_QUESTION_FOCUS_MAX_CHARS,
|
|
75
83
|
STUDIO_SIDE_QUESTION_QUESTION_MAX_CHARS,
|
|
76
84
|
} from "./shared/studio-side-question.js";
|
|
@@ -114,7 +122,7 @@ type StudioQuartoPreviewStatus = "idle" | "starting" | "running" | "stopping" |
|
|
|
114
122
|
type StudioShowMeSourceKind = "selection" | "response" | "editor" | "context";
|
|
115
123
|
type StudioSideQuestionFocusKind = "selection" | "section" | "editor" | "response" | "none";
|
|
116
124
|
type StudioSideQuestionGatherScope = "none" | "folder" | "repo" | "custom";
|
|
117
|
-
type StudioSideQuestionThinking =
|
|
125
|
+
type StudioSideQuestionThinking = ModelThinkingLevel;
|
|
118
126
|
type StudioSideQuestionStatus = "idle" | "running" | "error";
|
|
119
127
|
|
|
120
128
|
interface StudioQuartoPreviewContext {
|
|
@@ -314,11 +322,24 @@ interface InitialStudioDocument {
|
|
|
314
322
|
path?: string;
|
|
315
323
|
draftId?: string;
|
|
316
324
|
resourceDir?: string;
|
|
325
|
+
diskRevision?: string;
|
|
326
|
+
watchFile?: boolean;
|
|
317
327
|
}
|
|
318
328
|
|
|
329
|
+
type StudioDiskWriteResult =
|
|
330
|
+
| { ok: true; path: string; revision: string; size: number }
|
|
331
|
+
| {
|
|
332
|
+
ok: false;
|
|
333
|
+
conflict?: boolean;
|
|
334
|
+
reason?: string;
|
|
335
|
+
path?: string;
|
|
336
|
+
currentRevision?: string | null;
|
|
337
|
+
message: string;
|
|
338
|
+
};
|
|
339
|
+
|
|
319
340
|
interface StudioLaunchSelection {
|
|
320
341
|
document: InitialStudioDocument;
|
|
321
|
-
kind: "document" | "pdf-preview";
|
|
342
|
+
kind: "document" | "pdf-preview" | "watched-preview";
|
|
322
343
|
mode?: StudioUiMode;
|
|
323
344
|
transient?: boolean;
|
|
324
345
|
skipWorkspaceRestore?: boolean;
|
|
@@ -719,12 +740,17 @@ interface SaveAsRequestMessage {
|
|
|
719
740
|
requestId: string;
|
|
720
741
|
path: string;
|
|
721
742
|
content: string;
|
|
743
|
+
overwrite?: boolean;
|
|
744
|
+
expectedRevision?: string;
|
|
722
745
|
}
|
|
723
746
|
|
|
724
747
|
interface SaveOverRequestMessage {
|
|
725
748
|
type: "save_over_request";
|
|
726
749
|
requestId: string;
|
|
750
|
+
path: string;
|
|
727
751
|
content: string;
|
|
752
|
+
expectedRevision?: string;
|
|
753
|
+
force?: boolean;
|
|
728
754
|
}
|
|
729
755
|
|
|
730
756
|
interface RefreshFromDiskRequestMessage {
|
|
@@ -733,6 +759,12 @@ interface RefreshFromDiskRequestMessage {
|
|
|
733
759
|
path?: string;
|
|
734
760
|
}
|
|
735
761
|
|
|
762
|
+
interface WatchFileSubscribeMessage {
|
|
763
|
+
type: "watch_file_subscribe";
|
|
764
|
+
path: string;
|
|
765
|
+
revision?: string;
|
|
766
|
+
}
|
|
767
|
+
|
|
736
768
|
interface SendToEditorRequestMessage {
|
|
737
769
|
type: "send_to_editor_request";
|
|
738
770
|
requestId: string;
|
|
@@ -821,6 +853,7 @@ type IncomingStudioMessage =
|
|
|
821
853
|
| SaveAsRequestMessage
|
|
822
854
|
| SaveOverRequestMessage
|
|
823
855
|
| RefreshFromDiskRequestMessage
|
|
856
|
+
| WatchFileSubscribeMessage
|
|
824
857
|
| SendToEditorRequestMessage
|
|
825
858
|
| GetFromEditorRequestMessage
|
|
826
859
|
| QuartoPreviewCheckRequestMessage
|
|
@@ -833,6 +866,7 @@ type IncomingStudioMessage =
|
|
|
833
866
|
|
|
834
867
|
const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
835
868
|
const PREVIEW_RENDER_MAX_CHARS = 400_000;
|
|
869
|
+
const STUDIO_WATCHED_FILE_MAX_CLIENTS = 16;
|
|
836
870
|
const STUDIO_COMPLETION_MAX_TEXT_CHARS = 250_000;
|
|
837
871
|
const STUDIO_COMPLETION_MAX_CONTEXT_CHARS = 12_000;
|
|
838
872
|
const STUDIO_COMPLETION_PREFIX_CHARS = 12_000;
|
|
@@ -2463,8 +2497,8 @@ function resolveStudioPath(pathArg: string, cwd: string): { ok: true; resolved:
|
|
|
2463
2497
|
return { ok: true, resolved, label: normalized };
|
|
2464
2498
|
}
|
|
2465
2499
|
|
|
2466
|
-
function readStudioFile(pathArg: string, cwd: string):
|
|
2467
|
-
| { ok: true; text: string; label: string; resolvedPath: string }
|
|
2500
|
+
function readStudioFile(pathArg: string, cwd: string, options?: { requireCanonicalPath?: boolean }):
|
|
2501
|
+
| { ok: true; text: string; label: string; resolvedPath: string; diskRevision: string }
|
|
2468
2502
|
| { ok: false; message: string } {
|
|
2469
2503
|
const resolved = resolveStudioPath(pathArg, cwd);
|
|
2470
2504
|
if (resolved.ok === false) {
|
|
@@ -2472,39 +2506,41 @@ function readStudioFile(pathArg: string, cwd: string):
|
|
|
2472
2506
|
}
|
|
2473
2507
|
|
|
2474
2508
|
try {
|
|
2475
|
-
const
|
|
2476
|
-
if (
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
const buf = readFileSync(resolved.resolved);
|
|
2489
|
-
// Heuristic: check the first 8KB for binary indicators
|
|
2490
|
-
const sample = buf.subarray(0, 8192);
|
|
2509
|
+
const snapshot = readStudioDiskFileSnapshot(resolved.resolved);
|
|
2510
|
+
if (options?.requireCanonicalPath) {
|
|
2511
|
+
const requestedPath = resolve(resolved.resolved);
|
|
2512
|
+
const canonicalPath = resolve(snapshot.path);
|
|
2513
|
+
const pathsMatch = process.platform === "win32"
|
|
2514
|
+
? requestedPath.toLowerCase() === canonicalPath.toLowerCase()
|
|
2515
|
+
: requestedPath === canonicalPath;
|
|
2516
|
+
if (!pathsMatch) {
|
|
2517
|
+
return { ok: false, message: `Could not read file: ${resolved.label} (the file location now resolves somewhere else)` };
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
// Heuristic: check the first 8KB for binary indicators.
|
|
2521
|
+
const sample = snapshot.buffer.subarray(0, 8192);
|
|
2491
2522
|
let nulCount = 0;
|
|
2492
2523
|
let controlCount = 0;
|
|
2493
2524
|
for (let i = 0; i < sample.length; i++) {
|
|
2494
2525
|
const b = sample[i];
|
|
2495
2526
|
if (b === 0x00) nulCount++;
|
|
2496
|
-
// Control chars excluding tab (0x09), newline (0x0A), carriage return (0x0D)
|
|
2527
|
+
// Control chars excluding tab (0x09), newline (0x0A), carriage return (0x0D).
|
|
2497
2528
|
else if (b < 0x08 || (b > 0x0D && b < 0x20 && b !== 0x1B)) controlCount++;
|
|
2498
2529
|
}
|
|
2499
2530
|
if (nulCount > 0 || (sample.length > 0 && controlCount / sample.length > 0.1)) {
|
|
2500
2531
|
return { ok: false, message: `File appears to be binary: ${resolved.label}` };
|
|
2501
2532
|
}
|
|
2502
|
-
|
|
2503
|
-
|
|
2533
|
+
return {
|
|
2534
|
+
ok: true,
|
|
2535
|
+
text: snapshot.buffer.toString("utf-8"),
|
|
2536
|
+
label: resolved.label,
|
|
2537
|
+
resolvedPath: snapshot.path,
|
|
2538
|
+
diskRevision: snapshot.revision,
|
|
2539
|
+
};
|
|
2504
2540
|
} catch (error) {
|
|
2505
2541
|
return {
|
|
2506
2542
|
ok: false,
|
|
2507
|
-
message: `
|
|
2543
|
+
message: `Could not read file: ${resolved.label} (${error instanceof Error ? error.message : String(error)})`,
|
|
2508
2544
|
};
|
|
2509
2545
|
}
|
|
2510
2546
|
}
|
|
@@ -2864,23 +2900,18 @@ function writeStudioPreviewExportFile(path: string | null, data: Buffer): { file
|
|
|
2864
2900
|
}
|
|
2865
2901
|
}
|
|
2866
2902
|
|
|
2867
|
-
function writeStudioFile(pathArg: string, cwd: string, content: string):
|
|
2868
|
-
| { ok: true; label: string; resolvedPath: string }
|
|
2869
|
-
| { ok: false; message: string } {
|
|
2870
|
-
const
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
}
|
|
2879
|
-
return {
|
|
2880
|
-
ok: false,
|
|
2881
|
-
message: `Failed to write file: ${resolved.label} (${error instanceof Error ? error.message : String(error)})`,
|
|
2882
|
-
};
|
|
2883
|
-
}
|
|
2903
|
+
function writeStudioFile(pathArg: string, cwd: string, content: string, overwrite = false, expectedRevision?: string):
|
|
2904
|
+
| { ok: true; label: string; resolvedPath: string; diskRevision: string }
|
|
2905
|
+
| { ok: false; conflict?: boolean; reason?: string; path?: string; currentRevision?: string | null; message: string } {
|
|
2906
|
+
const normalizedLabel = normalizePathInput(pathArg);
|
|
2907
|
+
const result = saveStudioDiskFileAs({ path: pathArg, cwd, content, overwrite, expectedRevision }) as StudioDiskWriteResult;
|
|
2908
|
+
if (result.ok === false) return result;
|
|
2909
|
+
return {
|
|
2910
|
+
ok: true,
|
|
2911
|
+
label: normalizedLabel || basename(result.path),
|
|
2912
|
+
resolvedPath: result.path,
|
|
2913
|
+
diskRevision: result.revision,
|
|
2914
|
+
};
|
|
2884
2915
|
}
|
|
2885
2916
|
|
|
2886
2917
|
function writeStudioSideQuestionMarkdownFile(
|
|
@@ -7961,6 +7992,13 @@ async function respondLocalPreviewLinkJson(
|
|
|
7961
7992
|
}
|
|
7962
7993
|
|
|
7963
7994
|
const action = (requestUrl.searchParams.get("action") ?? "resolve").trim().toLowerCase();
|
|
7995
|
+
const originDocId = (requestUrl.searchParams.get("docId") ?? "").trim();
|
|
7996
|
+
const originDocument = originDocId ? readTransientStudioDocument(originDocId) : null;
|
|
7997
|
+
const originatesFromWatchedPreview = requestUrl.searchParams.get("watchedFile") === "1" || originDocument?.watchFile === true;
|
|
7998
|
+
if (action === "document" && originatesFromWatchedPreview) {
|
|
7999
|
+
respondJson(res, 409, { ok: false, error: "Open here is unavailable in a watched preview. Open a separate editable file tab instead." });
|
|
8000
|
+
return;
|
|
8001
|
+
}
|
|
7964
8002
|
const basePayload = {
|
|
7965
8003
|
ok: true,
|
|
7966
8004
|
kind: resource.kind,
|
|
@@ -7990,10 +8028,14 @@ async function respondLocalPreviewLinkJson(
|
|
|
7990
8028
|
return;
|
|
7991
8029
|
}
|
|
7992
8030
|
|
|
7993
|
-
if (action !== "document" && action !== "editor-url") {
|
|
8031
|
+
if (action !== "document" && action !== "editor-url" && action !== "watch-url") {
|
|
7994
8032
|
respondJson(res, 400, { ok: false, error: "Unsupported local link action." });
|
|
7995
8033
|
return;
|
|
7996
8034
|
}
|
|
8035
|
+
if (action === "watch-url" && resource.kind !== "text") {
|
|
8036
|
+
respondJson(res, 400, { ok: false, error: "Follow-changes preview is available only for readable text files." });
|
|
8037
|
+
return;
|
|
8038
|
+
}
|
|
7997
8039
|
if (resource.kind !== "text" && resource.kind !== "office") {
|
|
7998
8040
|
respondJson(res, 400, { ok: false, error: "This local resource is not a document Studio can load into the editor." });
|
|
7999
8041
|
return;
|
|
@@ -8038,6 +8080,7 @@ async function respondLocalPreviewLinkJson(
|
|
|
8038
8080
|
source: "file",
|
|
8039
8081
|
path: file.resolvedPath,
|
|
8040
8082
|
resourceDir: documentResourceDir,
|
|
8083
|
+
diskRevision: file.diskRevision,
|
|
8041
8084
|
};
|
|
8042
8085
|
}
|
|
8043
8086
|
if (action === "document") {
|
|
@@ -8047,16 +8090,27 @@ async function respondLocalPreviewLinkJson(
|
|
|
8047
8090
|
label: document.label,
|
|
8048
8091
|
converted,
|
|
8049
8092
|
resourceDir: documentResourceDir,
|
|
8093
|
+
diskRevision: document.diskRevision || null,
|
|
8050
8094
|
});
|
|
8051
8095
|
return;
|
|
8052
8096
|
}
|
|
8053
8097
|
|
|
8098
|
+
if (action === "watch-url") {
|
|
8099
|
+
if (responseText.length > PREVIEW_RENDER_MAX_CHARS) {
|
|
8100
|
+
respondJson(res, 413, { ok: false, error: `Watched preview source exceeds ${PREVIEW_RENDER_MAX_CHARS.toLocaleString()} characters.` });
|
|
8101
|
+
return;
|
|
8102
|
+
}
|
|
8103
|
+
document = { ...document, watchFile: true };
|
|
8104
|
+
}
|
|
8054
8105
|
const docId = storeTransientStudioDocument(document);
|
|
8055
8106
|
respondJson(res, 200, {
|
|
8056
8107
|
...basePayload,
|
|
8057
8108
|
converted,
|
|
8058
8109
|
resourceDir: documentResourceDir,
|
|
8059
|
-
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, {
|
|
8110
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, {
|
|
8111
|
+
skipWorkspaceRestore: true,
|
|
8112
|
+
paneFocus: action === "watch-url" ? "right" : undefined,
|
|
8113
|
+
}),
|
|
8060
8114
|
});
|
|
8061
8115
|
}
|
|
8062
8116
|
|
|
@@ -9956,21 +10010,35 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
9956
10010
|
msg.type === "save_as_request" &&
|
|
9957
10011
|
typeof msg.requestId === "string" &&
|
|
9958
10012
|
typeof msg.path === "string" &&
|
|
9959
|
-
typeof msg.content === "string"
|
|
10013
|
+
typeof msg.content === "string" &&
|
|
10014
|
+
(msg.overwrite === undefined || typeof msg.overwrite === "boolean") &&
|
|
10015
|
+
(msg.expectedRevision === undefined || typeof msg.expectedRevision === "string")
|
|
9960
10016
|
) {
|
|
9961
10017
|
return {
|
|
9962
10018
|
type: "save_as_request",
|
|
9963
10019
|
requestId: msg.requestId,
|
|
9964
10020
|
path: msg.path,
|
|
9965
10021
|
content: msg.content,
|
|
10022
|
+
overwrite: msg.overwrite === true,
|
|
10023
|
+
expectedRevision: typeof msg.expectedRevision === "string" ? msg.expectedRevision : undefined,
|
|
9966
10024
|
};
|
|
9967
10025
|
}
|
|
9968
10026
|
|
|
9969
|
-
if (
|
|
10027
|
+
if (
|
|
10028
|
+
msg.type === "save_over_request"
|
|
10029
|
+
&& typeof msg.requestId === "string"
|
|
10030
|
+
&& typeof msg.path === "string"
|
|
10031
|
+
&& typeof msg.content === "string"
|
|
10032
|
+
&& (msg.expectedRevision === undefined || typeof msg.expectedRevision === "string")
|
|
10033
|
+
&& (msg.force === undefined || typeof msg.force === "boolean")
|
|
10034
|
+
) {
|
|
9970
10035
|
return {
|
|
9971
10036
|
type: "save_over_request",
|
|
9972
10037
|
requestId: msg.requestId,
|
|
10038
|
+
path: msg.path,
|
|
9973
10039
|
content: msg.content,
|
|
10040
|
+
expectedRevision: typeof msg.expectedRevision === "string" ? msg.expectedRevision : undefined,
|
|
10041
|
+
force: msg.force === true,
|
|
9974
10042
|
};
|
|
9975
10043
|
}
|
|
9976
10044
|
|
|
@@ -9986,6 +10054,19 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
9986
10054
|
};
|
|
9987
10055
|
}
|
|
9988
10056
|
|
|
10057
|
+
if (
|
|
10058
|
+
msg.type === "watch_file_subscribe"
|
|
10059
|
+
&& typeof msg.path === "string"
|
|
10060
|
+
&& msg.path.length <= 16_384
|
|
10061
|
+
&& (msg.revision === undefined || typeof msg.revision === "string")
|
|
10062
|
+
) {
|
|
10063
|
+
return {
|
|
10064
|
+
type: "watch_file_subscribe",
|
|
10065
|
+
path: msg.path,
|
|
10066
|
+
revision: typeof msg.revision === "string" ? msg.revision : undefined,
|
|
10067
|
+
};
|
|
10068
|
+
}
|
|
10069
|
+
|
|
9989
10070
|
if (msg.type === "send_to_editor_request" && typeof msg.requestId === "string" && typeof msg.content === "string") {
|
|
9990
10071
|
return {
|
|
9991
10072
|
type: "send_to_editor_request",
|
|
@@ -11322,6 +11403,7 @@ function buildStudioRelativeUrl(
|
|
|
11322
11403
|
if (doc?.path) params.set("docPath", doc.path);
|
|
11323
11404
|
if (doc?.draftId) params.set("draftId", doc.draftId);
|
|
11324
11405
|
if (doc?.resourceDir) params.set("resourceDir", doc.resourceDir);
|
|
11406
|
+
if (doc?.watchFile) params.set("watchFile", "1");
|
|
11325
11407
|
if (options?.skipWorkspaceRestore) params.set("skipWorkspaceRestore", "1");
|
|
11326
11408
|
if (options?.paneFocus) params.set("paneFocus", options.paneFocus);
|
|
11327
11409
|
return `/?${params.toString()}`;
|
|
@@ -11411,16 +11493,36 @@ function resolveRequestedStudioDocumentFromUrl(
|
|
|
11411
11493
|
const requestedLabel = (requestUrl.searchParams.get("docLabel") ?? "").trim();
|
|
11412
11494
|
const requestedDraftId = (requestUrl.searchParams.get("draftId") ?? "").trim();
|
|
11413
11495
|
const requestedResourceDir = (requestUrl.searchParams.get("resourceDir") ?? "").trim();
|
|
11496
|
+
const requestedWatchFile = requestUrl.searchParams.get("watchFile") === "1";
|
|
11414
11497
|
|
|
11415
11498
|
if (requestedPath) {
|
|
11416
11499
|
const file = readStudioFile(requestedPath, studioCwd);
|
|
11417
|
-
|
|
11500
|
+
const requestedCanonicalTextPath = resolve(requestedPath);
|
|
11501
|
+
const resolvedFilePath = file.ok !== false ? resolve(file.resolvedPath) : "";
|
|
11502
|
+
const watchedPathStayedCanonical = !requestedWatchFile || (
|
|
11503
|
+
process.platform === "win32"
|
|
11504
|
+
? requestedCanonicalTextPath.toLowerCase() === resolvedFilePath.toLowerCase()
|
|
11505
|
+
: requestedCanonicalTextPath === resolvedFilePath
|
|
11506
|
+
);
|
|
11507
|
+
if (file.ok !== false && watchedPathStayedCanonical) {
|
|
11418
11508
|
return {
|
|
11419
11509
|
text: file.text,
|
|
11420
11510
|
label: requestedLabel || file.label,
|
|
11421
11511
|
source: "file",
|
|
11422
11512
|
path: file.resolvedPath,
|
|
11423
11513
|
resourceDir: requestedResourceDir || fallback?.resourceDir || studioCwd,
|
|
11514
|
+
diskRevision: file.diskRevision,
|
|
11515
|
+
watchFile: requestedWatchFile,
|
|
11516
|
+
};
|
|
11517
|
+
}
|
|
11518
|
+
if (requestedWatchFile && isAbsolute(requestedPath)) {
|
|
11519
|
+
return {
|
|
11520
|
+
text: fallback?.watchFile && fallback.path === requestedPath ? fallback.text : "",
|
|
11521
|
+
label: requestedLabel || basename(requestedPath),
|
|
11522
|
+
source: "file",
|
|
11523
|
+
path: resolve(requestedPath),
|
|
11524
|
+
resourceDir: requestedResourceDir || dirname(resolve(requestedPath)),
|
|
11525
|
+
watchFile: true,
|
|
11424
11526
|
};
|
|
11425
11527
|
}
|
|
11426
11528
|
}
|
|
@@ -11705,6 +11807,8 @@ function buildStudioHtml(
|
|
|
11705
11807
|
const initialPath = escapeHtmlForInline(initialDocument?.path ?? "");
|
|
11706
11808
|
const initialDraftId = escapeHtmlForInline(initialDocument?.draftId ?? "");
|
|
11707
11809
|
const initialResourceDir = escapeHtmlForInline(initialDocument?.resourceDir ?? "");
|
|
11810
|
+
const initialDiskRevision = escapeHtmlForInline(initialDocument?.diskRevision ?? "");
|
|
11811
|
+
const initialWatchFile = initialDocument?.watchFile === true ? "1" : "0";
|
|
11708
11812
|
const initialModel = escapeHtmlForInline(initialModelLabel ?? "none");
|
|
11709
11813
|
const initialTerminal = escapeHtmlForInline(initialTerminalLabel ?? "unknown");
|
|
11710
11814
|
const initialTerminalDetailAttr = escapeHtmlForInline(initialTerminalDetail ?? initialTerminalLabel ?? "unknown");
|
|
@@ -11781,12 +11885,12 @@ ${cssVarsBlock}
|
|
|
11781
11885
|
</style>
|
|
11782
11886
|
<link rel="stylesheet" href="${stylesheetHref}" />
|
|
11783
11887
|
</head>
|
|
11784
|
-
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-initial-draft-id="${initialDraftId}" data-initial-resource-dir="${initialResourceDir}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-terminal-detail="${initialTerminalDetailAttr}" data-theme-name="${initialTheme}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}" data-ssh-session="${initialSshSession}">
|
|
11888
|
+
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-initial-draft-id="${initialDraftId}" data-initial-resource-dir="${initialResourceDir}" data-initial-disk-revision="${initialDiskRevision}" data-watched-file-preview="${initialWatchFile}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-terminal-detail="${initialTerminalDetailAttr}" data-theme-name="${initialTheme}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}" data-ssh-session="${initialSshSession}">
|
|
11785
11889
|
<header id="studioHeader">
|
|
11786
11890
|
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
|
|
11787
11891
|
<div class="controls">
|
|
11788
|
-
<button id="saveAsBtn" type="button" title="Save editor content to a new file path. Cmd/Ctrl+S falls back here
|
|
11789
|
-
<button id="saveOverBtn" type="button" title="
|
|
11892
|
+
<button id="saveAsBtn" type="button" title="Save editor content to a new file path. Shortcut: Cmd/Ctrl+Shift+S; Cmd/Ctrl+S falls back here for new drafts.">Save editor as…</button>
|
|
11893
|
+
<button id="saveOverBtn" type="button" title="Save only when the current file still matches Studio's disk revision. Shortcut: Cmd/Ctrl+S.">Save editor</button>
|
|
11790
11894
|
<button id="refreshFromDiskBtn" type="button" title="Reload the current file-backed document from disk.">Refresh from disk</button>
|
|
11791
11895
|
<button id="clearWorkspaceBtn" type="button" title="Clear editor text and reset this tab to a fresh blank draft. Saved files and responses are not changed.">Reset editor</button>
|
|
11792
11896
|
<button id="importFileBtn" type="button" title="Import a file as an editable copy.">Import file copy…</button>
|
|
@@ -11804,10 +11908,12 @@ ${cssVarsBlock}
|
|
|
11804
11908
|
<section id="leftPane">
|
|
11805
11909
|
<div id="leftSectionHeader" class="section-header">
|
|
11806
11910
|
<div class="section-header-main">
|
|
11807
|
-
<
|
|
11808
|
-
<
|
|
11809
|
-
|
|
11810
|
-
|
|
11911
|
+
<span id="editorViewSelectWrap" class="studio-header-select-wrap">
|
|
11912
|
+
<select id="editorViewSelect" aria-label="Editor view mode" title="Editor view mode. Shortcut: F7 when the editor pane is active; F6 switches panes.">
|
|
11913
|
+
<option value="markdown" selected>Editor (Raw)</option>
|
|
11914
|
+
<option value="preview">Editor (Preview)</option>
|
|
11915
|
+
</select>
|
|
11916
|
+
</span>
|
|
11811
11917
|
</div>
|
|
11812
11918
|
<div class="section-header-actions">
|
|
11813
11919
|
<button id="leftFocusBtn" class="pane-focus-btn" type="button" title="Show only the editor pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
|
|
@@ -11835,7 +11941,7 @@ ${cssVarsBlock}
|
|
|
11835
11941
|
</div>
|
|
11836
11942
|
<div class="source-actions-row repl-action-line" hidden>
|
|
11837
11943
|
<button id="sendReplBtn" type="button" hidden title="Send the current selection, or the full editor text, to the active REPL session shown in the right pane.">Send to REPL</button>
|
|
11838
|
-
<select id="replSendModeSelect" hidden aria-label="REPL send mode" title="Choose how Send to REPL interprets the editor text.">
|
|
11944
|
+
<select id="replSendModeSelect" class="studio-flat-select" hidden aria-label="REPL send mode" title="Choose how Send to REPL interprets the editor text.">
|
|
11839
11945
|
<option value="raw" selected>Send mode: Raw</option>
|
|
11840
11946
|
<option value="literate">Send mode: Literate</option>
|
|
11841
11947
|
</select>
|
|
@@ -11844,11 +11950,11 @@ ${cssVarsBlock}
|
|
|
11844
11950
|
<button id="copyDraftBtn" type="button" title="Copy the current editor text to the clipboard.">Copy</button>
|
|
11845
11951
|
<button id="suggestCompletionBtn" type="button" title="Ask the current model for a short completion at the editor cursor. Shortcut: Option/Alt+Tab where available, or Cmd/Ctrl+Shift+Space from the editor.">Suggest</button>
|
|
11846
11952
|
<button id="suggestCompletionOptionsBtn" type="button" hidden title="Suggestion context options">▾</button>
|
|
11847
|
-
<select id="completionContextSelect" hidden aria-label="Suggestion context mode" title="Choose how much context Suggest includes.">
|
|
11953
|
+
<select id="completionContextSelect" class="studio-flat-select" hidden aria-label="Suggestion context mode" title="Choose how much context Suggest includes.">
|
|
11848
11954
|
<option value="cursor" selected>Context: editor only</option>
|
|
11849
11955
|
<option value="session">Context: editor + latest response</option>
|
|
11850
11956
|
</select>
|
|
11851
|
-
<select id="completionModelSelect" hidden aria-label="Suggestion model" title="Choose the model used for Suggest. Suggestions use direct completion with thinking off and do not change the main Pi model.">
|
|
11957
|
+
<select id="completionModelSelect" class="studio-flat-select" hidden aria-label="Suggestion model" title="Choose the model used for Suggest. Suggestions use direct completion with thinking off and do not change the main Pi model.">
|
|
11852
11958
|
<option value="current" selected>Suggestion model: current Pi model</option>
|
|
11853
11959
|
</select>
|
|
11854
11960
|
<button id="openCompanionBtn" type="button" title="Open a blank editor-only Studio tab.">New editor tab</button>
|
|
@@ -11856,7 +11962,7 @@ ${cssVarsBlock}
|
|
|
11856
11962
|
</div>
|
|
11857
11963
|
<div class="source-actions-row">
|
|
11858
11964
|
<button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Annotation header</button>
|
|
11859
|
-
<select id="annotationModeSelect" aria-label="Inline annotation visibility mode" title="On: keep and send [an: ...] markers. Hide: keep markers in the editor, hide them in preview, and strip before Run/Critique.">
|
|
11965
|
+
<select id="annotationModeSelect" class="studio-flat-select" aria-label="Inline annotation visibility mode" title="On: keep and send [an: ...] markers. Hide: keep markers in the editor, hide them in preview, and strip before Run/Critique.">
|
|
11860
11966
|
<option value="on" selected>Inline annotations: On</option>
|
|
11861
11967
|
<option value="off">Inline annotations: Hide</option>
|
|
11862
11968
|
</select>
|
|
@@ -11864,7 +11970,7 @@ ${cssVarsBlock}
|
|
|
11864
11970
|
<button id="saveAnnotatedBtn" type="button" title="Save full editor content (including [an: ...] markers) as a .annotated.md file.">Save .annotated.md</button>
|
|
11865
11971
|
</div>
|
|
11866
11972
|
<div class="source-actions-row">
|
|
11867
|
-
<select id="lensSelect" aria-label="Critique focus">
|
|
11973
|
+
<select id="lensSelect" class="studio-flat-select" aria-label="Critique focus">
|
|
11868
11974
|
<option value="auto" selected>Critique: Auto</option>
|
|
11869
11975
|
<option value="writing">Critique: Writing</option>
|
|
11870
11976
|
<option value="code">Critique: Code</option>
|
|
@@ -11874,7 +11980,7 @@ ${cssVarsBlock}
|
|
|
11874
11980
|
<button id="showMeResponseBtn" type="button" hidden title="Explain the response displayed in the right pane using the smallest useful visual or structural representation.">Explain displayed response</button>
|
|
11875
11981
|
<button id="askAsideBtn" type="button" title="Open a separate side-question thread without adding it to the main Pi conversation.">Side question</button>
|
|
11876
11982
|
<button id="quizBtn" type="button" title="Open an active quiz for the current editor selection or document.">Quiz me</button>
|
|
11877
|
-
<select id="highlightSelect" aria-label="Editor syntax highlighting">
|
|
11983
|
+
<select id="highlightSelect" class="studio-flat-select" aria-label="Editor syntax highlighting">
|
|
11878
11984
|
<option value="off">Syntax highlight: Off</option>
|
|
11879
11985
|
<option value="bash">Syntax highlight: Bash</option>
|
|
11880
11986
|
<option value="c">Syntax highlight: C</option>
|
|
@@ -11904,11 +12010,11 @@ ${cssVarsBlock}
|
|
|
11904
12010
|
<option value="xml">Syntax highlight: XML</option>
|
|
11905
12011
|
<option value="yaml">Syntax highlight: YAML</option>
|
|
11906
12012
|
</select>
|
|
11907
|
-
<select id="lineNumbersSelect" aria-label="Editor line numbers">
|
|
12013
|
+
<select id="lineNumbersSelect" class="studio-flat-select" aria-label="Editor line numbers">
|
|
11908
12014
|
<option value="off">Line numbers: Off</option>
|
|
11909
12015
|
<option value="on" selected>Line numbers: On</option>
|
|
11910
12016
|
</select>
|
|
11911
|
-
<select id="editorFontSizeSelect" aria-label="Editor text size" title="Adjust raw editor text size.">
|
|
12017
|
+
<select id="editorFontSizeSelect" class="studio-flat-select" aria-label="Editor text size" title="Adjust raw editor text size.">
|
|
11912
12018
|
<option value="10">Editor text: 10px</option>
|
|
11913
12019
|
<option value="11">Editor text: 11px</option>
|
|
11914
12020
|
<option value="12" selected>Editor text: 12px</option>
|
|
@@ -12006,20 +12112,23 @@ ${cssVarsBlock}
|
|
|
12006
12112
|
<section id="rightPane">
|
|
12007
12113
|
<div id="rightSectionHeader" class="section-header">
|
|
12008
12114
|
<div class="section-header-main">
|
|
12009
|
-
<
|
|
12010
|
-
<
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12115
|
+
<span id="rightViewSelectWrap" class="studio-header-select-wrap">
|
|
12116
|
+
<select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+1–8 switches directly between all right-pane views. Cmd/Ctrl+Alt+P/E/W/F/Q keep mnemonic shortcuts for Preview, Editor Preview, Working, Files, and Side questions.">
|
|
12117
|
+
<option value="markdown">Response (Raw)</option>
|
|
12118
|
+
<option value="preview" selected>Response (Preview)</option>
|
|
12119
|
+
<option value="editor-preview">Editor (Preview)</option>
|
|
12120
|
+
<option value="editor-quarto-preview" hidden>Editor (Quarto Preview)</option>
|
|
12121
|
+
<option value="trace">Working</option>
|
|
12122
|
+
<option value="changes">Changes</option>
|
|
12123
|
+
<option value="files">Files</option>
|
|
12124
|
+
<option value="repl">REPL</option>
|
|
12125
|
+
<option value="side-questions">Side questions</option>
|
|
12126
|
+
</select>
|
|
12127
|
+
</span>
|
|
12020
12128
|
</div>
|
|
12021
12129
|
<div class="section-header-actions">
|
|
12022
12130
|
<button id="rightFocusBtn" class="pane-focus-btn" type="button" title="Show only the response pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
|
|
12131
|
+
<button id="watchedOpenEditableBtn" type="button" hidden title="Open this watched file in a separate editable Studio tab with conflict-safe saving.">Open editable tab</button>
|
|
12023
12132
|
<span id="exportPreviewControls" class="export-preview-controls">
|
|
12024
12133
|
<button id="exportPdfBtn" class="export-preview-trigger" type="button" aria-haspopup="menu" aria-expanded="false" title="Choose a format and export the current right-pane preview.">Export right preview</button>
|
|
12025
12134
|
<div id="exportPreviewMenu" class="export-preview-menu" role="menu" hidden>
|
|
@@ -12042,28 +12151,34 @@ ${cssVarsBlock}
|
|
|
12042
12151
|
<div class="response-wrap">
|
|
12043
12152
|
<div id="responseActions" class="response-actions">
|
|
12044
12153
|
<div class="response-actions-row response-options-row">
|
|
12045
|
-
<
|
|
12046
|
-
<
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
<
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
|
|
12154
|
+
<span id="followSelectWrap" class="studio-menu-select-wrap response-option-select-wrap">
|
|
12155
|
+
<select id="followSelect" aria-label="Auto-update response">
|
|
12156
|
+
<option value="on" selected>Auto-update response: On</option>
|
|
12157
|
+
<option value="off">Auto-update response: Off</option>
|
|
12158
|
+
</select>
|
|
12159
|
+
</span>
|
|
12160
|
+
<span id="responseHighlightSelectWrap" class="studio-menu-select-wrap response-option-select-wrap">
|
|
12161
|
+
<select id="responseHighlightSelect" aria-label="Response markdown highlighting">
|
|
12162
|
+
<option value="off">Syntax highlight: Off</option>
|
|
12163
|
+
<option value="on" selected>Syntax highlight: On</option>
|
|
12164
|
+
</select>
|
|
12165
|
+
</span>
|
|
12166
|
+
<span id="responseFontSizeSelectWrap" class="studio-menu-select-wrap response-option-select-wrap">
|
|
12167
|
+
<select id="responseFontSizeSelect" aria-label="Response text size" title="Adjust right-pane response, preview, and working text size.">
|
|
12168
|
+
<option value="11">Response text: 11px</option>
|
|
12169
|
+
<option value="12">Response text: 12px</option>
|
|
12170
|
+
<option value="12.5">Response text: 12.5px</option>
|
|
12171
|
+
<option value="13">Response text: 13px</option>
|
|
12172
|
+
<option value="13.5" selected>Response text: 13.5px</option>
|
|
12173
|
+
<option value="14">Response text: 14px</option>
|
|
12174
|
+
<option value="14.5">Response text: 14.5px</option>
|
|
12175
|
+
<option value="15">Response text: 15px</option>
|
|
12176
|
+
<option value="15.5">Response text: 15.5px</option>
|
|
12177
|
+
<option value="16">Response text: 16px</option>
|
|
12178
|
+
<option value="18">Response text: 18px</option>
|
|
12179
|
+
<option value="20">Response text: 20px</option>
|
|
12180
|
+
</select>
|
|
12181
|
+
</span>
|
|
12067
12182
|
</div>
|
|
12068
12183
|
<div class="response-actions-row history-row">
|
|
12069
12184
|
<button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Fetch latest response</button>
|
|
@@ -12137,7 +12252,8 @@ ${cssVarsBlock}
|
|
|
12137
12252
|
<section class="shortcuts-group">
|
|
12138
12253
|
<h3>Editor</h3>
|
|
12139
12254
|
<dl>
|
|
12140
|
-
<div><dt>Cmd/Ctrl+S</dt><dd>Save editor</dd></div>
|
|
12255
|
+
<div><dt>Cmd/Ctrl+S</dt><dd>Save editor when the disk revision still matches</dd></div>
|
|
12256
|
+
<div><dt>Cmd/Ctrl+Shift+S</dt><dd>Save editor as a new file</dd></div>
|
|
12141
12257
|
<div class="shortcuts-full-only"><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
|
|
12142
12258
|
<div><dt>Option/Alt+Tab or Cmd/Ctrl+Shift+Space</dt><dd>Suggest a completion at the editor cursor</dd></div>
|
|
12143
12259
|
<div><dt>Tab</dt><dd>Insert a visible completion suggestion; otherwise indent selected editor text</dd></div>
|
|
@@ -12213,6 +12329,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
12213
12329
|
let serverState: StudioServerState | null = null;
|
|
12214
12330
|
const studioWorkspaceStateStore = createStudioWorkspaceStateStore();
|
|
12215
12331
|
const studioResourceGrantRegistry = createStudioResourceGrantRegistry();
|
|
12332
|
+
const studioFileWatchers = new Map<WebSocket, ReturnType<typeof createStudioFileWatcher>>();
|
|
12333
|
+
const studioWatchedClientPaths = new Map<WebSocket, string>();
|
|
12334
|
+
const studioFileWatchSubscriptionGenerations = new Map<WebSocket, number>();
|
|
12216
12335
|
let activeRequest: ActiveStudioRequest | null = null;
|
|
12217
12336
|
let studioDirectRunChain: StudioDirectRunChain | null = null;
|
|
12218
12337
|
let queuedStudioDirectRequests: QueuedStudioDirectRequest[] = [];
|
|
@@ -12273,6 +12392,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
12273
12392
|
actionRequestId: null,
|
|
12274
12393
|
};
|
|
12275
12394
|
|
|
12395
|
+
const closeStudioFileWatcher = async (client: WebSocket): Promise<void> => {
|
|
12396
|
+
const watcher = studioFileWatchers.get(client);
|
|
12397
|
+
if (!watcher) return;
|
|
12398
|
+
studioFileWatchers.delete(client);
|
|
12399
|
+
await watcher.close().catch(() => undefined);
|
|
12400
|
+
};
|
|
12401
|
+
|
|
12402
|
+
const closeAllStudioFileWatchers = async (): Promise<void> => {
|
|
12403
|
+
const clients = Array.from(studioFileWatchers.keys());
|
|
12404
|
+
await Promise.all(clients.map((client) => closeStudioFileWatcher(client)));
|
|
12405
|
+
// Keep socket capability metadata until each socket is closed. Clearing it
|
|
12406
|
+
// while an authenticated socket is still open would briefly make a watched
|
|
12407
|
+
// preview look writable to the save handlers.
|
|
12408
|
+
};
|
|
12409
|
+
|
|
12276
12410
|
const recordStudioDocumentResourceGrants = (document: InitialStudioDocument, cwd = studioCwd) => {
|
|
12277
12411
|
if (document.path) {
|
|
12278
12412
|
try {
|
|
@@ -13573,6 +13707,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
13573
13707
|
}
|
|
13574
13708
|
: null;
|
|
13575
13709
|
|
|
13710
|
+
const getCurrentStudioSideQuestionThinkingLevels = (): StudioSideQuestionThinking[] =>
|
|
13711
|
+
getStudioSideQuestionThinkingLevels(latestModelRequestCtx?.model ?? lastCommandCtx?.model) as StudioSideQuestionThinking[];
|
|
13712
|
+
|
|
13576
13713
|
const getStudioThemeOptions = () => {
|
|
13577
13714
|
try {
|
|
13578
13715
|
const themes = lastCommandCtx?.ui?.getAllThemes?.() ?? [];
|
|
@@ -13608,6 +13745,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
13608
13745
|
modelLabel: currentModelLabel,
|
|
13609
13746
|
currentModel: getCurrentStudioModelDescriptor(),
|
|
13610
13747
|
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
13748
|
+
sideQuestionThinkingLevels: getCurrentStudioSideQuestionThinkingLevels(),
|
|
13611
13749
|
piModels: modelOptions,
|
|
13612
13750
|
suggestionModels: modelOptions,
|
|
13613
13751
|
currentTheme: getCurrentStudioThemeDescriptor(),
|
|
@@ -13914,6 +14052,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
13914
14052
|
const payload: Record<string, unknown> = {
|
|
13915
14053
|
type: "side_question_state",
|
|
13916
14054
|
state: getStudioSideQuestionPublicState(),
|
|
14055
|
+
sideQuestionThinkingLevels: getCurrentStudioSideQuestionThinkingLevels(),
|
|
13917
14056
|
webSearchAvailable: Boolean(String(process.env.BRAVE_API_KEY || "").trim()),
|
|
13918
14057
|
};
|
|
13919
14058
|
if (client) payload.availablePiTools = getPublicStudioSideQuestionToolCatalog();
|
|
@@ -14135,6 +14274,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14135
14274
|
const model = latestModelRequestCtx?.model ?? ctx.model;
|
|
14136
14275
|
if (!model) throw new Error("No active Pi model is available for side questions.");
|
|
14137
14276
|
await resolveStudioModelRequestAuth({ model, modelRegistry: ctx.modelRegistry }, model);
|
|
14277
|
+
const thinking = resolveStudioSideQuestionThinking(model, context.thinking) as StudioSideQuestionThinking;
|
|
14138
14278
|
const gitSnapshot = context.gitContext
|
|
14139
14279
|
? await captureStudioSideQuestionGitSnapshot(contextRoot, { runGit: runStudioSideQuestionGitCommand }) as StudioSideQuestionGitSnapshot
|
|
14140
14280
|
: null;
|
|
@@ -14173,7 +14313,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14173
14313
|
customTools: tools,
|
|
14174
14314
|
sessionManager: sideSessionManager,
|
|
14175
14315
|
model,
|
|
14176
|
-
thinking
|
|
14316
|
+
thinking,
|
|
14177
14317
|
inheritedPrompt: stripStudioDynamicSystemPromptFooter(ctx.getSystemPrompt()),
|
|
14178
14318
|
});
|
|
14179
14319
|
session = agentRuntime.session;
|
|
@@ -14182,7 +14322,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14182
14322
|
cwd: workingDirectory,
|
|
14183
14323
|
sessionManager: sideSessionManager,
|
|
14184
14324
|
model,
|
|
14185
|
-
thinkingLevel:
|
|
14325
|
+
thinkingLevel: thinking,
|
|
14186
14326
|
tools: activeToolNames,
|
|
14187
14327
|
customTools: tools,
|
|
14188
14328
|
resourceLoader: createStudioSideQuestionResourceLoader(ctx),
|
|
@@ -14215,7 +14355,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14215
14355
|
tools: selectedPiTools,
|
|
14216
14356
|
},
|
|
14217
14357
|
modelLabel: formatStudioModelOptionLabel(model),
|
|
14218
|
-
thinking
|
|
14358
|
+
thinking,
|
|
14219
14359
|
messages: [],
|
|
14220
14360
|
activity: [],
|
|
14221
14361
|
error: context.webSearch && !webAvailable ? "Web search was requested but BRAVE_API_KEY is not configured; this thread will use local context only." : "",
|
|
@@ -14393,6 +14533,92 @@ export default function (pi: ExtensionAPI) {
|
|
|
14393
14533
|
}
|
|
14394
14534
|
};
|
|
14395
14535
|
|
|
14536
|
+
const subscribeStudioWatchedFile = async (client: WebSocket, msg: WatchFileSubscribeMessage): Promise<void> => {
|
|
14537
|
+
const subscriptionGeneration = (studioFileWatchSubscriptionGenerations.get(client) ?? 0) + 1;
|
|
14538
|
+
studioFileWatchSubscriptionGenerations.set(client, subscriptionGeneration);
|
|
14539
|
+
await closeStudioFileWatcher(client);
|
|
14540
|
+
if (studioFileWatchSubscriptionGenerations.get(client) !== subscriptionGeneration || client.readyState !== WebSocket.OPEN) return;
|
|
14541
|
+
if (studioFileWatchers.size >= STUDIO_WATCHED_FILE_MAX_CLIENTS) {
|
|
14542
|
+
sendToClient(client, {
|
|
14543
|
+
type: "watched_file_error",
|
|
14544
|
+
path: msg.path,
|
|
14545
|
+
message: `Studio watched-file client limit reached (${STUDIO_WATCHED_FILE_MAX_CLIENTS}).`,
|
|
14546
|
+
});
|
|
14547
|
+
return;
|
|
14548
|
+
}
|
|
14549
|
+
const canonicalPath = resolve(msg.path);
|
|
14550
|
+
const clientWatchedPath = studioWatchedClientPaths.get(client) ?? "";
|
|
14551
|
+
if (clientWatchedPath !== canonicalPath) {
|
|
14552
|
+
sendToClient(client, {
|
|
14553
|
+
type: "watched_file_error",
|
|
14554
|
+
path: canonicalPath,
|
|
14555
|
+
message: "This socket is not authorized for that server-created watched preview.",
|
|
14556
|
+
});
|
|
14557
|
+
return;
|
|
14558
|
+
}
|
|
14559
|
+
|
|
14560
|
+
const watcher = createStudioFileWatcher({
|
|
14561
|
+
filePath: canonicalPath,
|
|
14562
|
+
initialRevision: msg.revision,
|
|
14563
|
+
readSnapshot: (filePath: string) => {
|
|
14564
|
+
const file = readStudioFile(filePath, dirname(filePath), { requireCanonicalPath: true });
|
|
14565
|
+
if (file.ok === false) throw new Error(file.message);
|
|
14566
|
+
if (file.text.length > PREVIEW_RENDER_MAX_CHARS) {
|
|
14567
|
+
throw new Error(`Watched file exceeds the ${PREVIEW_RENDER_MAX_CHARS.toLocaleString()}-character preview limit.`);
|
|
14568
|
+
}
|
|
14569
|
+
return { path: file.resolvedPath, text: file.text, revision: file.diskRevision };
|
|
14570
|
+
},
|
|
14571
|
+
onUpdate: (snapshot: { path: string; text: string; revision: string }, details: { generation: number; recovered: boolean }) => {
|
|
14572
|
+
if (studioFileWatchers.get(client) !== watcher || client.readyState !== WebSocket.OPEN) return;
|
|
14573
|
+
sendToClient(client, {
|
|
14574
|
+
type: "watched_file_update",
|
|
14575
|
+
path: snapshot.path,
|
|
14576
|
+
text: snapshot.text,
|
|
14577
|
+
diskRevision: snapshot.revision,
|
|
14578
|
+
generation: details.generation,
|
|
14579
|
+
recovered: details.recovered,
|
|
14580
|
+
message: `Updated ${basename(snapshot.path)} from disk.`,
|
|
14581
|
+
});
|
|
14582
|
+
},
|
|
14583
|
+
onError: (error: unknown, details: { generation: number; lastRevision: string | null }) => {
|
|
14584
|
+
if (studioFileWatchers.get(client) !== watcher || client.readyState !== WebSocket.OPEN) return;
|
|
14585
|
+
sendToClient(client, {
|
|
14586
|
+
type: "watched_file_error",
|
|
14587
|
+
path: canonicalPath,
|
|
14588
|
+
diskRevision: details.lastRevision,
|
|
14589
|
+
generation: details.generation,
|
|
14590
|
+
message: `Could not refresh ${basename(canonicalPath)}; keeping the last good preview: ${error instanceof Error ? error.message : String(error)}`,
|
|
14591
|
+
});
|
|
14592
|
+
},
|
|
14593
|
+
onRecovered: (snapshot: { path: string; revision: string }, details: { generation: number; changed: boolean }) => {
|
|
14594
|
+
if (studioFileWatchers.get(client) !== watcher || client.readyState !== WebSocket.OPEN || details.changed) return;
|
|
14595
|
+
sendToClient(client, {
|
|
14596
|
+
type: "watched_file_ready",
|
|
14597
|
+
path: snapshot.path,
|
|
14598
|
+
diskRevision: snapshot.revision,
|
|
14599
|
+
generation: details.generation,
|
|
14600
|
+
recovered: true,
|
|
14601
|
+
message: `Watching ${basename(snapshot.path)} for disk changes.`,
|
|
14602
|
+
});
|
|
14603
|
+
},
|
|
14604
|
+
});
|
|
14605
|
+
studioFileWatchers.set(client, watcher);
|
|
14606
|
+
await watcher.refresh();
|
|
14607
|
+
if (
|
|
14608
|
+
studioFileWatchers.get(client) === watcher
|
|
14609
|
+
&& client.readyState === WebSocket.OPEN
|
|
14610
|
+
&& !watcher.error
|
|
14611
|
+
) {
|
|
14612
|
+
sendToClient(client, {
|
|
14613
|
+
type: "watched_file_ready",
|
|
14614
|
+
path: canonicalPath,
|
|
14615
|
+
diskRevision: watcher.revision,
|
|
14616
|
+
generation: watcher.generation,
|
|
14617
|
+
message: `Watching ${basename(canonicalPath)} for disk changes.`,
|
|
14618
|
+
});
|
|
14619
|
+
}
|
|
14620
|
+
};
|
|
14621
|
+
|
|
14396
14622
|
const handleStudioMessage = (client: WebSocket, msg: IncomingStudioMessage) => {
|
|
14397
14623
|
if (msg.type === "ping") {
|
|
14398
14624
|
sendToClient(client, { type: "pong", timestamp: Date.now() });
|
|
@@ -14402,6 +14628,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
14402
14628
|
studioWorkspaceStateStore.set(msg.tabStateId, msg.state);
|
|
14403
14629
|
return;
|
|
14404
14630
|
}
|
|
14631
|
+
if (msg.type === "watch_file_subscribe") {
|
|
14632
|
+
void subscribeStudioWatchedFile(client, msg).catch((error) => {
|
|
14633
|
+
sendToClient(client, {
|
|
14634
|
+
type: "watched_file_error",
|
|
14635
|
+
path: msg.path,
|
|
14636
|
+
message: `Could not start watched preview: ${error instanceof Error ? error.message : String(error)}`,
|
|
14637
|
+
});
|
|
14638
|
+
});
|
|
14639
|
+
return;
|
|
14640
|
+
}
|
|
14405
14641
|
|
|
14406
14642
|
emitDebugEvent("studio_message", {
|
|
14407
14643
|
type: msg.type,
|
|
@@ -14423,6 +14659,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14423
14659
|
modelLabel: currentModelLabel,
|
|
14424
14660
|
currentModel: getCurrentStudioModelDescriptor(),
|
|
14425
14661
|
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
14662
|
+
sideQuestionThinkingLevels: getCurrentStudioSideQuestionThinkingLevels(),
|
|
14426
14663
|
piModels: getStudioModelOptions(),
|
|
14427
14664
|
suggestionModels: getStudioModelOptions(),
|
|
14428
14665
|
currentTheme: getCurrentStudioThemeDescriptor(),
|
|
@@ -15372,6 +15609,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
15372
15609
|
}
|
|
15373
15610
|
|
|
15374
15611
|
if (msg.type === "save_as_request") {
|
|
15612
|
+
if (studioWatchedClientPaths.has(client)) {
|
|
15613
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Watched previews are read-only. Open an editable Studio tab before saving." });
|
|
15614
|
+
return;
|
|
15615
|
+
}
|
|
15375
15616
|
if (!isValidRequestId(msg.requestId)) {
|
|
15376
15617
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
15377
15618
|
return;
|
|
@@ -15380,14 +15621,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
15380
15621
|
sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
|
|
15381
15622
|
return;
|
|
15382
15623
|
}
|
|
15383
|
-
if (!msg.content.trim()) {
|
|
15384
|
-
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Nothing to save." });
|
|
15385
|
-
return;
|
|
15386
|
-
}
|
|
15387
15624
|
|
|
15388
|
-
const result = writeStudioFile(msg.path, studioCwd, msg.content);
|
|
15625
|
+
const result = writeStudioFile(msg.path, studioCwd, msg.content, msg.overwrite === true, msg.expectedRevision);
|
|
15389
15626
|
if (result.ok === false) {
|
|
15390
|
-
|
|
15627
|
+
if (result.conflict) {
|
|
15628
|
+
sendToClient(client, {
|
|
15629
|
+
type: "save_as_conflict",
|
|
15630
|
+
requestId: msg.requestId,
|
|
15631
|
+
path: result.path || msg.path,
|
|
15632
|
+
currentRevision: result.currentRevision || null,
|
|
15633
|
+
reason: result.reason || "target-exists",
|
|
15634
|
+
message: result.message,
|
|
15635
|
+
});
|
|
15636
|
+
} else {
|
|
15637
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: result.message });
|
|
15638
|
+
}
|
|
15391
15639
|
return;
|
|
15392
15640
|
}
|
|
15393
15641
|
|
|
@@ -15397,6 +15645,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15397
15645
|
source: "file",
|
|
15398
15646
|
path: result.resolvedPath,
|
|
15399
15647
|
resourceDir: dirname(result.resolvedPath),
|
|
15648
|
+
diskRevision: result.diskRevision,
|
|
15400
15649
|
};
|
|
15401
15650
|
recordStudioDocumentResourceGrants(initialStudioDocument);
|
|
15402
15651
|
|
|
@@ -15406,12 +15655,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
15406
15655
|
path: result.resolvedPath,
|
|
15407
15656
|
label: result.label,
|
|
15408
15657
|
resourceDir: dirname(result.resolvedPath),
|
|
15658
|
+
diskRevision: result.diskRevision,
|
|
15409
15659
|
message: `Saved editor text to ${result.label}`,
|
|
15410
15660
|
});
|
|
15411
15661
|
return;
|
|
15412
15662
|
}
|
|
15413
15663
|
|
|
15414
15664
|
if (msg.type === "save_over_request") {
|
|
15665
|
+
if (studioWatchedClientPaths.has(client)) {
|
|
15666
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Watched previews are read-only. Open an editable Studio tab before saving." });
|
|
15667
|
+
return;
|
|
15668
|
+
}
|
|
15415
15669
|
if (!isValidRequestId(msg.requestId)) {
|
|
15416
15670
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
15417
15671
|
return;
|
|
@@ -15420,35 +15674,52 @@ export default function (pi: ExtensionAPI) {
|
|
|
15420
15674
|
sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
|
|
15421
15675
|
return;
|
|
15422
15676
|
}
|
|
15423
|
-
|
|
15424
|
-
|
|
15425
|
-
|
|
15426
|
-
|
|
15427
|
-
|
|
15428
|
-
|
|
15677
|
+
|
|
15678
|
+
const result = saveStudioDiskFileIfRevision({
|
|
15679
|
+
path: msg.path,
|
|
15680
|
+
content: msg.content,
|
|
15681
|
+
expectedRevision: msg.expectedRevision,
|
|
15682
|
+
force: msg.force === true,
|
|
15683
|
+
}) as StudioDiskWriteResult;
|
|
15684
|
+
if (result.ok === false) {
|
|
15685
|
+
if (result.conflict) {
|
|
15686
|
+
sendToClient(client, {
|
|
15687
|
+
type: "save_conflict",
|
|
15688
|
+
requestId: msg.requestId,
|
|
15689
|
+
path: result.path || msg.path,
|
|
15690
|
+
currentRevision: result.currentRevision || null,
|
|
15691
|
+
reason: result.reason || "disk-changed",
|
|
15692
|
+
canOverwrite: result.reason !== "location-changed" && result.reason !== "hard-linked-file",
|
|
15693
|
+
message: result.message,
|
|
15694
|
+
});
|
|
15695
|
+
} else {
|
|
15696
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: result.message });
|
|
15697
|
+
}
|
|
15429
15698
|
return;
|
|
15430
15699
|
}
|
|
15431
15700
|
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
|
|
15437
|
-
|
|
15438
|
-
|
|
15439
|
-
|
|
15440
|
-
|
|
15441
|
-
|
|
15442
|
-
|
|
15443
|
-
|
|
15444
|
-
|
|
15445
|
-
|
|
15446
|
-
|
|
15447
|
-
|
|
15448
|
-
|
|
15449
|
-
|
|
15450
|
-
|
|
15451
|
-
|
|
15701
|
+
const previousDocument = initialStudioDocument;
|
|
15702
|
+
const existingLabel = previousDocument?.path === result.path
|
|
15703
|
+
? previousDocument.label
|
|
15704
|
+
: basename(result.path);
|
|
15705
|
+
initialStudioDocument = {
|
|
15706
|
+
text: msg.content,
|
|
15707
|
+
label: existingLabel,
|
|
15708
|
+
source: "file",
|
|
15709
|
+
path: result.path,
|
|
15710
|
+
resourceDir: dirname(result.path),
|
|
15711
|
+
diskRevision: result.revision,
|
|
15712
|
+
};
|
|
15713
|
+
recordStudioDocumentResourceGrants(initialStudioDocument);
|
|
15714
|
+
sendToClient(client, {
|
|
15715
|
+
type: "saved",
|
|
15716
|
+
requestId: msg.requestId,
|
|
15717
|
+
path: result.path,
|
|
15718
|
+
label: existingLabel,
|
|
15719
|
+
resourceDir: dirname(result.path),
|
|
15720
|
+
diskRevision: result.revision,
|
|
15721
|
+
message: `Saved over ${existingLabel}`,
|
|
15722
|
+
});
|
|
15452
15723
|
return;
|
|
15453
15724
|
}
|
|
15454
15725
|
|
|
@@ -15472,7 +15743,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15472
15743
|
return;
|
|
15473
15744
|
}
|
|
15474
15745
|
|
|
15475
|
-
const refreshed = readStudioFile(refreshPath, studioCwd);
|
|
15746
|
+
const refreshed = readStudioFile(refreshPath, studioCwd, { requireCanonicalPath: true });
|
|
15476
15747
|
if (refreshed.ok === false) {
|
|
15477
15748
|
sendToClient(client, {
|
|
15478
15749
|
type: "error",
|
|
@@ -15488,6 +15759,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15488
15759
|
source: "file",
|
|
15489
15760
|
path: refreshed.resolvedPath,
|
|
15490
15761
|
resourceDir: dirname(refreshed.resolvedPath),
|
|
15762
|
+
diskRevision: refreshed.diskRevision,
|
|
15491
15763
|
};
|
|
15492
15764
|
if (!requestedPath || initialStudioDocument?.path === refreshed.resolvedPath) {
|
|
15493
15765
|
initialStudioDocument = refreshedDocument;
|
|
@@ -16855,7 +17127,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
16855
17127
|
studioCwd,
|
|
16856
17128
|
studioResourceGrantRegistry,
|
|
16857
17129
|
);
|
|
16858
|
-
await respondLocalPreviewLinkJson(
|
|
17130
|
+
await respondLocalPreviewLinkJson(
|
|
17131
|
+
req,
|
|
17132
|
+
res,
|
|
17133
|
+
requestUrl,
|
|
17134
|
+
resource,
|
|
17135
|
+
serverState,
|
|
17136
|
+
studioResourceGrantRegistry,
|
|
17137
|
+
);
|
|
16859
17138
|
} catch (error) {
|
|
16860
17139
|
if (error instanceof StudioResourceGrantRequiredError) {
|
|
16861
17140
|
respondStudioResourceGrantRequiredJson(res, error);
|
|
@@ -17019,6 +17298,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
17019
17298
|
const host = req.headers.host ?? `127.0.0.1:${state.port}`;
|
|
17020
17299
|
const requestUrl = new URL(req.url ?? "/ws", `http://${host}`);
|
|
17021
17300
|
const clientMode = normalizeStudioUiMode(requestUrl.searchParams.get("mode"));
|
|
17301
|
+
const requestedWatchedPath = (requestUrl.searchParams.get("watchPath") ?? "").trim();
|
|
17302
|
+
const canonicalWatchedPath = requestedWatchedPath ? resolve(requestedWatchedPath) : "";
|
|
17303
|
+
const requestedDocId = (requestUrl.searchParams.get("docId") ?? "").trim();
|
|
17304
|
+
const capabilityDocument = requestedDocId ? readTransientStudioDocument(requestedDocId) : null;
|
|
17305
|
+
const capabilityWatchedPath = capabilityDocument?.watchFile && capabilityDocument.path
|
|
17306
|
+
? resolve(capabilityDocument.path)
|
|
17307
|
+
: "";
|
|
17308
|
+
const comparableWatchedPath = process.platform === "win32" ? canonicalWatchedPath.toLowerCase() : canonicalWatchedPath;
|
|
17309
|
+
const comparableCapabilityPath = process.platform === "win32" ? capabilityWatchedPath.toLowerCase() : capabilityWatchedPath;
|
|
17310
|
+
const requestsWatchedCapability = Boolean(canonicalWatchedPath || capabilityWatchedPath);
|
|
17311
|
+
if (
|
|
17312
|
+
requestsWatchedCapability
|
|
17313
|
+
&& (
|
|
17314
|
+
clientMode !== "editor-only"
|
|
17315
|
+
|| !canonicalWatchedPath
|
|
17316
|
+
|| !capabilityWatchedPath
|
|
17317
|
+
|| comparableWatchedPath !== comparableCapabilityPath
|
|
17318
|
+
)
|
|
17319
|
+
) {
|
|
17320
|
+
try {
|
|
17321
|
+
ws.close(4003, "Watched preview authorization unavailable");
|
|
17322
|
+
} catch {
|
|
17323
|
+
// Ignore close errors.
|
|
17324
|
+
}
|
|
17325
|
+
return;
|
|
17326
|
+
}
|
|
17022
17327
|
if (clientMode === "full") {
|
|
17023
17328
|
for (const client of clients) {
|
|
17024
17329
|
if (client.readyState !== WebSocket.OPEN) continue;
|
|
@@ -17034,6 +17339,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
17034
17339
|
}
|
|
17035
17340
|
clients.add(ws);
|
|
17036
17341
|
clientModes.set(ws, clientMode);
|
|
17342
|
+
if (canonicalWatchedPath) studioWatchedClientPaths.set(ws, canonicalWatchedPath);
|
|
17037
17343
|
emitDebugEvent("studio_ws_connected", { clients: clients.size, mode: clientMode });
|
|
17038
17344
|
broadcastState();
|
|
17039
17345
|
|
|
@@ -17046,15 +17352,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
17046
17352
|
handleStudioMessage(ws, parsed);
|
|
17047
17353
|
});
|
|
17048
17354
|
|
|
17049
|
-
|
|
17355
|
+
const cleanupClient = () => {
|
|
17050
17356
|
clients.delete(ws);
|
|
17051
17357
|
clientModes.delete(ws);
|
|
17358
|
+
studioWatchedClientPaths.delete(ws);
|
|
17359
|
+
const cleanupGeneration = (studioFileWatchSubscriptionGenerations.get(ws) ?? 0) + 1;
|
|
17360
|
+
studioFileWatchSubscriptionGenerations.set(ws, cleanupGeneration);
|
|
17361
|
+
void closeStudioFileWatcher(ws).finally(() => {
|
|
17362
|
+
if (studioFileWatchSubscriptionGenerations.get(ws) === cleanupGeneration) {
|
|
17363
|
+
studioFileWatchSubscriptionGenerations.delete(ws);
|
|
17364
|
+
}
|
|
17365
|
+
});
|
|
17366
|
+
};
|
|
17367
|
+
|
|
17368
|
+
ws.on("close", () => {
|
|
17369
|
+
cleanupClient();
|
|
17052
17370
|
emitDebugEvent("studio_ws_disconnected", { clients: clients.size });
|
|
17053
17371
|
});
|
|
17054
17372
|
|
|
17055
17373
|
ws.on("error", () => {
|
|
17056
|
-
|
|
17057
|
-
|
|
17374
|
+
// Make the socket unusable before removing its watched-path marker;
|
|
17375
|
+
// otherwise an error event could briefly weaken read-only enforcement.
|
|
17376
|
+
try {
|
|
17377
|
+
ws.terminate();
|
|
17378
|
+
} catch {
|
|
17379
|
+
try { ws.close(); } catch {}
|
|
17380
|
+
}
|
|
17381
|
+
cleanupClient();
|
|
17058
17382
|
});
|
|
17059
17383
|
});
|
|
17060
17384
|
|
|
@@ -17122,7 +17446,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
17122
17446
|
const stopServer = async () => {
|
|
17123
17447
|
studioSideQuestionStartRequestId = null;
|
|
17124
17448
|
await disposeStudioSideQuestionRuntime();
|
|
17125
|
-
|
|
17449
|
+
await closeAllStudioFileWatchers();
|
|
17450
|
+
if (!serverState) {
|
|
17451
|
+
studioWatchedClientPaths.clear();
|
|
17452
|
+
studioFileWatchSubscriptionGenerations.clear();
|
|
17453
|
+
return;
|
|
17454
|
+
}
|
|
17126
17455
|
clearStudioDirectRunState();
|
|
17127
17456
|
clearActiveRequest();
|
|
17128
17457
|
clearPendingStudioCompletion();
|
|
@@ -17148,6 +17477,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
17148
17477
|
await new Promise<void>((resolve) => {
|
|
17149
17478
|
state.server.close(() => resolve());
|
|
17150
17479
|
});
|
|
17480
|
+
studioWatchedClientPaths.clear();
|
|
17481
|
+
studioFileWatchSubscriptionGenerations.clear();
|
|
17151
17482
|
studioWorkspaceStateStore.clear();
|
|
17152
17483
|
studioResourceGrantRegistry.clear();
|
|
17153
17484
|
};
|
|
@@ -17166,6 +17497,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
17166
17497
|
await disposeStudioSideQuestionRuntime();
|
|
17167
17498
|
studioTraceHistory.clear();
|
|
17168
17499
|
lastCommandCtx = null;
|
|
17500
|
+
for (const client of studioWatchedClientPaths.keys()) {
|
|
17501
|
+
try { client.close(4001, "Watched preview session changed"); } catch {}
|
|
17502
|
+
}
|
|
17503
|
+
await closeAllStudioFileWatchers();
|
|
17504
|
+
transientStudioDocuments.clear();
|
|
17505
|
+
studioResourceGrantRegistry.clear();
|
|
17506
|
+
initialStudioDocument = null;
|
|
17169
17507
|
}
|
|
17170
17508
|
latestModelRequestCtx = ctx;
|
|
17171
17509
|
hydrateLatestAssistant(ctx.sessionManager.getBranch());
|
|
@@ -17517,7 +17855,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
17517
17855
|
const resolveStudioLaunchDocument = (
|
|
17518
17856
|
trimmed: string,
|
|
17519
17857
|
ctx: ExtensionCommandContext,
|
|
17520
|
-
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; allowPdfPreview?: boolean; watchPdf?: boolean },
|
|
17858
|
+
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; allowPdfPreview?: boolean; watchPdf?: boolean; watchText?: boolean },
|
|
17521
17859
|
): StudioLaunchSelection | null => {
|
|
17522
17860
|
const defaultSource = options?.defaultSource === "blank" ? "blank" : "last-response";
|
|
17523
17861
|
const commandLabel = options?.commandLabel ?? "/studio";
|
|
@@ -17624,6 +17962,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
17624
17962
|
return null;
|
|
17625
17963
|
}
|
|
17626
17964
|
|
|
17965
|
+
if (options?.watchText) {
|
|
17966
|
+
if (file.text.length > PREVIEW_RENDER_MAX_CHARS) {
|
|
17967
|
+
ctx.ui.notify(`Watched preview source exceeds ${PREVIEW_RENDER_MAX_CHARS.toLocaleString()} characters.`, "error");
|
|
17968
|
+
return null;
|
|
17969
|
+
}
|
|
17970
|
+
return {
|
|
17971
|
+
document: {
|
|
17972
|
+
text: file.text,
|
|
17973
|
+
label: file.label,
|
|
17974
|
+
source: "file",
|
|
17975
|
+
path: file.resolvedPath,
|
|
17976
|
+
resourceDir: dirname(file.resolvedPath),
|
|
17977
|
+
diskRevision: file.diskRevision,
|
|
17978
|
+
watchFile: true,
|
|
17979
|
+
},
|
|
17980
|
+
kind: "watched-preview",
|
|
17981
|
+
mode: "editor-only",
|
|
17982
|
+
transient: true,
|
|
17983
|
+
skipWorkspaceRestore: true,
|
|
17984
|
+
paneFocus: "right",
|
|
17985
|
+
resourcePath: file.resolvedPath,
|
|
17986
|
+
};
|
|
17987
|
+
}
|
|
17988
|
+
|
|
17627
17989
|
if (file.text.length > 200_000) {
|
|
17628
17990
|
ctx.ui.notify(
|
|
17629
17991
|
"Loaded a large file. Studio critique requests currently reject documents over 200k characters.",
|
|
@@ -17637,6 +17999,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
17637
17999
|
source: "file",
|
|
17638
18000
|
path: file.resolvedPath,
|
|
17639
18001
|
resourceDir: ctx.cwd,
|
|
18002
|
+
diskRevision: file.diskRevision,
|
|
17640
18003
|
});
|
|
17641
18004
|
};
|
|
17642
18005
|
|
|
@@ -17894,7 +18257,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
17894
18257
|
trimmed: string,
|
|
17895
18258
|
ctx: ExtensionCommandContext,
|
|
17896
18259
|
mode: StudioUiMode,
|
|
17897
|
-
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; replaceExistingFull?: boolean; allowPdfPreview?: boolean; watchPdf?: boolean },
|
|
18260
|
+
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; replaceExistingFull?: boolean; allowPdfPreview?: boolean; watchPdf?: boolean; watchText?: boolean },
|
|
17898
18261
|
) => {
|
|
17899
18262
|
const launchOpenFlags = parseStudioLaunchOpenFlags(trimmed);
|
|
17900
18263
|
if (launchOpenFlags.error) {
|
|
@@ -17910,11 +18273,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
17910
18273
|
const launchesPdfPreview = parsedLaunchPath
|
|
17911
18274
|
? Boolean(parseStudioPdfLaunchTarget(normalizePathInput(parsedLaunchPath)))
|
|
17912
18275
|
: false;
|
|
17913
|
-
if (launchOpenFlags.watchPdf && !
|
|
17914
|
-
ctx.ui.notify("--watch requires a local PDF path, for example: /studio --watch
|
|
18276
|
+
if (launchOpenFlags.watchPdf && !parsedLaunchPath) {
|
|
18277
|
+
ctx.ui.notify("--watch requires a local text or PDF path, for example: /studio --watch notes.md", "error");
|
|
17915
18278
|
return;
|
|
17916
18279
|
}
|
|
17917
|
-
const
|
|
18280
|
+
const launchesWatchedTextPreview = Boolean(launchOpenFlags.watchPdf && parsedLaunchPath && !launchesPdfPreview);
|
|
18281
|
+
const requestedLaunchMode: StudioUiMode = launchesPdfPreview || launchesWatchedTextPreview ? "editor-only" : mode;
|
|
17918
18282
|
if (requestedLaunchMode === "full" && hasConnectedFullStudioView()) {
|
|
17919
18283
|
if (options?.replaceExistingFull) {
|
|
17920
18284
|
closeStudioClientsByMode("full", 4001, "Full Studio replaced");
|
|
@@ -17948,7 +18312,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
17948
18312
|
|
|
17949
18313
|
const selection = resolveStudioLaunchDocument(launchArgs, ctx, {
|
|
17950
18314
|
...options,
|
|
17951
|
-
watchPdf: launchOpenFlags.watchPdf,
|
|
18315
|
+
watchPdf: launchOpenFlags.watchPdf && launchesPdfPreview,
|
|
18316
|
+
watchText: launchesWatchedTextPreview,
|
|
17952
18317
|
});
|
|
17953
18318
|
if (!selection) return;
|
|
17954
18319
|
const selected = selection.document;
|
|
@@ -17974,7 +18339,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
17974
18339
|
?? (launchOpenFlags.noBrowser ? buildStudioForwardingHint(state.port, url, { prefix: "Browser auto-open was skipped because --no-browser was used." }) : null);
|
|
17975
18340
|
const openedLabel = selection.kind === "pdf-preview"
|
|
17976
18341
|
? "pi Studio PDF preview"
|
|
17977
|
-
: (
|
|
18342
|
+
: (selection.kind === "watched-preview"
|
|
18343
|
+
? "pi Studio watched file preview"
|
|
18344
|
+
: (launchMode === "editor-only" ? "pi Studio editor-only view" : "pi Studio"));
|
|
17978
18345
|
|
|
17979
18346
|
const shouldOpenBrowser = shouldAutoOpenStudioBrowser({
|
|
17980
18347
|
openRemoteBrowser: launchOpenFlags.openRemoteBrowser,
|
|
@@ -17989,6 +18356,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
17989
18356
|
if (selection.kind === "pdf-preview") {
|
|
17990
18357
|
const watchLabel = launchOpenFlags.watchPdf ? " (auto-refresh on)" : "";
|
|
17991
18358
|
ctx.ui.notify(`Opened ${openedLabel}${watchLabel}: ${selection.resourcePath ?? selected.label}`, "info");
|
|
18359
|
+
} else if (selection.kind === "watched-preview") {
|
|
18360
|
+
ctx.ui.notify(`Opened ${openedLabel}: ${selection.resourcePath ?? selected.label}`, "info");
|
|
17992
18361
|
} else if (selected.source === "file") {
|
|
17993
18362
|
ctx.ui.notify(`Opened ${openedLabel} with file loaded: ${selected.label}`, "info");
|
|
17994
18363
|
} else if (selected.source === "last-response") {
|
|
@@ -18011,7 +18380,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
18011
18380
|
};
|
|
18012
18381
|
|
|
18013
18382
|
pi.registerCommand("studio", {
|
|
18014
|
-
description: "Open pi Studio browser UI or a
|
|
18383
|
+
description: "Open pi Studio browser UI or a read-only watched preview (/studio, /studio <file>, /studio --watch <file>, /studio --blank, /studio --last, /studio --no-browser)",
|
|
18015
18384
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
18016
18385
|
const trimmed = args.trim();
|
|
18017
18386
|
|
|
@@ -18042,7 +18411,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
18042
18411
|
"Usage: /studio [path|--blank|--last]\n"
|
|
18043
18412
|
+ " /studio Open studio with last model response (fallback: blank)\n"
|
|
18044
18413
|
+ " /studio <path> Open a text file in Studio, or a PDF in a read-only companion preview\n"
|
|
18045
|
-
+ " /studio --watch <
|
|
18414
|
+
+ " /studio --watch <file> Open a read-only text preview that follows disk changes, or a PDF with auto-refresh\n"
|
|
18046
18415
|
+ " /studio --blank Open with blank editor\n"
|
|
18047
18416
|
+ " /studio --last Open with last model response\n"
|
|
18048
18417
|
+ " /studio --no-browser Print the Studio URL without opening a browser\n"
|
|
@@ -18093,7 +18462,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
18093
18462
|
});
|
|
18094
18463
|
|
|
18095
18464
|
pi.registerCommand("studio-editor-only", {
|
|
18096
|
-
description: "Open pi Studio in editor-only mode or
|
|
18465
|
+
description: "Open pi Studio in editor-only mode or a read-only watched preview (/studio-editor-only, /studio-editor-only <file>, /studio-editor-only --watch <file>)",
|
|
18097
18466
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
18098
18467
|
const trimmed = args.trim();
|
|
18099
18468
|
if (trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
@@ -18101,7 +18470,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
18101
18470
|
"Usage: /studio-editor-only [path|--blank|--last]\n"
|
|
18102
18471
|
+ " /studio-editor-only Open an editor-only Studio view (default: blank editor)\n"
|
|
18103
18472
|
+ " /studio-editor-only <path> Open a text file for editing, or a PDF in a read-only preview\n"
|
|
18104
|
-
+ " /studio-editor-only --watch <
|
|
18473
|
+
+ " /studio-editor-only --watch <file> Open a read-only text preview that follows disk changes, or a PDF with auto-refresh\n"
|
|
18105
18474
|
+ " /studio-editor-only --blank Open with blank editor\n"
|
|
18106
18475
|
+ " /studio-editor-only --last Open with last model response loaded into the editor\n"
|
|
18107
18476
|
+ " /studio-editor-only --no-browser Print URL without opening a browser\n"
|
|
@@ -18457,6 +18826,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
18457
18826
|
label: file.label,
|
|
18458
18827
|
source: "file",
|
|
18459
18828
|
path: file.resolvedPath,
|
|
18829
|
+
diskRevision: file.diskRevision,
|
|
18460
18830
|
};
|
|
18461
18831
|
initialStudioDocument = nextDoc;
|
|
18462
18832
|
recordStudioDocumentResourceGrants(nextDoc, ctx.cwd);
|