pi-studio 0.9.54 → 0.9.56
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 +22 -0
- package/README.md +16 -3
- package/ROADMAP.md +24 -1
- package/client/studio-client.js +259 -65
- package/client/studio.css +141 -5
- package/index.ts +806 -213
- package/package.json +1 -1
- package/shared/REPL_SESSION_RECORD_PROTOCOL.md +70 -0
- package/shared/repl-session-record.js +623 -0
- package/shared/studio-side-question.js +32 -3
package/index.ts
CHANGED
|
@@ -42,6 +42,19 @@ import { createStudioResourceGrantRegistry } from "./shared/studio-resource-gran
|
|
|
42
42
|
import { prepareStudioLatexForPandoc } from "./shared/studio-latex-pandoc-compat.js";
|
|
43
43
|
import { isStudioCmuxSession, openStudioUrlInBrowser } from "./shared/studio-browser-launcher.js";
|
|
44
44
|
import { buildStudioReplTmuxStartArgs } from "./shared/studio-repl-tmux.js";
|
|
45
|
+
import {
|
|
46
|
+
REPL_SESSION_RECORD_ID_OPTION,
|
|
47
|
+
REPL_SESSION_RECORD_VERSION,
|
|
48
|
+
REPL_SESSION_RECORD_VERSION_OPTION,
|
|
49
|
+
acquireReplSessionSendLease,
|
|
50
|
+
clearReplSessionRecord,
|
|
51
|
+
createReplSessionRecordId,
|
|
52
|
+
ensureReplSessionRecord,
|
|
53
|
+
getReplSessionRecordPath,
|
|
54
|
+
isValidReplSessionRecordId,
|
|
55
|
+
readReplSessionRecord,
|
|
56
|
+
upsertReplSessionRecordEntry,
|
|
57
|
+
} from "./shared/repl-session-record.js";
|
|
45
58
|
import { buildStudioForwardingHint, buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
|
|
46
59
|
import {
|
|
47
60
|
buildStudioPendingPage,
|
|
@@ -74,9 +87,11 @@ import {
|
|
|
74
87
|
import {
|
|
75
88
|
buildStudioSideQuestionFollowUpPrompt,
|
|
76
89
|
buildStudioSideQuestionPrompt,
|
|
90
|
+
getStudioSideQuestionThinkingLevels,
|
|
77
91
|
normalizeStudioSideQuestionFocusKind,
|
|
78
92
|
normalizeStudioSideQuestionGatherScope,
|
|
79
93
|
normalizeStudioSideQuestionThinking,
|
|
94
|
+
resolveStudioSideQuestionThinking,
|
|
80
95
|
STUDIO_SIDE_QUESTION_FOCUS_MAX_CHARS,
|
|
81
96
|
STUDIO_SIDE_QUESTION_QUESTION_MAX_CHARS,
|
|
82
97
|
} from "./shared/studio-side-question.js";
|
|
@@ -120,7 +135,7 @@ type StudioQuartoPreviewStatus = "idle" | "starting" | "running" | "stopping" |
|
|
|
120
135
|
type StudioShowMeSourceKind = "selection" | "response" | "editor" | "context";
|
|
121
136
|
type StudioSideQuestionFocusKind = "selection" | "section" | "editor" | "response" | "none";
|
|
122
137
|
type StudioSideQuestionGatherScope = "none" | "folder" | "repo" | "custom";
|
|
123
|
-
type StudioSideQuestionThinking =
|
|
138
|
+
type StudioSideQuestionThinking = ModelThinkingLevel;
|
|
124
139
|
type StudioSideQuestionStatus = "idle" | "running" | "error";
|
|
125
140
|
|
|
126
141
|
interface StudioQuartoPreviewContext {
|
|
@@ -247,9 +262,14 @@ interface StudioContextUsageSnapshot {
|
|
|
247
262
|
interface StudioReplSessionInfo {
|
|
248
263
|
sessionName: string;
|
|
249
264
|
target: string;
|
|
265
|
+
tmuxSessionId: string;
|
|
266
|
+
tmuxSessionCreatedAt: number;
|
|
250
267
|
runtime: StudioReplRuntime | "unknown";
|
|
251
268
|
label: string;
|
|
252
269
|
source: "studio" | "pi-repl" | "tmux";
|
|
270
|
+
recordId?: string;
|
|
271
|
+
recordPath?: string;
|
|
272
|
+
recordWarning?: string;
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
interface StudioReplJournalEntry {
|
|
@@ -257,15 +277,22 @@ interface StudioReplJournalEntry {
|
|
|
257
277
|
requestId: string;
|
|
258
278
|
createdAt: number;
|
|
259
279
|
updatedAt: number;
|
|
280
|
+
completedAt: number | null;
|
|
260
281
|
sessionName: string;
|
|
261
282
|
runtime: StudioReplRuntime | "unknown";
|
|
283
|
+
origin: "pi-studio" | "pi-repl" | "unknown";
|
|
262
284
|
label: string;
|
|
263
285
|
mode: "raw" | "literate" | "agent";
|
|
264
286
|
prose: string;
|
|
265
287
|
code: string;
|
|
266
288
|
output: string;
|
|
267
|
-
status: "sent" | "captured" | "timeout" | "error" | "note";
|
|
289
|
+
status: "sending" | "sent" | "captured" | "timeout" | "error" | "note";
|
|
268
290
|
skippedChunks: number;
|
|
291
|
+
codeOmittedChars?: number;
|
|
292
|
+
proseOmittedChars?: number;
|
|
293
|
+
outputOmittedChars?: number;
|
|
294
|
+
/** Studio-local delivery state; not persisted in the shared protocol snapshot. */
|
|
295
|
+
sharedSynced?: boolean;
|
|
269
296
|
}
|
|
270
297
|
|
|
271
298
|
interface PreparedStudioPdfExport {
|
|
@@ -719,6 +746,32 @@ interface ReplSendRequestMessage {
|
|
|
719
746
|
requestId: string;
|
|
720
747
|
sessionName: string;
|
|
721
748
|
text: string;
|
|
749
|
+
journalEntryId?: string;
|
|
750
|
+
createdAt?: number;
|
|
751
|
+
label?: string;
|
|
752
|
+
mode?: StudioReplJournalEntry["mode"];
|
|
753
|
+
prose?: string;
|
|
754
|
+
skippedChunks?: number;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
interface ReplJournalUpsertRequestMessage {
|
|
758
|
+
type: "repl_journal_upsert_request";
|
|
759
|
+
requestId: string;
|
|
760
|
+
sessionName: string;
|
|
761
|
+
entry: Partial<StudioReplJournalEntry>;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
interface ReplJournalImportRequestMessage {
|
|
765
|
+
type: "repl_journal_import_request";
|
|
766
|
+
requestId: string;
|
|
767
|
+
sessionName: string;
|
|
768
|
+
entries: Array<Partial<StudioReplJournalEntry>>;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
interface ReplJournalClearRequestMessage {
|
|
772
|
+
type: "repl_journal_clear_request";
|
|
773
|
+
requestId: string;
|
|
774
|
+
sessionName: string;
|
|
722
775
|
}
|
|
723
776
|
|
|
724
777
|
interface ReplInterruptRequestMessage {
|
|
@@ -846,6 +899,9 @@ type IncomingStudioMessage =
|
|
|
846
899
|
| ReplStartRequestMessage
|
|
847
900
|
| ReplStopRequestMessage
|
|
848
901
|
| ReplSendRequestMessage
|
|
902
|
+
| ReplJournalUpsertRequestMessage
|
|
903
|
+
| ReplJournalImportRequestMessage
|
|
904
|
+
| ReplJournalClearRequestMessage
|
|
849
905
|
| ReplInterruptRequestMessage
|
|
850
906
|
| CompactRequestMessage
|
|
851
907
|
| SaveAsRequestMessage
|
|
@@ -911,6 +967,7 @@ const STUDIO_REPL_SEND_MAX_CHARS = 200_000;
|
|
|
911
967
|
const STUDIO_REPL_SEND_DEFAULT_TIMEOUT_MS = 20_000;
|
|
912
968
|
const STUDIO_REPL_SEND_MAX_TIMEOUT_MS = 120_000;
|
|
913
969
|
const STUDIO_REPL_JOURNAL_MAX_ENTRIES = 300;
|
|
970
|
+
const STUDIO_REPL_RUNTIME_OPTION = "@pi_repl_runtime";
|
|
914
971
|
const STUDIO_REPL_CONTROL_ROOT = join(tmpdir(), "pi-studio-repl");
|
|
915
972
|
const STUDIO_SUBPROCESS_OUTPUT_MAX_BYTES = 2_000_000;
|
|
916
973
|
const STUDIO_PANDOC_TIMEOUT_MS = readStudioPositiveEnvMs("PI_STUDIO_PANDOC_TIMEOUT_MS", 120_000, 5_000, 15 * 60_000);
|
|
@@ -1204,6 +1261,7 @@ let studioPersistentStateCache: StudioPersistentState | null = null;
|
|
|
1204
1261
|
let studioPersistentStateQueue: Promise<void> = Promise.resolve();
|
|
1205
1262
|
let transientStudioDocuments: Map<string, { document: InitialStudioDocument; createdAt: number }> = new Map();
|
|
1206
1263
|
let studioReplJournalEntries: StudioReplJournalEntry[] = [];
|
|
1264
|
+
const studioReplUnsyncedJournalEntryIds = new Set<string>();
|
|
1207
1265
|
|
|
1208
1266
|
function createEmptyStudioPersistentState(): StudioPersistentState {
|
|
1209
1267
|
return {
|
|
@@ -8105,7 +8163,10 @@ async function respondLocalPreviewLinkJson(
|
|
|
8105
8163
|
...basePayload,
|
|
8106
8164
|
converted,
|
|
8107
8165
|
resourceDir: documentResourceDir,
|
|
8108
|
-
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, {
|
|
8166
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, {
|
|
8167
|
+
skipWorkspaceRestore: true,
|
|
8168
|
+
paneFocus: action === "watch-url" ? "right" : undefined,
|
|
8169
|
+
}),
|
|
8109
8170
|
});
|
|
8110
8171
|
}
|
|
8111
8172
|
|
|
@@ -9674,6 +9735,31 @@ function normalizeStudioQuizThinking(value: unknown): StudioQuizThinking {
|
|
|
9674
9735
|
return "minimal";
|
|
9675
9736
|
}
|
|
9676
9737
|
|
|
9738
|
+
function parseStudioReplJournalEntryInput(value: unknown): Partial<StudioReplJournalEntry> | null {
|
|
9739
|
+
if (!value || typeof value !== "object") return null;
|
|
9740
|
+
const entry = value as Record<string, unknown>;
|
|
9741
|
+
const code = typeof entry.code === "string" ? entry.code.slice(0, STUDIO_REPL_SEND_MAX_CHARS) : "";
|
|
9742
|
+
const prose = typeof entry.prose === "string" ? entry.prose.slice(0, 80_000) : "";
|
|
9743
|
+
const output = typeof entry.output === "string" ? entry.output.slice(0, 200_000) : "";
|
|
9744
|
+
if (!code.trim() && !prose.trim() && !output.trim()) return null;
|
|
9745
|
+
return {
|
|
9746
|
+
id: typeof entry.id === "string" ? entry.id.slice(0, 240) : undefined,
|
|
9747
|
+
requestId: typeof entry.requestId === "string" ? entry.requestId.slice(0, 300) : undefined,
|
|
9748
|
+
createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : undefined,
|
|
9749
|
+
updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : undefined,
|
|
9750
|
+
completedAt: typeof entry.completedAt === "number" && Number.isFinite(entry.completedAt) ? entry.completedAt : null,
|
|
9751
|
+
runtime: normalizeStudioReplRuntime(entry.runtime) || "unknown",
|
|
9752
|
+
origin: entry.origin === "pi-repl" ? "pi-repl" : "pi-studio",
|
|
9753
|
+
label: typeof entry.label === "string" ? entry.label.slice(0, 240) : undefined,
|
|
9754
|
+
mode: normalizeStudioReplJournalMode(entry.mode),
|
|
9755
|
+
prose,
|
|
9756
|
+
code,
|
|
9757
|
+
output,
|
|
9758
|
+
status: normalizeStudioReplJournalStatus(entry.status),
|
|
9759
|
+
skippedChunks: Math.max(0, Math.min(100_000, Math.floor(Number(entry.skippedChunks) || 0))),
|
|
9760
|
+
};
|
|
9761
|
+
}
|
|
9762
|
+
|
|
9677
9763
|
function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
9678
9764
|
let parsed: unknown;
|
|
9679
9765
|
try {
|
|
@@ -9978,9 +10064,29 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
9978
10064
|
requestId: msg.requestId,
|
|
9979
10065
|
sessionName: msg.sessionName,
|
|
9980
10066
|
text: msg.text,
|
|
10067
|
+
journalEntryId: typeof msg.journalEntryId === "string" ? msg.journalEntryId.slice(0, 240) : undefined,
|
|
10068
|
+
createdAt: typeof msg.createdAt === "number" && Number.isFinite(msg.createdAt) ? msg.createdAt : undefined,
|
|
10069
|
+
label: typeof msg.label === "string" ? msg.label.slice(0, 240) : undefined,
|
|
10070
|
+
mode: normalizeStudioReplJournalMode(msg.mode),
|
|
10071
|
+
prose: typeof msg.prose === "string" ? msg.prose.slice(0, 80_000) : undefined,
|
|
10072
|
+
skippedChunks: Math.max(0, Math.min(100_000, Math.floor(Number(msg.skippedChunks) || 0))),
|
|
9981
10073
|
};
|
|
9982
10074
|
}
|
|
9983
10075
|
|
|
10076
|
+
if (msg.type === "repl_journal_upsert_request" && typeof msg.requestId === "string" && typeof msg.sessionName === "string") {
|
|
10077
|
+
const entry = parseStudioReplJournalEntryInput(msg.entry);
|
|
10078
|
+
if (entry) return { type: "repl_journal_upsert_request", requestId: msg.requestId, sessionName: msg.sessionName, entry };
|
|
10079
|
+
}
|
|
10080
|
+
|
|
10081
|
+
if (msg.type === "repl_journal_import_request" && typeof msg.requestId === "string" && typeof msg.sessionName === "string" && Array.isArray(msg.entries)) {
|
|
10082
|
+
const entries = msg.entries.slice(0, 80).map(parseStudioReplJournalEntryInput).filter((entry): entry is Partial<StudioReplJournalEntry> => Boolean(entry));
|
|
10083
|
+
return { type: "repl_journal_import_request", requestId: msg.requestId, sessionName: msg.sessionName, entries };
|
|
10084
|
+
}
|
|
10085
|
+
|
|
10086
|
+
if (msg.type === "repl_journal_clear_request" && typeof msg.requestId === "string" && typeof msg.sessionName === "string") {
|
|
10087
|
+
return { type: "repl_journal_clear_request", requestId: msg.requestId, sessionName: msg.sessionName };
|
|
10088
|
+
}
|
|
10089
|
+
|
|
9984
10090
|
if (msg.type === "repl_interrupt_request" && typeof msg.requestId === "string" && typeof msg.sessionName === "string") {
|
|
9985
10091
|
return {
|
|
9986
10092
|
type: "repl_interrupt_request",
|
|
@@ -10872,9 +10978,111 @@ function runStudioTmux(args: string[], options?: { cwd?: string; input?: string;
|
|
|
10872
10978
|
return { ok: true, stdout, stderr };
|
|
10873
10979
|
}
|
|
10874
10980
|
|
|
10981
|
+
function readStudioReplTmuxOption(sessionName: string, optionName: string): string | undefined {
|
|
10982
|
+
const result = runStudioTmux(["show-options", "-v", "-t", sessionName, optionName], { timeout: 3_000 });
|
|
10983
|
+
if (!result.ok) return undefined;
|
|
10984
|
+
const value = result.stdout.trim();
|
|
10985
|
+
return value || undefined;
|
|
10986
|
+
}
|
|
10987
|
+
|
|
10988
|
+
function setStudioReplTmuxOptionIfAbsent(sessionName: string, optionName: string, value: string): boolean {
|
|
10989
|
+
return runStudioTmux(["set-option", "-qo", "-t", sessionName, optionName, value], { timeout: 3_000 }).ok;
|
|
10990
|
+
}
|
|
10991
|
+
|
|
10992
|
+
function attachStudioReplSessionRecord(
|
|
10993
|
+
session: StudioReplSessionInfo,
|
|
10994
|
+
recordIdHint?: string,
|
|
10995
|
+
versionHint?: string,
|
|
10996
|
+
): StudioReplSessionInfo {
|
|
10997
|
+
let recordId = recordIdHint || readStudioReplTmuxOption(session.sessionName, REPL_SESSION_RECORD_ID_OPTION);
|
|
10998
|
+
let version = versionHint || readStudioReplTmuxOption(session.sessionName, REPL_SESSION_RECORD_VERSION_OPTION);
|
|
10999
|
+
if (recordId && !isValidReplSessionRecordId(recordId)) {
|
|
11000
|
+
return { ...session, recordWarning: "Invalid shared REPL record metadata; Studio left it untouched." };
|
|
11001
|
+
}
|
|
11002
|
+
if (!recordId) {
|
|
11003
|
+
const candidate = createReplSessionRecordId();
|
|
11004
|
+
if (!setStudioReplTmuxOptionIfAbsent(session.sessionName, REPL_SESSION_RECORD_ID_OPTION, candidate)) {
|
|
11005
|
+
return { ...session, recordWarning: "Studio could not attach shared record metadata to this tmux session." };
|
|
11006
|
+
}
|
|
11007
|
+
recordId = readStudioReplTmuxOption(session.sessionName, REPL_SESSION_RECORD_ID_OPTION);
|
|
11008
|
+
}
|
|
11009
|
+
if (!recordId || !isValidReplSessionRecordId(recordId)) {
|
|
11010
|
+
return { ...session, recordWarning: "Studio could not read valid shared record metadata from this tmux session." };
|
|
11011
|
+
}
|
|
11012
|
+
if (!version) {
|
|
11013
|
+
setStudioReplTmuxOptionIfAbsent(session.sessionName, REPL_SESSION_RECORD_VERSION_OPTION, String(REPL_SESSION_RECORD_VERSION));
|
|
11014
|
+
version = readStudioReplTmuxOption(session.sessionName, REPL_SESSION_RECORD_VERSION_OPTION);
|
|
11015
|
+
}
|
|
11016
|
+
const recordPath = getReplSessionRecordPath(recordId);
|
|
11017
|
+
if (version !== String(REPL_SESSION_RECORD_VERSION)) {
|
|
11018
|
+
return {
|
|
11019
|
+
...session,
|
|
11020
|
+
recordId,
|
|
11021
|
+
recordPath,
|
|
11022
|
+
recordWarning: `Shared REPL record version ${version || "unknown"} is not supported by this Studio version.`,
|
|
11023
|
+
};
|
|
11024
|
+
}
|
|
11025
|
+
try {
|
|
11026
|
+
ensureReplSessionRecord(recordId, {
|
|
11027
|
+
sessionName: session.sessionName,
|
|
11028
|
+
tmuxSessionId: session.tmuxSessionId,
|
|
11029
|
+
tmuxSessionCreatedAt: session.tmuxSessionCreatedAt,
|
|
11030
|
+
runtime: session.runtime,
|
|
11031
|
+
});
|
|
11032
|
+
return { ...session, recordId, recordPath };
|
|
11033
|
+
} catch (error) {
|
|
11034
|
+
return {
|
|
11035
|
+
...session,
|
|
11036
|
+
recordId,
|
|
11037
|
+
recordPath,
|
|
11038
|
+
recordWarning: error instanceof Error ? error.message : String(error),
|
|
11039
|
+
};
|
|
11040
|
+
}
|
|
11041
|
+
}
|
|
11042
|
+
|
|
11043
|
+
function makeStudioReplSessionInfo(
|
|
11044
|
+
sessionName: string,
|
|
11045
|
+
tmuxSessionId = "",
|
|
11046
|
+
tmuxSessionCreatedRaw = "0",
|
|
11047
|
+
runtimeMetadata = "",
|
|
11048
|
+
recordIdHint = "",
|
|
11049
|
+
versionHint = "",
|
|
11050
|
+
): StudioReplSessionInfo {
|
|
11051
|
+
const inferred = inferStudioReplSessionRuntime(sessionName);
|
|
11052
|
+
const metadataRuntime = normalizeStudioReplRuntime(runtimeMetadata);
|
|
11053
|
+
const runtime = metadataRuntime || inferred.runtime;
|
|
11054
|
+
return attachStudioReplSessionRecord({
|
|
11055
|
+
sessionName,
|
|
11056
|
+
target: getStudioReplPaneTarget(tmuxSessionId || sessionName),
|
|
11057
|
+
tmuxSessionId,
|
|
11058
|
+
tmuxSessionCreatedAt: Math.max(0, Math.floor(Number(tmuxSessionCreatedRaw) || 0)),
|
|
11059
|
+
runtime,
|
|
11060
|
+
label: formatStudioReplSessionLabel(sessionName, runtime, inferred.source),
|
|
11061
|
+
source: inferred.source,
|
|
11062
|
+
}, recordIdHint, versionHint);
|
|
11063
|
+
}
|
|
11064
|
+
|
|
11065
|
+
function inspectStudioReplSession(sessionName: string): StudioReplSessionInfo | null {
|
|
11066
|
+
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return null;
|
|
11067
|
+
const result = runStudioTmux([
|
|
11068
|
+
"display-message",
|
|
11069
|
+
"-p",
|
|
11070
|
+
"-t",
|
|
11071
|
+
getStudioReplPaneTarget(sessionName),
|
|
11072
|
+
"#{session_name}\t#{session_id}\t#{session_created}\t#{@pi_repl_runtime}\t#{@pi_repl_record_id}\t#{@pi_repl_record_version}",
|
|
11073
|
+
], { timeout: 3_000 });
|
|
11074
|
+
if (!result.ok) return null;
|
|
11075
|
+
const [resolvedName, tmuxSessionId, createdAt, runtime, recordId, version] = result.stdout.trim().split("\t");
|
|
11076
|
+
return makeStudioReplSessionInfo(resolvedName || sessionName, tmuxSessionId, createdAt, runtime, recordId, version);
|
|
11077
|
+
}
|
|
11078
|
+
|
|
10875
11079
|
function listStudioReplSessions(): { tmuxAvailable: boolean; sessions: StudioReplSessionInfo[]; error?: string } {
|
|
10876
11080
|
if (!isTmuxAvailable()) return { tmuxAvailable: false, sessions: [], error: "tmux is not available." };
|
|
10877
|
-
const result = runStudioTmux([
|
|
11081
|
+
const result = runStudioTmux([
|
|
11082
|
+
"list-sessions",
|
|
11083
|
+
"-F",
|
|
11084
|
+
"#{session_name}\t#{session_id}\t#{session_created}\t#{@pi_repl_runtime}\t#{@pi_repl_record_id}\t#{@pi_repl_record_version}",
|
|
11085
|
+
], { timeout: 3_000 });
|
|
10878
11086
|
if (!result.ok) {
|
|
10879
11087
|
const message = result.message.toLowerCase().includes("no server running") ? "No tmux sessions are running." : result.message;
|
|
10880
11088
|
return { tmuxAvailable: true, sessions: [], error: message };
|
|
@@ -10883,30 +11091,18 @@ function listStudioReplSessions(): { tmuxAvailable: boolean; sessions: StudioRep
|
|
|
10883
11091
|
.split(/\r?\n/)
|
|
10884
11092
|
.map((line) => line.trim())
|
|
10885
11093
|
.filter(Boolean)
|
|
10886
|
-
.
|
|
10887
|
-
.
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
target: getStudioReplPaneTarget(sessionName),
|
|
10892
|
-
runtime: inferred.runtime,
|
|
10893
|
-
label: formatStudioReplSessionLabel(sessionName, inferred.runtime, inferred.source),
|
|
10894
|
-
source: inferred.source,
|
|
10895
|
-
};
|
|
10896
|
-
});
|
|
11094
|
+
.map((line) => line.split("\t"))
|
|
11095
|
+
.filter(([sessionName]) => Boolean(sessionName && shouldShowStudioReplTmuxSession(sessionName)))
|
|
11096
|
+
.map(([sessionName = "", tmuxSessionId = "", createdAt = "0", runtime = "", recordId = "", version = ""]) => (
|
|
11097
|
+
makeStudioReplSessionInfo(sessionName, tmuxSessionId, createdAt, runtime, recordId, version)
|
|
11098
|
+
));
|
|
10897
11099
|
return { tmuxAvailable: true, sessions };
|
|
10898
11100
|
}
|
|
10899
11101
|
|
|
10900
11102
|
function captureStudioReplSession(sessionName: string): { ok: true; transcript: string; session: StudioReplSessionInfo } | { ok: false; message: string } {
|
|
10901
11103
|
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
|
|
10902
|
-
const
|
|
10903
|
-
|
|
10904
|
-
sessionName,
|
|
10905
|
-
target: getStudioReplPaneTarget(sessionName),
|
|
10906
|
-
runtime: inferred.runtime,
|
|
10907
|
-
label: formatStudioReplSessionLabel(sessionName, inferred.runtime, inferred.source),
|
|
10908
|
-
source: inferred.source,
|
|
10909
|
-
};
|
|
11104
|
+
const session = inspectStudioReplSession(sessionName);
|
|
11105
|
+
if (!session) return { ok: false, message: `No tmux REPL session named ${sessionName}.` };
|
|
10910
11106
|
const result = runStudioTmux(["capture-pane", "-J", "-p", "-t", session.target, "-S", `-${STUDIO_REPL_CAPTURE_LINES}`], { timeout: 3_000 });
|
|
10911
11107
|
if (!result.ok) return { ok: false, message: result.message };
|
|
10912
11108
|
return { ok: true, transcript: String(result.stdout || "").replace(/[\t ]+$/gm, "").trimEnd(), session };
|
|
@@ -10918,31 +11114,26 @@ function startStudioReplSession(runtime: StudioReplRuntime, cwd: string, options
|
|
|
10918
11114
|
const sessionName = options?.newSession ? getNewStudioReplSessionName(runtime, commandOverride) : getStudioReplSessionName(runtime, commandOverride);
|
|
10919
11115
|
const existing = runStudioTmux(["has-session", "-t", sessionName], { timeout: 3_000 });
|
|
10920
11116
|
if (existing.ok) {
|
|
10921
|
-
const
|
|
11117
|
+
const session = inspectStudioReplSession(sessionName);
|
|
11118
|
+
if (!session) return { ok: false, message: `Could not inspect existing REPL session ${sessionName}.` };
|
|
10922
11119
|
return {
|
|
10923
11120
|
ok: true,
|
|
10924
|
-
session
|
|
10925
|
-
sessionName,
|
|
10926
|
-
target: getStudioReplPaneTarget(sessionName),
|
|
10927
|
-
runtime: inferred.runtime,
|
|
10928
|
-
label: formatStudioReplSessionLabel(sessionName, inferred.runtime, inferred.source),
|
|
10929
|
-
source: inferred.source,
|
|
10930
|
-
},
|
|
11121
|
+
session,
|
|
10931
11122
|
message: `${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL is already running.`,
|
|
10932
11123
|
};
|
|
10933
11124
|
}
|
|
10934
11125
|
const command = getStudioReplRuntimeCommand(runtime, commandOverride);
|
|
10935
11126
|
const result = runStudioTmux(buildStudioReplTmuxStartArgs(sessionName, cwd || process.cwd(), command), { timeout: 5_000 });
|
|
10936
11127
|
if (!result.ok) return { ok: false, message: result.message || `Failed to start ${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL.` };
|
|
11128
|
+
runStudioTmux(["set-option", "-q", "-t", sessionName, STUDIO_REPL_RUNTIME_OPTION, runtime], { timeout: 3_000 });
|
|
11129
|
+
const session = inspectStudioReplSession(sessionName);
|
|
11130
|
+
if (!session) {
|
|
11131
|
+
runStudioTmux(["kill-session", "-t", sessionName], { timeout: 3_000 });
|
|
11132
|
+
return { ok: false, message: `Started ${sessionName}, but could not initialize its shared session metadata.` };
|
|
11133
|
+
}
|
|
10937
11134
|
return {
|
|
10938
11135
|
ok: true,
|
|
10939
|
-
session
|
|
10940
|
-
sessionName,
|
|
10941
|
-
target: getStudioReplPaneTarget(sessionName),
|
|
10942
|
-
runtime,
|
|
10943
|
-
label: formatStudioReplSessionLabel(sessionName, runtime, "studio"),
|
|
10944
|
-
source: "studio",
|
|
10945
|
-
},
|
|
11136
|
+
session,
|
|
10946
11137
|
message: `Started ${options?.newSession ? "new " : ""}${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL${commandOverride ? ` with custom command: ${commandOverride}` : ""}.`,
|
|
10947
11138
|
};
|
|
10948
11139
|
}
|
|
@@ -10980,7 +11171,15 @@ type StudioReplSendSuccess = {
|
|
|
10980
11171
|
controlFiles?: StudioReplControlFiles;
|
|
10981
11172
|
};
|
|
10982
11173
|
|
|
10983
|
-
type StudioReplSendFailure = {
|
|
11174
|
+
type StudioReplSendFailure = {
|
|
11175
|
+
ok: false;
|
|
11176
|
+
message: string;
|
|
11177
|
+
submissionStarted?: boolean;
|
|
11178
|
+
runtime?: StudioReplRuntime | "unknown";
|
|
11179
|
+
usedControlFile?: boolean;
|
|
11180
|
+
submissionText?: string;
|
|
11181
|
+
controlFiles?: StudioReplControlFiles;
|
|
11182
|
+
};
|
|
10984
11183
|
|
|
10985
11184
|
function sleep(ms: number): Promise<void> {
|
|
10986
11185
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
@@ -11007,7 +11206,9 @@ function getStudioReplControlFiles(sessionName: string, runtime: StudioReplRunti
|
|
|
11007
11206
|
? "ghci"
|
|
11008
11207
|
: runtime === "clojure"
|
|
11009
11208
|
? "clj"
|
|
11010
|
-
: "
|
|
11209
|
+
: runtime === "shell"
|
|
11210
|
+
? "sh"
|
|
11211
|
+
: "py";
|
|
11011
11212
|
return {
|
|
11012
11213
|
dir,
|
|
11013
11214
|
sourceFile: join(dir, `studio-repl-${safeRuntime}.${extension}`),
|
|
@@ -11122,22 +11323,28 @@ function buildStudioReplControlSource(runtime: StudioReplRuntime, code: string,
|
|
|
11122
11323
|
if (runtime === "r") return buildStudioRControlSource(code, doneFile);
|
|
11123
11324
|
if (runtime === "ghci") return `${code.replace(/\r/g, "").trimEnd()}\n:! touch ${shellQuote(doneFile)}\n`;
|
|
11124
11325
|
if (runtime === "clojure") return buildStudioClojureControlSource(code, doneFile);
|
|
11326
|
+
if (runtime === "shell") return `${code.replace(/\r/g, "").trimEnd()}\n`;
|
|
11125
11327
|
return null;
|
|
11126
11328
|
}
|
|
11127
11329
|
|
|
11128
|
-
function buildStudioReplSubmissionLine(runtime: StudioReplRuntime, sourceFile: string): string {
|
|
11330
|
+
function buildStudioReplSubmissionLine(runtime: StudioReplRuntime, sourceFile: string, doneFile: string): string {
|
|
11129
11331
|
const quotedPath = JSON.stringify(sourceFile);
|
|
11130
11332
|
if (runtime === "julia") return `include(${quotedPath})`;
|
|
11131
11333
|
if (runtime === "r") return `source(${quotedPath}, local=.GlobalEnv)`;
|
|
11132
11334
|
if (runtime === "ghci") return `:script ${quotedPath}`;
|
|
11133
11335
|
if (runtime === "clojure") return `(do (load-file ${quotedPath}) :pi-studio/silent)`;
|
|
11336
|
+
if (runtime === "shell") return `. ${shellQuote(sourceFile)}; touch ${shellQuote(doneFile)}`;
|
|
11134
11337
|
return `exec(open(${quotedPath}, encoding="utf-8").read(), globals())`;
|
|
11135
11338
|
}
|
|
11136
11339
|
|
|
11137
|
-
function prepareStudioReplSubmission(
|
|
11340
|
+
function prepareStudioReplSubmission(
|
|
11341
|
+
sessionName: string,
|
|
11342
|
+
source: string,
|
|
11343
|
+
runtimeHint?: StudioReplRuntime | "unknown",
|
|
11344
|
+
): StudioReplPreparedSubmission {
|
|
11138
11345
|
const normalizedSource = String(source || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
11139
|
-
const runtime = inferStudioReplSessionRuntime(sessionName).runtime;
|
|
11140
|
-
if (runtime !== "unknown"
|
|
11346
|
+
const runtime = runtimeHint && runtimeHint !== "unknown" ? runtimeHint : inferStudioReplSessionRuntime(sessionName).runtime;
|
|
11347
|
+
if (runtime !== "unknown") {
|
|
11141
11348
|
const controlFiles = getStudioReplControlFiles(sessionName, runtime);
|
|
11142
11349
|
const controlSource = buildStudioReplControlSource(runtime, normalizedSource, controlFiles.doneFile);
|
|
11143
11350
|
if (controlSource) {
|
|
@@ -11148,7 +11355,7 @@ function prepareStudioReplSubmission(sessionName: string, source: string): Studi
|
|
|
11148
11355
|
// Ignore stale done file cleanup failures.
|
|
11149
11356
|
}
|
|
11150
11357
|
writeFileSync(controlFiles.sourceFile, controlSource, "utf-8");
|
|
11151
|
-
const submissionLine = buildStudioReplSubmissionLine(runtime, controlFiles.sourceFile);
|
|
11358
|
+
const submissionLine = buildStudioReplSubmissionLine(runtime, controlFiles.sourceFile, controlFiles.doneFile);
|
|
11152
11359
|
return {
|
|
11153
11360
|
runtime,
|
|
11154
11361
|
usedControlFile: true,
|
|
@@ -11165,32 +11372,45 @@ function prepareStudioReplSubmission(sessionName: string, source: string): Studi
|
|
|
11165
11372
|
};
|
|
11166
11373
|
}
|
|
11167
11374
|
|
|
11168
|
-
function pasteTextToStudioReplPane(sessionName: string, text: string): { ok: true } | { ok: false; message: string } {
|
|
11375
|
+
function pasteTextToStudioReplPane(sessionName: string, text: string, paneTarget?: string): { ok: true } | { ok: false; message: string; submissionStarted: boolean } {
|
|
11169
11376
|
const bufferName = `pi-studio-repl-${randomUUID().replace(/-/g, "")}`;
|
|
11170
|
-
const target = getStudioReplPaneTarget(sessionName);
|
|
11377
|
+
const target = paneTarget || getStudioReplPaneTarget(sessionName);
|
|
11171
11378
|
const loadResult = runStudioTmux(["load-buffer", "-b", bufferName, "-"], { input: text, timeout: 5_000 });
|
|
11172
|
-
if (!loadResult.ok) return { ok: false, message: loadResult.message || "Failed to load text into tmux buffer." };
|
|
11379
|
+
if (!loadResult.ok) return { ok: false, message: loadResult.message || "Failed to load text into tmux buffer.", submissionStarted: false };
|
|
11173
11380
|
try {
|
|
11174
11381
|
const pasteResult = runStudioTmux(["paste-buffer", "-d", "-b", bufferName, "-t", target], { timeout: 5_000 });
|
|
11175
|
-
if (!pasteResult.ok) return { ok: false, message: pasteResult.message || "Failed to paste text into REPL session." };
|
|
11382
|
+
if (!pasteResult.ok) return { ok: false, message: pasteResult.message || "Failed to paste text into REPL session.", submissionStarted: false };
|
|
11176
11383
|
const enterResult = runStudioTmux(["send-keys", "-t", target, "C-m"], { timeout: 5_000 });
|
|
11177
|
-
if (!enterResult.ok) return { ok: false, message: enterResult.message || "Failed to send Enter to REPL session." };
|
|
11384
|
+
if (!enterResult.ok) return { ok: false, message: enterResult.message || "Failed to send Enter to REPL session.", submissionStarted: true };
|
|
11178
11385
|
return { ok: true };
|
|
11179
11386
|
} finally {
|
|
11180
11387
|
runStudioTmux(["delete-buffer", "-b", bufferName], { timeout: 2_000 });
|
|
11181
11388
|
}
|
|
11182
11389
|
}
|
|
11183
11390
|
|
|
11184
|
-
function sendTextToStudioReplSession(
|
|
11391
|
+
function sendTextToStudioReplSession(
|
|
11392
|
+
sessionName: string,
|
|
11393
|
+
text: string,
|
|
11394
|
+
paneTarget?: string,
|
|
11395
|
+
runtimeHint?: StudioReplRuntime | "unknown",
|
|
11396
|
+
): StudioReplSendSuccess | StudioReplSendFailure {
|
|
11185
11397
|
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
|
|
11186
11398
|
const source = String(text || "");
|
|
11187
11399
|
if (!source.trim()) return { ok: false, message: "Editor text is empty." };
|
|
11188
11400
|
if (source.length > STUDIO_REPL_SEND_MAX_CHARS) {
|
|
11189
11401
|
return { ok: false, message: `REPL input is too large (${source.length} chars; max ${STUDIO_REPL_SEND_MAX_CHARS}).` };
|
|
11190
11402
|
}
|
|
11191
|
-
const prepared = prepareStudioReplSubmission(sessionName, source);
|
|
11192
|
-
const pasted = pasteTextToStudioReplPane(sessionName, prepared.submissionText);
|
|
11193
|
-
if (!pasted.ok) return {
|
|
11403
|
+
const prepared = prepareStudioReplSubmission(sessionName, source, runtimeHint);
|
|
11404
|
+
const pasted = pasteTextToStudioReplPane(sessionName, prepared.submissionText, paneTarget);
|
|
11405
|
+
if (!pasted.ok) return {
|
|
11406
|
+
ok: false,
|
|
11407
|
+
message: pasted.message,
|
|
11408
|
+
submissionStarted: pasted.submissionStarted,
|
|
11409
|
+
runtime: prepared.runtime,
|
|
11410
|
+
usedControlFile: prepared.usedControlFile,
|
|
11411
|
+
submissionText: prepared.submissionText,
|
|
11412
|
+
controlFiles: prepared.controlFiles,
|
|
11413
|
+
};
|
|
11194
11414
|
return {
|
|
11195
11415
|
ok: true,
|
|
11196
11416
|
message: "Sent to REPL.",
|
|
@@ -11230,6 +11450,7 @@ function stripStudioReplSubmissionEcho(output: string): string {
|
|
|
11230
11450
|
/^.*source\([\s\S]*?pi-studio-re[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
|
|
11231
11451
|
/^.*:script\s+[\s\S]*?pi-studio-re[\s\S]*?\.ghci"?\s*$/gm,
|
|
11232
11452
|
/^.*\(do\s+\(load-file\s+[\s\S]*?pi-studio-re[\s\S]*?:pi-studio\/silent\)\s*$/gm,
|
|
11453
|
+
/^.*\.\s+[\s\S]*?pi-studio-re[\s\S]*?\.sh[\s\S]*?done\.flag.*$/gm,
|
|
11233
11454
|
];
|
|
11234
11455
|
for (const pattern of submissionEchoPatterns) value = value.replace(pattern, "");
|
|
11235
11456
|
return value.replace(/^(?:\s*\n)+/, "").replace(/[\t ]+$/gm, "").trimEnd();
|
|
@@ -11252,18 +11473,27 @@ function normalizeStudioReplJournalMode(mode: unknown): StudioReplJournalEntry["
|
|
|
11252
11473
|
}
|
|
11253
11474
|
|
|
11254
11475
|
function normalizeStudioReplJournalStatus(status: unknown): StudioReplJournalEntry["status"] {
|
|
11255
|
-
return status === "
|
|
11476
|
+
return status === "sending"
|
|
11477
|
+
|| status === "captured"
|
|
11478
|
+
|| status === "timeout"
|
|
11479
|
+
|| status === "error"
|
|
11480
|
+
|| status === "note"
|
|
11481
|
+
? status
|
|
11482
|
+
: "sent";
|
|
11256
11483
|
}
|
|
11257
11484
|
|
|
11258
|
-
function makeStudioReplJournalEntry(details: Partial<StudioReplJournalEntry> & { sessionName: string; code
|
|
11485
|
+
function makeStudioReplJournalEntry(details: Partial<StudioReplJournalEntry> & { sessionName: string; code?: string }): StudioReplJournalEntry {
|
|
11259
11486
|
const now = Date.now();
|
|
11487
|
+
const createdAt = typeof details.createdAt === "number" && Number.isFinite(details.createdAt) ? details.createdAt : now;
|
|
11260
11488
|
return {
|
|
11261
11489
|
id: typeof details.id === "string" && details.id.trim() ? details.id.trim() : `repl-journal-${now.toString(36)}-${randomUUID().slice(0, 8)}`,
|
|
11262
11490
|
requestId: typeof details.requestId === "string" ? details.requestId : "",
|
|
11263
|
-
createdAt
|
|
11491
|
+
createdAt,
|
|
11264
11492
|
updatedAt: typeof details.updatedAt === "number" && Number.isFinite(details.updatedAt) ? details.updatedAt : now,
|
|
11493
|
+
completedAt: typeof details.completedAt === "number" && Number.isFinite(details.completedAt) ? details.completedAt : null,
|
|
11265
11494
|
sessionName: String(details.sessionName || ""),
|
|
11266
|
-
runtime: details.runtime || "unknown",
|
|
11495
|
+
runtime: normalizeStudioReplRuntime(details.runtime) || "unknown",
|
|
11496
|
+
origin: details.origin === "pi-repl" || details.origin === "pi-studio" ? details.origin : "unknown",
|
|
11267
11497
|
label: typeof details.label === "string" && details.label.trim() ? details.label.trim() : "REPL send",
|
|
11268
11498
|
mode: normalizeStudioReplJournalMode(details.mode),
|
|
11269
11499
|
prose: typeof details.prose === "string" ? details.prose : "",
|
|
@@ -11271,6 +11501,10 @@ function makeStudioReplJournalEntry(details: Partial<StudioReplJournalEntry> & {
|
|
|
11271
11501
|
output: typeof details.output === "string" ? details.output : "",
|
|
11272
11502
|
status: normalizeStudioReplJournalStatus(details.status),
|
|
11273
11503
|
skippedChunks: Math.max(0, Math.floor(Number(details.skippedChunks) || 0)),
|
|
11504
|
+
codeOmittedChars: Math.max(0, Math.floor(Number(details.codeOmittedChars) || 0)),
|
|
11505
|
+
proseOmittedChars: Math.max(0, Math.floor(Number(details.proseOmittedChars) || 0)),
|
|
11506
|
+
outputOmittedChars: Math.max(0, Math.floor(Number(details.outputOmittedChars) || 0)),
|
|
11507
|
+
sharedSynced: details.sharedSynced === true,
|
|
11274
11508
|
};
|
|
11275
11509
|
}
|
|
11276
11510
|
|
|
@@ -11293,47 +11527,192 @@ function upsertStudioReplJournalEntry(entry: StudioReplJournalEntry): StudioRepl
|
|
|
11293
11527
|
studioReplJournalEntries = studioReplJournalEntries
|
|
11294
11528
|
.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0))
|
|
11295
11529
|
.slice(-STUDIO_REPL_JOURNAL_MAX_ENTRIES);
|
|
11530
|
+
const retainedIds = new Set(studioReplJournalEntries.map((candidate) => candidate.id));
|
|
11531
|
+
for (const unsyncedId of studioReplUnsyncedJournalEntryIds) {
|
|
11532
|
+
if (!retainedIds.has(unsyncedId)) studioReplUnsyncedJournalEntryIds.delete(unsyncedId);
|
|
11533
|
+
}
|
|
11296
11534
|
return studioReplJournalEntries.find((candidate) => candidate.id === entry.id || (entry.requestId && candidate.requestId === entry.requestId)) || entry;
|
|
11297
11535
|
}
|
|
11298
11536
|
|
|
11299
|
-
function
|
|
11300
|
-
|
|
11537
|
+
function getStudioReplSessionRecordIdentity(session: StudioReplSessionInfo): {
|
|
11538
|
+
sessionName: string;
|
|
11539
|
+
tmuxSessionId: string;
|
|
11540
|
+
tmuxSessionCreatedAt: number;
|
|
11541
|
+
runtime: StudioReplRuntime | "unknown";
|
|
11542
|
+
} {
|
|
11543
|
+
return {
|
|
11544
|
+
sessionName: session.sessionName,
|
|
11545
|
+
tmuxSessionId: session.tmuxSessionId,
|
|
11546
|
+
tmuxSessionCreatedAt: session.tmuxSessionCreatedAt,
|
|
11547
|
+
runtime: session.runtime,
|
|
11548
|
+
};
|
|
11549
|
+
}
|
|
11550
|
+
|
|
11551
|
+
function isSameStudioReplSessionLifetime(left: StudioReplSessionInfo, right: StudioReplSessionInfo): boolean {
|
|
11552
|
+
return left.sessionName === right.sessionName
|
|
11553
|
+
&& left.tmuxSessionId === right.tmuxSessionId
|
|
11554
|
+
&& left.tmuxSessionCreatedAt === right.tmuxSessionCreatedAt;
|
|
11555
|
+
}
|
|
11556
|
+
|
|
11557
|
+
function recordStudioReplJournalEntry(details: Partial<StudioReplJournalEntry> & { sessionName: string; code?: string }): StudioReplJournalEntry {
|
|
11558
|
+
let entry = upsertStudioReplJournalEntry(makeStudioReplJournalEntry({
|
|
11559
|
+
...details,
|
|
11560
|
+
origin: details.origin || "pi-studio",
|
|
11561
|
+
sharedSynced: false,
|
|
11562
|
+
}));
|
|
11563
|
+
const localEntryId = entry.id;
|
|
11564
|
+
studioReplUnsyncedJournalEntryIds.add(localEntryId);
|
|
11565
|
+
const session = inspectStudioReplSession(entry.sessionName);
|
|
11566
|
+
if (!session?.recordId || session.recordWarning) return entry;
|
|
11567
|
+
try {
|
|
11568
|
+
const recorded = upsertReplSessionRecordEntry(
|
|
11569
|
+
session.recordId,
|
|
11570
|
+
getStudioReplSessionRecordIdentity(session),
|
|
11571
|
+
entry,
|
|
11572
|
+
{ origin: entry.origin === "pi-repl" ? "pi-repl" : "pi-studio" },
|
|
11573
|
+
);
|
|
11574
|
+
entry = upsertStudioReplJournalEntry(makeStudioReplJournalEntry({
|
|
11575
|
+
...(recorded.entry as Partial<StudioReplJournalEntry> & { sessionName: string }),
|
|
11576
|
+
sharedSynced: true,
|
|
11577
|
+
}));
|
|
11578
|
+
studioReplUnsyncedJournalEntryIds.delete(localEntryId);
|
|
11579
|
+
studioReplUnsyncedJournalEntryIds.delete(entry.id);
|
|
11580
|
+
} catch {
|
|
11581
|
+
// Retain the in-memory entry as a standalone fallback when shared state is unavailable.
|
|
11582
|
+
}
|
|
11583
|
+
return entry;
|
|
11584
|
+
}
|
|
11585
|
+
|
|
11586
|
+
function getStudioReplJournalEntries(sessionName: string | null | undefined): StudioReplJournalEntry[] {
|
|
11587
|
+
const normalizedSessionName = String(sessionName || "").trim();
|
|
11588
|
+
const sessions = normalizedSessionName
|
|
11589
|
+
? [inspectStudioReplSession(normalizedSessionName)].filter((session): session is StudioReplSessionInfo => Boolean(session))
|
|
11590
|
+
: listStudioReplSessions().sessions;
|
|
11591
|
+
for (const session of sessions) {
|
|
11592
|
+
if (!session.recordId || session.recordWarning) continue;
|
|
11593
|
+
try {
|
|
11594
|
+
const record = readReplSessionRecord(session.recordId, getStudioReplSessionRecordIdentity(session));
|
|
11595
|
+
if (record) {
|
|
11596
|
+
const unsynced = studioReplJournalEntries.filter((entry) => (
|
|
11597
|
+
entry.sessionName === session.sessionName && studioReplUnsyncedJournalEntryIds.has(entry.id)
|
|
11598
|
+
));
|
|
11599
|
+
const sharedEntries = record.entries.map((sharedEntry: Partial<StudioReplJournalEntry> & { sessionName: string }) => (
|
|
11600
|
+
makeStudioReplJournalEntry({ ...sharedEntry, sharedSynced: true })
|
|
11601
|
+
));
|
|
11602
|
+
for (const sharedEntry of sharedEntries) studioReplUnsyncedJournalEntryIds.delete(sharedEntry.id);
|
|
11603
|
+
studioReplJournalEntries = [
|
|
11604
|
+
...studioReplJournalEntries.filter((entry) => entry.sessionName !== session.sessionName),
|
|
11605
|
+
...sharedEntries,
|
|
11606
|
+
...unsynced.filter((entry) => !sharedEntries.some((sharedEntry: StudioReplJournalEntry) => sharedEntry.id === entry.id)),
|
|
11607
|
+
].sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0)).slice(-STUDIO_REPL_JOURNAL_MAX_ENTRIES);
|
|
11608
|
+
}
|
|
11609
|
+
} catch {
|
|
11610
|
+
// The raw tmux mirror and in-memory Studio record remain independently usable.
|
|
11611
|
+
}
|
|
11612
|
+
}
|
|
11613
|
+
const entries = normalizedSessionName
|
|
11614
|
+
? studioReplJournalEntries.filter((entry) => entry.sessionName === normalizedSessionName)
|
|
11615
|
+
: studioReplJournalEntries;
|
|
11616
|
+
return entries.slice(-STUDIO_REPL_JOURNAL_MAX_ENTRIES).map((entry) => ({
|
|
11617
|
+
...entry,
|
|
11618
|
+
sharedSynced: !studioReplUnsyncedJournalEntryIds.has(entry.id) && entry.sharedSynced === true,
|
|
11619
|
+
}));
|
|
11301
11620
|
}
|
|
11302
11621
|
|
|
11303
11622
|
function updateStudioReplJournalEntryOutput(requestId: string, sessionName: string, output: string, status: StudioReplJournalEntry["status"]): void {
|
|
11304
11623
|
const normalizedRequestId = String(requestId || "");
|
|
11305
11624
|
const normalizedSessionName = String(sessionName || "");
|
|
11306
|
-
const existing =
|
|
11625
|
+
const existing = getStudioReplJournalEntries(normalizedSessionName).slice().reverse().find((entry) => (
|
|
11307
11626
|
(normalizedRequestId && entry.requestId === normalizedRequestId)
|
|
11308
|
-
|| (!normalizedRequestId &&
|
|
11627
|
+
|| (!normalizedRequestId && entry.status === "sent")
|
|
11309
11628
|
));
|
|
11310
11629
|
if (!existing) return;
|
|
11311
|
-
|
|
11630
|
+
recordStudioReplJournalEntry({
|
|
11312
11631
|
...existing,
|
|
11313
11632
|
output: String(output || ""),
|
|
11314
11633
|
status,
|
|
11634
|
+
completedAt: Date.now(),
|
|
11315
11635
|
updatedAt: Date.now(),
|
|
11316
11636
|
});
|
|
11317
11637
|
}
|
|
11318
11638
|
|
|
11319
|
-
function
|
|
11639
|
+
function clearStudioReplJournal(sessionName: string): void {
|
|
11320
11640
|
const normalizedSessionName = String(sessionName || "").trim();
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11641
|
+
if (!normalizedSessionName) return;
|
|
11642
|
+
const clearedIds = studioReplJournalEntries.filter((entry) => entry.sessionName === normalizedSessionName).map((entry) => entry.id);
|
|
11643
|
+
studioReplJournalEntries = studioReplJournalEntries.filter((entry) => entry.sessionName !== normalizedSessionName);
|
|
11644
|
+
for (const id of clearedIds) studioReplUnsyncedJournalEntryIds.delete(id);
|
|
11645
|
+
const session = inspectStudioReplSession(normalizedSessionName);
|
|
11646
|
+
if (!session?.recordId || session.recordWarning) return;
|
|
11647
|
+
clearReplSessionRecord(session.recordId, getStudioReplSessionRecordIdentity(session));
|
|
11325
11648
|
}
|
|
11326
11649
|
|
|
11327
|
-
async function waitForStudioReplDoneFile(doneFile: string | undefined, timeoutMs: number): Promise<boolean> {
|
|
11650
|
+
async function waitForStudioReplDoneFile(doneFile: string | undefined, timeoutMs: number, signal?: AbortSignal): Promise<boolean> {
|
|
11328
11651
|
if (!doneFile) return false;
|
|
11329
11652
|
const deadline = Date.now() + clampStudioReplSendTimeout(timeoutMs);
|
|
11330
11653
|
while (Date.now() < deadline) {
|
|
11331
11654
|
if (existsSync(doneFile)) return true;
|
|
11655
|
+
if (signal?.aborted) return false;
|
|
11332
11656
|
await sleep(100);
|
|
11333
11657
|
}
|
|
11334
11658
|
return existsSync(doneFile);
|
|
11335
11659
|
}
|
|
11336
11660
|
|
|
11661
|
+
function sleepWithoutKeepingStudioProcessAlive(ms: number): Promise<void> {
|
|
11662
|
+
return new Promise((resolveSleep) => {
|
|
11663
|
+
const timer = setTimeout(resolveSleep, ms);
|
|
11664
|
+
timer.unref?.();
|
|
11665
|
+
});
|
|
11666
|
+
}
|
|
11667
|
+
|
|
11668
|
+
function retainStudioReplSendLeaseUntilSubmissionSettles(
|
|
11669
|
+
session: StudioReplSessionInfo,
|
|
11670
|
+
doneFile: string,
|
|
11671
|
+
lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>>,
|
|
11672
|
+
): void {
|
|
11673
|
+
// A caller timeout or abort does not stop code already submitted to tmux.
|
|
11674
|
+
// Continue heartbeating the shared lease until the runtime wrapper reports
|
|
11675
|
+
// completion, or until this exact tmux session lifetime disappears.
|
|
11676
|
+
void (async () => {
|
|
11677
|
+
let nextIdentityCheck = 0;
|
|
11678
|
+
let missingChecks = 0;
|
|
11679
|
+
try {
|
|
11680
|
+
while (!existsSync(doneFile)) {
|
|
11681
|
+
if (Date.now() >= nextIdentityCheck) {
|
|
11682
|
+
try {
|
|
11683
|
+
const current = inspectStudioReplSession(session.sessionName);
|
|
11684
|
+
if (current && isSameStudioReplSessionLifetime(session, current)) {
|
|
11685
|
+
missingChecks = 0;
|
|
11686
|
+
} else {
|
|
11687
|
+
missingChecks += 1;
|
|
11688
|
+
if (missingChecks >= 3) return;
|
|
11689
|
+
}
|
|
11690
|
+
} catch {
|
|
11691
|
+
// A transient inspection failure must not make overlapping sends safe.
|
|
11692
|
+
missingChecks = 0;
|
|
11693
|
+
}
|
|
11694
|
+
nextIdentityCheck = Date.now() + 1_000;
|
|
11695
|
+
}
|
|
11696
|
+
await sleepWithoutKeepingStudioProcessAlive(100);
|
|
11697
|
+
}
|
|
11698
|
+
} finally {
|
|
11699
|
+
await lease.release().catch(() => undefined);
|
|
11700
|
+
}
|
|
11701
|
+
})();
|
|
11702
|
+
}
|
|
11703
|
+
|
|
11704
|
+
async function releaseOrRetainStudioReplSendLease(
|
|
11705
|
+
lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>>,
|
|
11706
|
+
session: StudioReplSessionInfo | null,
|
|
11707
|
+
doneFile: string | undefined,
|
|
11708
|
+
): Promise<void> {
|
|
11709
|
+
if (session && doneFile && !existsSync(doneFile)) {
|
|
11710
|
+
retainStudioReplSendLeaseUntilSubmissionSettles(session, doneFile, lease);
|
|
11711
|
+
return;
|
|
11712
|
+
}
|
|
11713
|
+
await lease.release().catch(() => undefined);
|
|
11714
|
+
}
|
|
11715
|
+
|
|
11337
11716
|
function interruptStudioReplSession(sessionName: string): { ok: true; message: string } | { ok: false; message: string } {
|
|
11338
11717
|
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
|
|
11339
11718
|
const result = runStudioTmux(["send-keys", "-t", getStudioReplPaneTarget(sessionName), "C-c"], { timeout: 5_000 });
|
|
@@ -11903,10 +12282,12 @@ ${cssVarsBlock}
|
|
|
11903
12282
|
<section id="leftPane">
|
|
11904
12283
|
<div id="leftSectionHeader" class="section-header">
|
|
11905
12284
|
<div class="section-header-main">
|
|
11906
|
-
<
|
|
11907
|
-
<
|
|
11908
|
-
|
|
11909
|
-
|
|
12285
|
+
<span id="editorViewSelectWrap" class="studio-header-select-wrap">
|
|
12286
|
+
<select id="editorViewSelect" aria-label="Editor view mode" title="Editor view mode. Shortcut: F7 when the editor pane is active; F6 switches panes.">
|
|
12287
|
+
<option value="markdown" selected>Editor (Raw)</option>
|
|
12288
|
+
<option value="preview">Editor (Preview)</option>
|
|
12289
|
+
</select>
|
|
12290
|
+
</span>
|
|
11910
12291
|
</div>
|
|
11911
12292
|
<div class="section-header-actions">
|
|
11912
12293
|
<button id="leftFocusBtn" class="pane-focus-btn" type="button" title="Show only the editor pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
|
|
@@ -11934,7 +12315,7 @@ ${cssVarsBlock}
|
|
|
11934
12315
|
</div>
|
|
11935
12316
|
<div class="source-actions-row repl-action-line" hidden>
|
|
11936
12317
|
<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>
|
|
11937
|
-
<select id="replSendModeSelect" hidden aria-label="REPL send mode" title="Choose how Send to REPL interprets the editor text.">
|
|
12318
|
+
<select id="replSendModeSelect" class="studio-flat-select" hidden aria-label="REPL send mode" title="Choose how Send to REPL interprets the editor text.">
|
|
11938
12319
|
<option value="raw" selected>Send mode: Raw</option>
|
|
11939
12320
|
<option value="literate">Send mode: Literate</option>
|
|
11940
12321
|
</select>
|
|
@@ -11943,11 +12324,11 @@ ${cssVarsBlock}
|
|
|
11943
12324
|
<button id="copyDraftBtn" type="button" title="Copy the current editor text to the clipboard.">Copy</button>
|
|
11944
12325
|
<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>
|
|
11945
12326
|
<button id="suggestCompletionOptionsBtn" type="button" hidden title="Suggestion context options">▾</button>
|
|
11946
|
-
<select id="completionContextSelect" hidden aria-label="Suggestion context mode" title="Choose how much context Suggest includes.">
|
|
12327
|
+
<select id="completionContextSelect" class="studio-flat-select" hidden aria-label="Suggestion context mode" title="Choose how much context Suggest includes.">
|
|
11947
12328
|
<option value="cursor" selected>Context: editor only</option>
|
|
11948
12329
|
<option value="session">Context: editor + latest response</option>
|
|
11949
12330
|
</select>
|
|
11950
|
-
<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.">
|
|
12331
|
+
<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.">
|
|
11951
12332
|
<option value="current" selected>Suggestion model: current Pi model</option>
|
|
11952
12333
|
</select>
|
|
11953
12334
|
<button id="openCompanionBtn" type="button" title="Open a blank editor-only Studio tab.">New editor tab</button>
|
|
@@ -11955,7 +12336,7 @@ ${cssVarsBlock}
|
|
|
11955
12336
|
</div>
|
|
11956
12337
|
<div class="source-actions-row">
|
|
11957
12338
|
<button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Annotation header</button>
|
|
11958
|
-
<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.">
|
|
12339
|
+
<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.">
|
|
11959
12340
|
<option value="on" selected>Inline annotations: On</option>
|
|
11960
12341
|
<option value="off">Inline annotations: Hide</option>
|
|
11961
12342
|
</select>
|
|
@@ -11963,7 +12344,7 @@ ${cssVarsBlock}
|
|
|
11963
12344
|
<button id="saveAnnotatedBtn" type="button" title="Save full editor content (including [an: ...] markers) as a .annotated.md file.">Save .annotated.md</button>
|
|
11964
12345
|
</div>
|
|
11965
12346
|
<div class="source-actions-row">
|
|
11966
|
-
<select id="lensSelect" aria-label="Critique focus">
|
|
12347
|
+
<select id="lensSelect" class="studio-flat-select" aria-label="Critique focus">
|
|
11967
12348
|
<option value="auto" selected>Critique: Auto</option>
|
|
11968
12349
|
<option value="writing">Critique: Writing</option>
|
|
11969
12350
|
<option value="code">Critique: Code</option>
|
|
@@ -11973,7 +12354,7 @@ ${cssVarsBlock}
|
|
|
11973
12354
|
<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>
|
|
11974
12355
|
<button id="askAsideBtn" type="button" title="Open a separate side-question thread without adding it to the main Pi conversation.">Side question</button>
|
|
11975
12356
|
<button id="quizBtn" type="button" title="Open an active quiz for the current editor selection or document.">Quiz me</button>
|
|
11976
|
-
<select id="highlightSelect" aria-label="Editor syntax highlighting">
|
|
12357
|
+
<select id="highlightSelect" class="studio-flat-select" aria-label="Editor syntax highlighting">
|
|
11977
12358
|
<option value="off">Syntax highlight: Off</option>
|
|
11978
12359
|
<option value="bash">Syntax highlight: Bash</option>
|
|
11979
12360
|
<option value="c">Syntax highlight: C</option>
|
|
@@ -12003,11 +12384,11 @@ ${cssVarsBlock}
|
|
|
12003
12384
|
<option value="xml">Syntax highlight: XML</option>
|
|
12004
12385
|
<option value="yaml">Syntax highlight: YAML</option>
|
|
12005
12386
|
</select>
|
|
12006
|
-
<select id="lineNumbersSelect" aria-label="Editor line numbers">
|
|
12387
|
+
<select id="lineNumbersSelect" class="studio-flat-select" aria-label="Editor line numbers">
|
|
12007
12388
|
<option value="off">Line numbers: Off</option>
|
|
12008
12389
|
<option value="on" selected>Line numbers: On</option>
|
|
12009
12390
|
</select>
|
|
12010
|
-
<select id="editorFontSizeSelect" aria-label="Editor text size" title="Adjust raw editor text size.">
|
|
12391
|
+
<select id="editorFontSizeSelect" class="studio-flat-select" aria-label="Editor text size" title="Adjust raw editor text size.">
|
|
12011
12392
|
<option value="10">Editor text: 10px</option>
|
|
12012
12393
|
<option value="11">Editor text: 11px</option>
|
|
12013
12394
|
<option value="12" selected>Editor text: 12px</option>
|
|
@@ -12105,17 +12486,19 @@ ${cssVarsBlock}
|
|
|
12105
12486
|
<section id="rightPane">
|
|
12106
12487
|
<div id="rightSectionHeader" class="section-header">
|
|
12107
12488
|
<div class="section-header-main">
|
|
12108
|
-
<
|
|
12109
|
-
<
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12489
|
+
<span id="rightViewSelectWrap" class="studio-header-select-wrap">
|
|
12490
|
+
<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.">
|
|
12491
|
+
<option value="markdown">Response (Raw)</option>
|
|
12492
|
+
<option value="preview" selected>Response (Preview)</option>
|
|
12493
|
+
<option value="editor-preview">Editor (Preview)</option>
|
|
12494
|
+
<option value="editor-quarto-preview" hidden>Editor (Quarto Preview)</option>
|
|
12495
|
+
<option value="trace">Working</option>
|
|
12496
|
+
<option value="changes">Changes</option>
|
|
12497
|
+
<option value="files">Files</option>
|
|
12498
|
+
<option value="repl">REPL</option>
|
|
12499
|
+
<option value="side-questions">Side questions</option>
|
|
12500
|
+
</select>
|
|
12501
|
+
</span>
|
|
12119
12502
|
</div>
|
|
12120
12503
|
<div class="section-header-actions">
|
|
12121
12504
|
<button id="rightFocusBtn" class="pane-focus-btn" type="button" title="Show only the response pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
|
|
@@ -12142,28 +12525,34 @@ ${cssVarsBlock}
|
|
|
12142
12525
|
<div class="response-wrap">
|
|
12143
12526
|
<div id="responseActions" class="response-actions">
|
|
12144
12527
|
<div class="response-actions-row response-options-row">
|
|
12145
|
-
<
|
|
12146
|
-
<
|
|
12147
|
-
|
|
12148
|
-
|
|
12149
|
-
|
|
12150
|
-
|
|
12151
|
-
|
|
12152
|
-
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
|
|
12158
|
-
<
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12528
|
+
<span id="followSelectWrap" class="studio-menu-select-wrap response-option-select-wrap">
|
|
12529
|
+
<select id="followSelect" aria-label="Auto-update response">
|
|
12530
|
+
<option value="on" selected>Auto-update response: On</option>
|
|
12531
|
+
<option value="off">Auto-update response: Off</option>
|
|
12532
|
+
</select>
|
|
12533
|
+
</span>
|
|
12534
|
+
<span id="responseHighlightSelectWrap" class="studio-menu-select-wrap response-option-select-wrap">
|
|
12535
|
+
<select id="responseHighlightSelect" aria-label="Response markdown highlighting">
|
|
12536
|
+
<option value="off">Syntax highlight: Off</option>
|
|
12537
|
+
<option value="on" selected>Syntax highlight: On</option>
|
|
12538
|
+
</select>
|
|
12539
|
+
</span>
|
|
12540
|
+
<span id="responseFontSizeSelectWrap" class="studio-menu-select-wrap response-option-select-wrap">
|
|
12541
|
+
<select id="responseFontSizeSelect" aria-label="Response text size" title="Adjust right-pane response, preview, and working text size.">
|
|
12542
|
+
<option value="11">Response text: 11px</option>
|
|
12543
|
+
<option value="12">Response text: 12px</option>
|
|
12544
|
+
<option value="12.5">Response text: 12.5px</option>
|
|
12545
|
+
<option value="13">Response text: 13px</option>
|
|
12546
|
+
<option value="13.5" selected>Response text: 13.5px</option>
|
|
12547
|
+
<option value="14">Response text: 14px</option>
|
|
12548
|
+
<option value="14.5">Response text: 14.5px</option>
|
|
12549
|
+
<option value="15">Response text: 15px</option>
|
|
12550
|
+
<option value="15.5">Response text: 15.5px</option>
|
|
12551
|
+
<option value="16">Response text: 16px</option>
|
|
12552
|
+
<option value="18">Response text: 18px</option>
|
|
12553
|
+
<option value="20">Response text: 20px</option>
|
|
12554
|
+
</select>
|
|
12555
|
+
</span>
|
|
12167
12556
|
</div>
|
|
12168
12557
|
<div class="response-actions-row history-row">
|
|
12169
12558
|
<button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Fetch latest response</button>
|
|
@@ -12464,6 +12853,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12464
12853
|
parameters: STUDIO_REPL_STATUS_TOOL_PARAMS,
|
|
12465
12854
|
async execute(_toolCallId, params) {
|
|
12466
12855
|
const selected = selectStudioReplSessionForTool({ sessionName: params.sessionName, target: params.target });
|
|
12856
|
+
const recordEntries = selected.session ? getStudioReplJournalEntries(selected.session.sessionName) : [];
|
|
12467
12857
|
const lines = [
|
|
12468
12858
|
`Active Studio REPL: ${studioReplActiveSessionName || "none"}`,
|
|
12469
12859
|
`tmux sessions visible to Studio: ${selected.sessions.length}`,
|
|
@@ -12471,6 +12861,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
12471
12861
|
if (selected.error) lines.push(`Selection: ${selected.error}`);
|
|
12472
12862
|
if (selected.session) {
|
|
12473
12863
|
lines.push(`Selected: ${selected.session.sessionName} (${selected.session.runtime}, ${selected.session.source})`);
|
|
12864
|
+
if (selected.session.recordId && !selected.session.recordWarning) {
|
|
12865
|
+
lines.push(`Shared clean record: ${recordEntries.length} entries (${selected.session.recordId})`);
|
|
12866
|
+
}
|
|
12867
|
+
if (selected.session.recordWarning) lines.push(`Shared record warning: ${selected.session.recordWarning}`);
|
|
12474
12868
|
}
|
|
12475
12869
|
for (const session of selected.sessions) {
|
|
12476
12870
|
lines.push(`- ${session.sessionName} | runtime=${session.runtime} | source=${session.source} | target=${session.target}`);
|
|
@@ -12480,6 +12874,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12480
12874
|
details: {
|
|
12481
12875
|
activeSessionName: studioReplActiveSessionName,
|
|
12482
12876
|
selectedSession: selected.session,
|
|
12877
|
+
selectedRecordEntries: recordEntries,
|
|
12483
12878
|
sessions: selected.sessions,
|
|
12484
12879
|
} as Record<string, unknown>,
|
|
12485
12880
|
};
|
|
@@ -12498,7 +12893,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12498
12893
|
],
|
|
12499
12894
|
parameters: STUDIO_REPL_SEND_TOOL_PARAMS,
|
|
12500
12895
|
executionMode: "sequential",
|
|
12501
|
-
async execute(toolCallId, params) {
|
|
12896
|
+
async execute(toolCallId, params, signal) {
|
|
12502
12897
|
const selected = selectStudioReplSessionForTool({ sessionName: params.sessionName, target: params.target });
|
|
12503
12898
|
if (!selected.session) {
|
|
12504
12899
|
return {
|
|
@@ -12507,75 +12902,151 @@ export default function (pi: ExtensionAPI) {
|
|
|
12507
12902
|
};
|
|
12508
12903
|
}
|
|
12509
12904
|
|
|
12510
|
-
const before = captureStudioReplSession(selected.session.sessionName);
|
|
12511
|
-
const beforeTranscript = before.ok ? before.transcript : "";
|
|
12512
|
-
const sent = sendTextToStudioReplSession(selected.session.sessionName, params.code);
|
|
12513
|
-
if (!sent.ok) {
|
|
12514
|
-
return {
|
|
12515
|
-
content: [{ type: "text", text: sent.message }],
|
|
12516
|
-
details: { ok: false, error: sent.message, session: selected.session, sessions: selected.sessions } as Record<string, unknown>,
|
|
12517
|
-
};
|
|
12518
|
-
}
|
|
12519
|
-
studioReplActiveSessionName = selected.session.sessionName;
|
|
12520
|
-
|
|
12521
12905
|
const timeoutMs = clampStudioReplSendTimeout(params.timeoutMs);
|
|
12522
|
-
let
|
|
12523
|
-
|
|
12524
|
-
|
|
12525
|
-
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
|
|
12532
|
-
|
|
12533
|
-
|
|
12534
|
-
|
|
12535
|
-
|
|
12536
|
-
|
|
12537
|
-
|
|
12538
|
-
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
sessionName:
|
|
12543
|
-
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
12566
|
-
|
|
12906
|
+
let lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>> | null = null;
|
|
12907
|
+
let submittedSession: StudioReplSessionInfo | null = null;
|
|
12908
|
+
let submittedDoneFile: string | undefined;
|
|
12909
|
+
let journalEntry: StudioReplJournalEntry | null = null;
|
|
12910
|
+
try {
|
|
12911
|
+
if (selected.session.recordId && !selected.session.recordWarning) {
|
|
12912
|
+
lease = await acquireReplSessionSendLease(selected.session.recordId, {
|
|
12913
|
+
owner: `pi-studio:tool:${toolCallId}`,
|
|
12914
|
+
waitMs: timeoutMs,
|
|
12915
|
+
signal,
|
|
12916
|
+
});
|
|
12917
|
+
}
|
|
12918
|
+
const currentSession = inspectStudioReplSession(selected.session.sessionName);
|
|
12919
|
+
if (!currentSession || !isSameStudioReplSessionLifetime(selected.session, currentSession)) {
|
|
12920
|
+
throw new Error(`REPL session ${selected.session.sessionName} changed while Studio was waiting to send.`);
|
|
12921
|
+
}
|
|
12922
|
+
if (lease && currentSession.recordId !== selected.session.recordId) {
|
|
12923
|
+
throw new Error(`The shared record for ${selected.session.sessionName} changed while Studio was waiting to send.`);
|
|
12924
|
+
}
|
|
12925
|
+
const before = captureStudioReplSession(currentSession.sessionName);
|
|
12926
|
+
if (!before.ok) throw new Error(`Could not capture ${currentSession.sessionName} before sending: ${before.message}`);
|
|
12927
|
+
if (!isSameStudioReplSessionLifetime(currentSession, before.session)) {
|
|
12928
|
+
throw new Error(`REPL session ${currentSession.sessionName} changed before Studio could capture it.`);
|
|
12929
|
+
}
|
|
12930
|
+
const beforeTranscript = before.transcript;
|
|
12931
|
+
journalEntry = recordStudioReplJournalEntry({
|
|
12932
|
+
id: `pi-studio:tool:${toolCallId}`,
|
|
12933
|
+
requestId: `tool:${toolCallId}`,
|
|
12934
|
+
sessionName: currentSession.sessionName,
|
|
12935
|
+
runtime: currentSession.runtime,
|
|
12936
|
+
origin: "pi-studio",
|
|
12937
|
+
label: "Pi",
|
|
12938
|
+
mode: "agent",
|
|
12939
|
+
code: params.code,
|
|
12940
|
+
status: "sending",
|
|
12941
|
+
});
|
|
12942
|
+
const sent = sendTextToStudioReplSession(currentSession.sessionName, params.code, currentSession.target, currentSession.runtime);
|
|
12943
|
+
if (!sent.ok) {
|
|
12944
|
+
if (sent.submissionStarted) {
|
|
12945
|
+
submittedSession = currentSession;
|
|
12946
|
+
submittedDoneFile = sent.controlFiles?.doneFile;
|
|
12947
|
+
}
|
|
12948
|
+
journalEntry = recordStudioReplJournalEntry({
|
|
12949
|
+
...journalEntry,
|
|
12950
|
+
output: sent.message,
|
|
12951
|
+
status: "error",
|
|
12952
|
+
completedAt: Date.now(),
|
|
12953
|
+
});
|
|
12954
|
+
return {
|
|
12955
|
+
content: [{ type: "text", text: sent.message }],
|
|
12956
|
+
details: { ok: false, error: sent.message, session: selected.session, sessions: selected.sessions, recordEntryId: journalEntry.id } as Record<string, unknown>,
|
|
12957
|
+
};
|
|
12958
|
+
}
|
|
12959
|
+
submittedSession = currentSession;
|
|
12960
|
+
submittedDoneFile = sent.controlFiles?.doneFile;
|
|
12961
|
+
studioReplActiveSessionName = selected.session.sessionName;
|
|
12962
|
+
|
|
12963
|
+
let completed = false;
|
|
12964
|
+
if (sent.controlFiles?.doneFile) {
|
|
12965
|
+
completed = await waitForStudioReplDoneFile(sent.controlFiles.doneFile, timeoutMs, signal);
|
|
12966
|
+
if (signal?.aborted && !completed) {
|
|
12967
|
+
throw new Error("studio_repl_send was aborted after submission; the shared session remains busy until the running code settles.");
|
|
12968
|
+
}
|
|
12969
|
+
} else {
|
|
12970
|
+
await sleep(Math.min(750, timeoutMs));
|
|
12971
|
+
}
|
|
12972
|
+
const after = captureStudioReplSession(currentSession.sessionName);
|
|
12973
|
+
if (!after.ok) throw new Error(`Could not capture ${currentSession.sessionName} after sending: ${after.message}`);
|
|
12974
|
+
if (!isSameStudioReplSessionLifetime(currentSession, after.session)) {
|
|
12975
|
+
throw new Error(`REPL session ${currentSession.sessionName} changed before Studio captured the result.`);
|
|
12976
|
+
}
|
|
12977
|
+
const afterTranscript = after.transcript;
|
|
12978
|
+
const rawOutput = extractStudioReplTranscriptDelta(beforeTranscript, afterTranscript);
|
|
12979
|
+
const output = cleanStudioReplCapturedOutput(rawOutput);
|
|
12980
|
+
const status: StudioReplJournalEntry["status"] = sent.controlFiles?.doneFile
|
|
12981
|
+
? (completed ? "captured" : "timeout")
|
|
12982
|
+
: (output.trim() ? "captured" : "sent");
|
|
12983
|
+
journalEntry = recordStudioReplJournalEntry({
|
|
12984
|
+
...journalEntry,
|
|
12985
|
+
runtime: sent.runtime === "unknown" ? selected.session.runtime : sent.runtime,
|
|
12986
|
+
output,
|
|
12987
|
+
status,
|
|
12988
|
+
completedAt: Date.now(),
|
|
12989
|
+
});
|
|
12990
|
+
const statusLine = sent.controlFiles?.doneFile
|
|
12991
|
+
? (completed ? "Completed." : `Timed out after ${timeoutMs} ms waiting for completion marker.`)
|
|
12992
|
+
: "Submitted.";
|
|
12993
|
+
const text = [
|
|
12994
|
+
`${statusLine} ${sent.message}`,
|
|
12995
|
+
output ? "" : undefined,
|
|
12996
|
+
output || undefined,
|
|
12997
|
+
].filter(Boolean).join("\n");
|
|
12998
|
+
broadcastStudioReplToolSend({
|
|
12999
|
+
toolCallId,
|
|
13000
|
+
sessionName: selected.session.sessionName,
|
|
13001
|
+
runtime: sent.runtime === "unknown" ? selected.session.runtime : sent.runtime,
|
|
13002
|
+
code: params.code,
|
|
13003
|
+
label: "Pi",
|
|
13004
|
+
output,
|
|
13005
|
+
sharedSynced: journalEntry.sharedSynced === true,
|
|
12567
13006
|
completed,
|
|
12568
13007
|
timedOut: Boolean(sent.controlFiles?.doneFile && !completed),
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
13008
|
+
transcript: afterTranscript,
|
|
13009
|
+
capturedAt: Date.now(),
|
|
13010
|
+
journalEntries: getStudioReplJournalEntries(selected.session.sessionName),
|
|
13011
|
+
});
|
|
13012
|
+
return {
|
|
13013
|
+
content: [{ type: "text", text }],
|
|
13014
|
+
details: {
|
|
13015
|
+
ok: true,
|
|
13016
|
+
completed,
|
|
13017
|
+
timedOut: Boolean(sent.controlFiles?.doneFile && !completed),
|
|
13018
|
+
timeoutMs,
|
|
13019
|
+
session: selected.session,
|
|
13020
|
+
sessions: selected.sessions,
|
|
13021
|
+
runtime: sent.runtime,
|
|
13022
|
+
usedControlFile: sent.usedControlFile,
|
|
13023
|
+
submissionText: sent.submissionText,
|
|
13024
|
+
controlFiles: sent.controlFiles,
|
|
13025
|
+
output,
|
|
13026
|
+
recordEntryId: journalEntry.id,
|
|
13027
|
+
recordPath: selected.session.recordPath,
|
|
13028
|
+
} as Record<string, unknown>,
|
|
13029
|
+
};
|
|
13030
|
+
} catch (error) {
|
|
13031
|
+
if (journalEntry) {
|
|
13032
|
+
try {
|
|
13033
|
+
recordStudioReplJournalEntry({
|
|
13034
|
+
...journalEntry,
|
|
13035
|
+
output: error instanceof Error ? error.message : String(error),
|
|
13036
|
+
status: error instanceof Error && /timed out/i.test(error.message) ? "timeout" : "error",
|
|
13037
|
+
completedAt: Date.now(),
|
|
13038
|
+
});
|
|
13039
|
+
} catch {
|
|
13040
|
+
// Preserve the execution error when record maintenance also fails.
|
|
13041
|
+
}
|
|
13042
|
+
}
|
|
13043
|
+
throw error;
|
|
13044
|
+
} finally {
|
|
13045
|
+
if (lease) {
|
|
13046
|
+
await releaseOrRetainStudioReplSendLease(lease, submittedSession, submittedDoneFile);
|
|
13047
|
+
lease = null;
|
|
13048
|
+
}
|
|
13049
|
+
}
|
|
12579
13050
|
},
|
|
12580
13051
|
});
|
|
12581
13052
|
|
|
@@ -13692,6 +14163,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
13692
14163
|
}
|
|
13693
14164
|
: null;
|
|
13694
14165
|
|
|
14166
|
+
const getCurrentStudioSideQuestionThinkingLevels = (): StudioSideQuestionThinking[] =>
|
|
14167
|
+
getStudioSideQuestionThinkingLevels(latestModelRequestCtx?.model ?? lastCommandCtx?.model) as StudioSideQuestionThinking[];
|
|
14168
|
+
|
|
13695
14169
|
const getStudioThemeOptions = () => {
|
|
13696
14170
|
try {
|
|
13697
14171
|
const themes = lastCommandCtx?.ui?.getAllThemes?.() ?? [];
|
|
@@ -13727,6 +14201,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
13727
14201
|
modelLabel: currentModelLabel,
|
|
13728
14202
|
currentModel: getCurrentStudioModelDescriptor(),
|
|
13729
14203
|
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
14204
|
+
sideQuestionThinkingLevels: getCurrentStudioSideQuestionThinkingLevels(),
|
|
13730
14205
|
piModels: modelOptions,
|
|
13731
14206
|
suggestionModels: modelOptions,
|
|
13732
14207
|
currentTheme: getCurrentStudioThemeDescriptor(),
|
|
@@ -14033,6 +14508,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14033
14508
|
const payload: Record<string, unknown> = {
|
|
14034
14509
|
type: "side_question_state",
|
|
14035
14510
|
state: getStudioSideQuestionPublicState(),
|
|
14511
|
+
sideQuestionThinkingLevels: getCurrentStudioSideQuestionThinkingLevels(),
|
|
14036
14512
|
webSearchAvailable: Boolean(String(process.env.BRAVE_API_KEY || "").trim()),
|
|
14037
14513
|
};
|
|
14038
14514
|
if (client) payload.availablePiTools = getPublicStudioSideQuestionToolCatalog();
|
|
@@ -14254,6 +14730,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14254
14730
|
const model = latestModelRequestCtx?.model ?? ctx.model;
|
|
14255
14731
|
if (!model) throw new Error("No active Pi model is available for side questions.");
|
|
14256
14732
|
await resolveStudioModelRequestAuth({ model, modelRegistry: ctx.modelRegistry }, model);
|
|
14733
|
+
const thinking = resolveStudioSideQuestionThinking(model, context.thinking) as StudioSideQuestionThinking;
|
|
14257
14734
|
const gitSnapshot = context.gitContext
|
|
14258
14735
|
? await captureStudioSideQuestionGitSnapshot(contextRoot, { runGit: runStudioSideQuestionGitCommand }) as StudioSideQuestionGitSnapshot
|
|
14259
14736
|
: null;
|
|
@@ -14292,7 +14769,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14292
14769
|
customTools: tools,
|
|
14293
14770
|
sessionManager: sideSessionManager,
|
|
14294
14771
|
model,
|
|
14295
|
-
thinking
|
|
14772
|
+
thinking,
|
|
14296
14773
|
inheritedPrompt: stripStudioDynamicSystemPromptFooter(ctx.getSystemPrompt()),
|
|
14297
14774
|
});
|
|
14298
14775
|
session = agentRuntime.session;
|
|
@@ -14301,7 +14778,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14301
14778
|
cwd: workingDirectory,
|
|
14302
14779
|
sessionManager: sideSessionManager,
|
|
14303
14780
|
model,
|
|
14304
|
-
thinkingLevel:
|
|
14781
|
+
thinkingLevel: thinking,
|
|
14305
14782
|
tools: activeToolNames,
|
|
14306
14783
|
customTools: tools,
|
|
14307
14784
|
resourceLoader: createStudioSideQuestionResourceLoader(ctx),
|
|
@@ -14334,7 +14811,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14334
14811
|
tools: selectedPiTools,
|
|
14335
14812
|
},
|
|
14336
14813
|
modelLabel: formatStudioModelOptionLabel(model),
|
|
14337
|
-
thinking
|
|
14814
|
+
thinking,
|
|
14338
14815
|
messages: [],
|
|
14339
14816
|
activity: [],
|
|
14340
14817
|
error: context.webSearch && !webAvailable ? "Web search was requested but BRAVE_API_KEY is not configured; this thread will use local context only." : "",
|
|
@@ -14638,6 +15115,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14638
15115
|
modelLabel: currentModelLabel,
|
|
14639
15116
|
currentModel: getCurrentStudioModelDescriptor(),
|
|
14640
15117
|
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
15118
|
+
sideQuestionThinkingLevels: getCurrentStudioSideQuestionThinkingLevels(),
|
|
14641
15119
|
piModels: getStudioModelOptions(),
|
|
14642
15120
|
suggestionModels: getStudioModelOptions(),
|
|
14643
15121
|
currentTheme: getCurrentStudioThemeDescriptor(),
|
|
@@ -15431,33 +15909,65 @@ export default function (pi: ExtensionAPI) {
|
|
|
15431
15909
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
15432
15910
|
return;
|
|
15433
15911
|
}
|
|
15434
|
-
const before = captureStudioReplSession(msg.sessionName);
|
|
15435
|
-
const beforeTranscript = before.ok ? before.transcript : "";
|
|
15436
|
-
const sent = sendTextToStudioReplSession(msg.sessionName, msg.text);
|
|
15437
|
-
if (!sent.ok) {
|
|
15438
|
-
sendToClient(client, { type: "error", requestId: msg.requestId, message: sent.message });
|
|
15439
|
-
sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId, replError: sent.message });
|
|
15440
|
-
return;
|
|
15441
|
-
}
|
|
15442
|
-
studioReplActiveSessionName = msg.sessionName;
|
|
15443
|
-
recordStudioReplJournalEntry({
|
|
15444
|
-
requestId: msg.requestId,
|
|
15445
|
-
sessionName: msg.sessionName,
|
|
15446
|
-
runtime: sent.runtime,
|
|
15447
|
-
label: "Studio",
|
|
15448
|
-
mode: "raw",
|
|
15449
|
-
code: msg.text,
|
|
15450
|
-
status: "sent",
|
|
15451
|
-
});
|
|
15452
|
-
sendToClient(client, {
|
|
15453
|
-
type: "repl_send_ack",
|
|
15454
|
-
requestId: msg.requestId,
|
|
15455
|
-
sessionName: msg.sessionName,
|
|
15456
|
-
message: sent.message,
|
|
15457
|
-
journalEntries: getStudioReplJournalEntries(msg.sessionName),
|
|
15458
|
-
});
|
|
15459
15912
|
void (async () => {
|
|
15913
|
+
const session = inspectStudioReplSession(msg.sessionName);
|
|
15914
|
+
let journalEntry: StudioReplJournalEntry | null = null;
|
|
15915
|
+
let lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>> | null = null;
|
|
15916
|
+
let submittedSession: StudioReplSessionInfo | null = null;
|
|
15917
|
+
let submittedDoneFile: string | undefined;
|
|
15460
15918
|
try {
|
|
15919
|
+
if (!session) throw new Error(`No tmux REPL session named ${msg.sessionName}.`);
|
|
15920
|
+
if (session.recordId && !session.recordWarning) {
|
|
15921
|
+
lease = await acquireReplSessionSendLease(session.recordId, {
|
|
15922
|
+
owner: `pi-studio:browser:${msg.requestId}`,
|
|
15923
|
+
waitMs: STUDIO_REPL_SEND_DEFAULT_TIMEOUT_MS,
|
|
15924
|
+
});
|
|
15925
|
+
}
|
|
15926
|
+
const currentSession = inspectStudioReplSession(msg.sessionName);
|
|
15927
|
+
if (!currentSession || !isSameStudioReplSessionLifetime(session, currentSession)) {
|
|
15928
|
+
throw new Error(`REPL session ${msg.sessionName} changed while Studio was waiting to send.`);
|
|
15929
|
+
}
|
|
15930
|
+
if (lease && currentSession.recordId !== session.recordId) {
|
|
15931
|
+
throw new Error(`The shared record for ${msg.sessionName} changed while Studio was waiting to send.`);
|
|
15932
|
+
}
|
|
15933
|
+
const before = captureStudioReplSession(msg.sessionName);
|
|
15934
|
+
if (!before.ok) throw new Error(`Could not capture ${msg.sessionName} before sending: ${before.message}`);
|
|
15935
|
+
if (!isSameStudioReplSessionLifetime(currentSession, before.session)) {
|
|
15936
|
+
throw new Error(`REPL session ${msg.sessionName} changed before Studio could capture it.`);
|
|
15937
|
+
}
|
|
15938
|
+
const beforeTranscript = before.transcript;
|
|
15939
|
+
journalEntry = recordStudioReplJournalEntry({
|
|
15940
|
+
id: msg.journalEntryId,
|
|
15941
|
+
requestId: msg.requestId,
|
|
15942
|
+
createdAt: msg.createdAt,
|
|
15943
|
+
sessionName: msg.sessionName,
|
|
15944
|
+
runtime: session.runtime,
|
|
15945
|
+
origin: "pi-studio",
|
|
15946
|
+
label: msg.label || "Studio",
|
|
15947
|
+
mode: msg.mode || "raw",
|
|
15948
|
+
prose: msg.prose || "",
|
|
15949
|
+
code: msg.text,
|
|
15950
|
+
status: "sending",
|
|
15951
|
+
skippedChunks: msg.skippedChunks,
|
|
15952
|
+
});
|
|
15953
|
+
const sent = sendTextToStudioReplSession(msg.sessionName, msg.text, currentSession.target, currentSession.runtime);
|
|
15954
|
+
if (!sent.ok) {
|
|
15955
|
+
if (sent.submissionStarted) {
|
|
15956
|
+
submittedSession = currentSession;
|
|
15957
|
+
submittedDoneFile = sent.controlFiles?.doneFile;
|
|
15958
|
+
}
|
|
15959
|
+
throw new Error(sent.message);
|
|
15960
|
+
}
|
|
15961
|
+
submittedSession = currentSession;
|
|
15962
|
+
submittedDoneFile = sent.controlFiles?.doneFile;
|
|
15963
|
+
studioReplActiveSessionName = msg.sessionName;
|
|
15964
|
+
sendToClient(client, {
|
|
15965
|
+
type: "repl_send_ack",
|
|
15966
|
+
requestId: msg.requestId,
|
|
15967
|
+
sessionName: msg.sessionName,
|
|
15968
|
+
message: sent.message,
|
|
15969
|
+
journalEntries: getStudioReplJournalEntries(msg.sessionName),
|
|
15970
|
+
});
|
|
15461
15971
|
const timeoutMs = STUDIO_REPL_SEND_DEFAULT_TIMEOUT_MS;
|
|
15462
15972
|
let completed = false;
|
|
15463
15973
|
if (sent.controlFiles?.doneFile) {
|
|
@@ -15466,24 +15976,107 @@ export default function (pi: ExtensionAPI) {
|
|
|
15466
15976
|
await sleep(Math.min(750, timeoutMs));
|
|
15467
15977
|
}
|
|
15468
15978
|
const after = captureStudioReplSession(msg.sessionName);
|
|
15469
|
-
|
|
15979
|
+
if (!after.ok) throw new Error(`Could not capture ${msg.sessionName} after sending: ${after.message}`);
|
|
15980
|
+
if (!isSameStudioReplSessionLifetime(currentSession, after.session)) {
|
|
15981
|
+
throw new Error(`REPL session ${msg.sessionName} changed before Studio captured the result.`);
|
|
15982
|
+
}
|
|
15983
|
+
const afterTranscript = after.transcript;
|
|
15470
15984
|
const rawOutput = extractStudioReplTranscriptDelta(beforeTranscript, afterTranscript);
|
|
15471
15985
|
const output = cleanStudioReplCapturedOutput(rawOutput);
|
|
15472
15986
|
updateStudioReplJournalEntryOutput(
|
|
15473
15987
|
msg.requestId,
|
|
15474
15988
|
msg.sessionName,
|
|
15475
15989
|
output,
|
|
15476
|
-
sent.controlFiles?.doneFile
|
|
15990
|
+
sent.controlFiles?.doneFile ? (completed ? "captured" : "timeout") : (output.trim() ? "captured" : "sent"),
|
|
15477
15991
|
);
|
|
15992
|
+
if (lease) {
|
|
15993
|
+
await releaseOrRetainStudioReplSendLease(lease, submittedSession, submittedDoneFile);
|
|
15994
|
+
lease = null;
|
|
15995
|
+
}
|
|
15478
15996
|
sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId });
|
|
15479
15997
|
} catch (error) {
|
|
15480
|
-
|
|
15481
|
-
|
|
15998
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15999
|
+
if (journalEntry) updateStudioReplJournalEntryOutput(msg.requestId, msg.sessionName, message, "error");
|
|
16000
|
+
if (lease) {
|
|
16001
|
+
await releaseOrRetainStudioReplSendLease(lease, submittedSession, submittedDoneFile);
|
|
16002
|
+
lease = null;
|
|
16003
|
+
}
|
|
16004
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message });
|
|
16005
|
+
sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId, replError: message });
|
|
16006
|
+
} finally {
|
|
16007
|
+
await lease?.release().catch(() => undefined);
|
|
15482
16008
|
}
|
|
15483
16009
|
})();
|
|
15484
16010
|
return;
|
|
15485
16011
|
}
|
|
15486
16012
|
|
|
16013
|
+
if (msg.type === "repl_journal_upsert_request") {
|
|
16014
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
16015
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
16016
|
+
return;
|
|
16017
|
+
}
|
|
16018
|
+
try {
|
|
16019
|
+
if (!inspectStudioReplSession(msg.sessionName)) throw new Error(`No tmux REPL session named ${msg.sessionName}.`);
|
|
16020
|
+
recordStudioReplJournalEntry({
|
|
16021
|
+
...msg.entry,
|
|
16022
|
+
sessionName: msg.sessionName,
|
|
16023
|
+
origin: "pi-studio",
|
|
16024
|
+
});
|
|
16025
|
+
sendToClient(client, {
|
|
16026
|
+
type: "repl_journal_ack",
|
|
16027
|
+
requestId: msg.requestId,
|
|
16028
|
+
sessionName: msg.sessionName,
|
|
16029
|
+
journalEntries: getStudioReplJournalEntries(msg.sessionName),
|
|
16030
|
+
});
|
|
16031
|
+
} catch (error) {
|
|
16032
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: error instanceof Error ? error.message : String(error) });
|
|
16033
|
+
}
|
|
16034
|
+
return;
|
|
16035
|
+
}
|
|
16036
|
+
|
|
16037
|
+
if (msg.type === "repl_journal_import_request") {
|
|
16038
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
16039
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
16040
|
+
return;
|
|
16041
|
+
}
|
|
16042
|
+
try {
|
|
16043
|
+
if (!inspectStudioReplSession(msg.sessionName)) throw new Error(`No tmux REPL session named ${msg.sessionName}.`);
|
|
16044
|
+
for (const entry of msg.entries) {
|
|
16045
|
+
recordStudioReplJournalEntry({ ...entry, sessionName: msg.sessionName, origin: "pi-studio" });
|
|
16046
|
+
}
|
|
16047
|
+
sendToClient(client, {
|
|
16048
|
+
type: "repl_journal_ack",
|
|
16049
|
+
requestId: msg.requestId,
|
|
16050
|
+
sessionName: msg.sessionName,
|
|
16051
|
+
journalEntries: getStudioReplJournalEntries(msg.sessionName),
|
|
16052
|
+
});
|
|
16053
|
+
} catch (error) {
|
|
16054
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: error instanceof Error ? error.message : String(error) });
|
|
16055
|
+
}
|
|
16056
|
+
return;
|
|
16057
|
+
}
|
|
16058
|
+
|
|
16059
|
+
if (msg.type === "repl_journal_clear_request") {
|
|
16060
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
16061
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
16062
|
+
return;
|
|
16063
|
+
}
|
|
16064
|
+
try {
|
|
16065
|
+
if (!inspectStudioReplSession(msg.sessionName)) throw new Error(`No tmux REPL session named ${msg.sessionName}.`);
|
|
16066
|
+
clearStudioReplJournal(msg.sessionName);
|
|
16067
|
+
sendToClient(client, {
|
|
16068
|
+
type: "repl_journal_ack",
|
|
16069
|
+
requestId: msg.requestId,
|
|
16070
|
+
sessionName: msg.sessionName,
|
|
16071
|
+
journalEntries: [],
|
|
16072
|
+
cleared: true,
|
|
16073
|
+
});
|
|
16074
|
+
} catch (error) {
|
|
16075
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: error instanceof Error ? error.message : String(error) });
|
|
16076
|
+
}
|
|
16077
|
+
return;
|
|
16078
|
+
}
|
|
16079
|
+
|
|
15487
16080
|
if (msg.type === "repl_interrupt_request") {
|
|
15488
16081
|
if (!isValidRequestId(msg.requestId)) {
|
|
15489
16082
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|