pi-studio 0.9.55 → 0.9.57
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 +29 -0
- package/README.md +28 -2
- package/ROADMAP.md +31 -1
- package/client/studio-client.js +228 -50
- package/index.ts +914 -220
- package/package.json +1 -1
- package/shared/REPL_SESSION_RECORD_PROTOCOL.md +93 -0
- package/shared/repl-control-files.js +158 -0
- package/shared/repl-session-record.js +623 -0
- package/shared/repl-submission-display.js +227 -0
package/index.ts
CHANGED
|
@@ -42,6 +42,28 @@ 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";
|
|
58
|
+
import {
|
|
59
|
+
createReplSubmissionDisplay,
|
|
60
|
+
normalizeReplSubmissionEchoMode,
|
|
61
|
+
stripReplSubmissionDisplay,
|
|
62
|
+
} from "./shared/repl-submission-display.js";
|
|
63
|
+
import {
|
|
64
|
+
cleanupPrivateReplControlFiles,
|
|
65
|
+
createPrivateReplControlFiles,
|
|
66
|
+
} from "./shared/repl-control-files.js";
|
|
45
67
|
import { buildStudioForwardingHint, buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
|
|
46
68
|
import {
|
|
47
69
|
buildStudioPendingPage,
|
|
@@ -114,6 +136,8 @@ type TerminalActivityPhase = "idle" | "running" | "tool" | "responding";
|
|
|
114
136
|
type StudioPromptMode = "response" | "run" | "effective";
|
|
115
137
|
type StudioPromptTriggerKind = "run" | "steer";
|
|
116
138
|
type StudioReplRuntime = "shell" | "python" | "ipython" | "julia" | "r" | "ghci" | "clojure";
|
|
139
|
+
type ReplSubmissionEchoMode = "off" | "summary" | "full";
|
|
140
|
+
type ReplSubmissionDisplay = ReturnType<typeof createReplSubmissionDisplay>;
|
|
117
141
|
type StudioQuizAngle = "general" | "scientist" | "mathematician" | "statistician" | "developer" | "reviewer";
|
|
118
142
|
type StudioQuizScope = "selection" | "editor" | "file" | "folder" | "repo";
|
|
119
143
|
type StudioQuizThinking = "off" | "minimal" | "low" | "medium" | "high";
|
|
@@ -249,9 +273,14 @@ interface StudioContextUsageSnapshot {
|
|
|
249
273
|
interface StudioReplSessionInfo {
|
|
250
274
|
sessionName: string;
|
|
251
275
|
target: string;
|
|
276
|
+
tmuxSessionId: string;
|
|
277
|
+
tmuxSessionCreatedAt: number;
|
|
252
278
|
runtime: StudioReplRuntime | "unknown";
|
|
253
279
|
label: string;
|
|
254
280
|
source: "studio" | "pi-repl" | "tmux";
|
|
281
|
+
recordId?: string;
|
|
282
|
+
recordPath?: string;
|
|
283
|
+
recordWarning?: string;
|
|
255
284
|
}
|
|
256
285
|
|
|
257
286
|
interface StudioReplJournalEntry {
|
|
@@ -259,15 +288,22 @@ interface StudioReplJournalEntry {
|
|
|
259
288
|
requestId: string;
|
|
260
289
|
createdAt: number;
|
|
261
290
|
updatedAt: number;
|
|
291
|
+
completedAt: number | null;
|
|
262
292
|
sessionName: string;
|
|
263
293
|
runtime: StudioReplRuntime | "unknown";
|
|
294
|
+
origin: "pi-studio" | "pi-repl" | "unknown";
|
|
264
295
|
label: string;
|
|
265
296
|
mode: "raw" | "literate" | "agent";
|
|
266
297
|
prose: string;
|
|
267
298
|
code: string;
|
|
268
299
|
output: string;
|
|
269
|
-
status: "sent" | "captured" | "timeout" | "error" | "note";
|
|
300
|
+
status: "sending" | "sent" | "captured" | "timeout" | "error" | "note";
|
|
270
301
|
skippedChunks: number;
|
|
302
|
+
codeOmittedChars?: number;
|
|
303
|
+
proseOmittedChars?: number;
|
|
304
|
+
outputOmittedChars?: number;
|
|
305
|
+
/** Studio-local delivery state; not persisted in the shared protocol snapshot. */
|
|
306
|
+
sharedSynced?: boolean;
|
|
271
307
|
}
|
|
272
308
|
|
|
273
309
|
interface PreparedStudioPdfExport {
|
|
@@ -721,6 +757,33 @@ interface ReplSendRequestMessage {
|
|
|
721
757
|
requestId: string;
|
|
722
758
|
sessionName: string;
|
|
723
759
|
text: string;
|
|
760
|
+
echoMode?: ReplSubmissionEchoMode;
|
|
761
|
+
journalEntryId?: string;
|
|
762
|
+
createdAt?: number;
|
|
763
|
+
label?: string;
|
|
764
|
+
mode?: StudioReplJournalEntry["mode"];
|
|
765
|
+
prose?: string;
|
|
766
|
+
skippedChunks?: number;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
interface ReplJournalUpsertRequestMessage {
|
|
770
|
+
type: "repl_journal_upsert_request";
|
|
771
|
+
requestId: string;
|
|
772
|
+
sessionName: string;
|
|
773
|
+
entry: Partial<StudioReplJournalEntry>;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
interface ReplJournalImportRequestMessage {
|
|
777
|
+
type: "repl_journal_import_request";
|
|
778
|
+
requestId: string;
|
|
779
|
+
sessionName: string;
|
|
780
|
+
entries: Array<Partial<StudioReplJournalEntry>>;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
interface ReplJournalClearRequestMessage {
|
|
784
|
+
type: "repl_journal_clear_request";
|
|
785
|
+
requestId: string;
|
|
786
|
+
sessionName: string;
|
|
724
787
|
}
|
|
725
788
|
|
|
726
789
|
interface ReplInterruptRequestMessage {
|
|
@@ -848,6 +911,9 @@ type IncomingStudioMessage =
|
|
|
848
911
|
| ReplStartRequestMessage
|
|
849
912
|
| ReplStopRequestMessage
|
|
850
913
|
| ReplSendRequestMessage
|
|
914
|
+
| ReplJournalUpsertRequestMessage
|
|
915
|
+
| ReplJournalImportRequestMessage
|
|
916
|
+
| ReplJournalClearRequestMessage
|
|
851
917
|
| ReplInterruptRequestMessage
|
|
852
918
|
| CompactRequestMessage
|
|
853
919
|
| SaveAsRequestMessage
|
|
@@ -913,7 +979,7 @@ const STUDIO_REPL_SEND_MAX_CHARS = 200_000;
|
|
|
913
979
|
const STUDIO_REPL_SEND_DEFAULT_TIMEOUT_MS = 20_000;
|
|
914
980
|
const STUDIO_REPL_SEND_MAX_TIMEOUT_MS = 120_000;
|
|
915
981
|
const STUDIO_REPL_JOURNAL_MAX_ENTRIES = 300;
|
|
916
|
-
const
|
|
982
|
+
const STUDIO_REPL_RUNTIME_OPTION = "@pi_repl_runtime";
|
|
917
983
|
const STUDIO_SUBPROCESS_OUTPUT_MAX_BYTES = 2_000_000;
|
|
918
984
|
const STUDIO_PANDOC_TIMEOUT_MS = readStudioPositiveEnvMs("PI_STUDIO_PANDOC_TIMEOUT_MS", 120_000, 5_000, 15 * 60_000);
|
|
919
985
|
const STUDIO_LATEX_TIMEOUT_MS = readStudioPositiveEnvMs("PI_STUDIO_LATEX_TIMEOUT_MS", 120_000, 5_000, 15 * 60_000);
|
|
@@ -933,7 +999,12 @@ const STUDIO_REPL_SEND_TOOL_PARAMS = Type.Object({
|
|
|
933
999
|
sessionName: Type.Optional(Type.String({ description: "Exact Studio/pi-repl tmux session name. If omitted, Studio uses the active REPL session, or the first session matching target." })),
|
|
934
1000
|
target: Type.Optional(Type.String({ description: "Optional runtime target: shell, python, ipython, julia, r, ghci, or clojure. Used when sessionName is omitted." })),
|
|
935
1001
|
timeoutMs: Type.Optional(Type.Number({ description: "Maximum time to wait for completion when Studio can detect it (default 20000, max 120000).", minimum: 1000, maximum: STUDIO_REPL_SEND_MAX_TIMEOUT_MS })),
|
|
1002
|
+
echoMode: Type.Optional(Type.Union(
|
|
1003
|
+
[Type.Literal("off"), Type.Literal("summary"), Type.Literal("full")],
|
|
1004
|
+
{ description: "How much submitted code to echo visibly in the raw REPL pane. Defaults to PI_STUDIO_REPL_ECHO_MODE or off. Summary shows short submissions in full and truncates longer ones; Full has larger bounds and writes source code into persistent raw terminal history." },
|
|
1005
|
+
)),
|
|
936
1006
|
});
|
|
1007
|
+
const STUDIO_REPL_TOOL_ECHO_MODE = normalizeReplSubmissionEchoMode(process.env.PI_STUDIO_REPL_ECHO_MODE) as ReplSubmissionEchoMode;
|
|
937
1008
|
const STUDIO_REPL_STATUS_TOOL_PARAMS = Type.Object({
|
|
938
1009
|
sessionName: Type.Optional(Type.String({ description: "Exact Studio/pi-repl tmux session name to inspect." })),
|
|
939
1010
|
target: Type.Optional(Type.String({ description: "Optional runtime target: shell, python, ipython, julia, r, ghci, or clojure. If omitted, report all Studio-visible REPL sessions." })),
|
|
@@ -1206,6 +1277,7 @@ let studioPersistentStateCache: StudioPersistentState | null = null;
|
|
|
1206
1277
|
let studioPersistentStateQueue: Promise<void> = Promise.resolve();
|
|
1207
1278
|
let transientStudioDocuments: Map<string, { document: InitialStudioDocument; createdAt: number }> = new Map();
|
|
1208
1279
|
let studioReplJournalEntries: StudioReplJournalEntry[] = [];
|
|
1280
|
+
const studioReplUnsyncedJournalEntryIds = new Set<string>();
|
|
1209
1281
|
|
|
1210
1282
|
function createEmptyStudioPersistentState(): StudioPersistentState {
|
|
1211
1283
|
return {
|
|
@@ -9679,6 +9751,31 @@ function normalizeStudioQuizThinking(value: unknown): StudioQuizThinking {
|
|
|
9679
9751
|
return "minimal";
|
|
9680
9752
|
}
|
|
9681
9753
|
|
|
9754
|
+
function parseStudioReplJournalEntryInput(value: unknown): Partial<StudioReplJournalEntry> | null {
|
|
9755
|
+
if (!value || typeof value !== "object") return null;
|
|
9756
|
+
const entry = value as Record<string, unknown>;
|
|
9757
|
+
const code = typeof entry.code === "string" ? entry.code.slice(0, STUDIO_REPL_SEND_MAX_CHARS) : "";
|
|
9758
|
+
const prose = typeof entry.prose === "string" ? entry.prose.slice(0, 80_000) : "";
|
|
9759
|
+
const output = typeof entry.output === "string" ? entry.output.slice(0, 200_000) : "";
|
|
9760
|
+
if (!code.trim() && !prose.trim() && !output.trim()) return null;
|
|
9761
|
+
return {
|
|
9762
|
+
id: typeof entry.id === "string" ? entry.id.slice(0, 240) : undefined,
|
|
9763
|
+
requestId: typeof entry.requestId === "string" ? entry.requestId.slice(0, 300) : undefined,
|
|
9764
|
+
createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : undefined,
|
|
9765
|
+
updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : undefined,
|
|
9766
|
+
completedAt: typeof entry.completedAt === "number" && Number.isFinite(entry.completedAt) ? entry.completedAt : null,
|
|
9767
|
+
runtime: normalizeStudioReplRuntime(entry.runtime) || "unknown",
|
|
9768
|
+
origin: entry.origin === "pi-repl" ? "pi-repl" : "pi-studio",
|
|
9769
|
+
label: typeof entry.label === "string" ? entry.label.slice(0, 240) : undefined,
|
|
9770
|
+
mode: normalizeStudioReplJournalMode(entry.mode),
|
|
9771
|
+
prose,
|
|
9772
|
+
code,
|
|
9773
|
+
output,
|
|
9774
|
+
status: normalizeStudioReplJournalStatus(entry.status),
|
|
9775
|
+
skippedChunks: Math.max(0, Math.min(100_000, Math.floor(Number(entry.skippedChunks) || 0))),
|
|
9776
|
+
};
|
|
9777
|
+
}
|
|
9778
|
+
|
|
9682
9779
|
function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
9683
9780
|
let parsed: unknown;
|
|
9684
9781
|
try {
|
|
@@ -9983,9 +10080,30 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
9983
10080
|
requestId: msg.requestId,
|
|
9984
10081
|
sessionName: msg.sessionName,
|
|
9985
10082
|
text: msg.text,
|
|
10083
|
+
echoMode: normalizeReplSubmissionEchoMode(msg.echoMode) as ReplSubmissionEchoMode,
|
|
10084
|
+
journalEntryId: typeof msg.journalEntryId === "string" ? msg.journalEntryId.slice(0, 240) : undefined,
|
|
10085
|
+
createdAt: typeof msg.createdAt === "number" && Number.isFinite(msg.createdAt) ? msg.createdAt : undefined,
|
|
10086
|
+
label: typeof msg.label === "string" ? msg.label.slice(0, 240) : undefined,
|
|
10087
|
+
mode: normalizeStudioReplJournalMode(msg.mode),
|
|
10088
|
+
prose: typeof msg.prose === "string" ? msg.prose.slice(0, 80_000) : undefined,
|
|
10089
|
+
skippedChunks: Math.max(0, Math.min(100_000, Math.floor(Number(msg.skippedChunks) || 0))),
|
|
9986
10090
|
};
|
|
9987
10091
|
}
|
|
9988
10092
|
|
|
10093
|
+
if (msg.type === "repl_journal_upsert_request" && typeof msg.requestId === "string" && typeof msg.sessionName === "string") {
|
|
10094
|
+
const entry = parseStudioReplJournalEntryInput(msg.entry);
|
|
10095
|
+
if (entry) return { type: "repl_journal_upsert_request", requestId: msg.requestId, sessionName: msg.sessionName, entry };
|
|
10096
|
+
}
|
|
10097
|
+
|
|
10098
|
+
if (msg.type === "repl_journal_import_request" && typeof msg.requestId === "string" && typeof msg.sessionName === "string" && Array.isArray(msg.entries)) {
|
|
10099
|
+
const entries = msg.entries.slice(0, 80).map(parseStudioReplJournalEntryInput).filter((entry): entry is Partial<StudioReplJournalEntry> => Boolean(entry));
|
|
10100
|
+
return { type: "repl_journal_import_request", requestId: msg.requestId, sessionName: msg.sessionName, entries };
|
|
10101
|
+
}
|
|
10102
|
+
|
|
10103
|
+
if (msg.type === "repl_journal_clear_request" && typeof msg.requestId === "string" && typeof msg.sessionName === "string") {
|
|
10104
|
+
return { type: "repl_journal_clear_request", requestId: msg.requestId, sessionName: msg.sessionName };
|
|
10105
|
+
}
|
|
10106
|
+
|
|
9989
10107
|
if (msg.type === "repl_interrupt_request" && typeof msg.requestId === "string" && typeof msg.sessionName === "string") {
|
|
9990
10108
|
return {
|
|
9991
10109
|
type: "repl_interrupt_request",
|
|
@@ -10877,9 +10995,111 @@ function runStudioTmux(args: string[], options?: { cwd?: string; input?: string;
|
|
|
10877
10995
|
return { ok: true, stdout, stderr };
|
|
10878
10996
|
}
|
|
10879
10997
|
|
|
10998
|
+
function readStudioReplTmuxOption(sessionName: string, optionName: string): string | undefined {
|
|
10999
|
+
const result = runStudioTmux(["show-options", "-v", "-t", sessionName, optionName], { timeout: 3_000 });
|
|
11000
|
+
if (!result.ok) return undefined;
|
|
11001
|
+
const value = result.stdout.trim();
|
|
11002
|
+
return value || undefined;
|
|
11003
|
+
}
|
|
11004
|
+
|
|
11005
|
+
function setStudioReplTmuxOptionIfAbsent(sessionName: string, optionName: string, value: string): boolean {
|
|
11006
|
+
return runStudioTmux(["set-option", "-qo", "-t", sessionName, optionName, value], { timeout: 3_000 }).ok;
|
|
11007
|
+
}
|
|
11008
|
+
|
|
11009
|
+
function attachStudioReplSessionRecord(
|
|
11010
|
+
session: StudioReplSessionInfo,
|
|
11011
|
+
recordIdHint?: string,
|
|
11012
|
+
versionHint?: string,
|
|
11013
|
+
): StudioReplSessionInfo {
|
|
11014
|
+
let recordId = recordIdHint || readStudioReplTmuxOption(session.sessionName, REPL_SESSION_RECORD_ID_OPTION);
|
|
11015
|
+
let version = versionHint || readStudioReplTmuxOption(session.sessionName, REPL_SESSION_RECORD_VERSION_OPTION);
|
|
11016
|
+
if (recordId && !isValidReplSessionRecordId(recordId)) {
|
|
11017
|
+
return { ...session, recordWarning: "Invalid shared REPL record metadata; Studio left it untouched." };
|
|
11018
|
+
}
|
|
11019
|
+
if (!recordId) {
|
|
11020
|
+
const candidate = createReplSessionRecordId();
|
|
11021
|
+
if (!setStudioReplTmuxOptionIfAbsent(session.sessionName, REPL_SESSION_RECORD_ID_OPTION, candidate)) {
|
|
11022
|
+
return { ...session, recordWarning: "Studio could not attach shared record metadata to this tmux session." };
|
|
11023
|
+
}
|
|
11024
|
+
recordId = readStudioReplTmuxOption(session.sessionName, REPL_SESSION_RECORD_ID_OPTION);
|
|
11025
|
+
}
|
|
11026
|
+
if (!recordId || !isValidReplSessionRecordId(recordId)) {
|
|
11027
|
+
return { ...session, recordWarning: "Studio could not read valid shared record metadata from this tmux session." };
|
|
11028
|
+
}
|
|
11029
|
+
if (!version) {
|
|
11030
|
+
setStudioReplTmuxOptionIfAbsent(session.sessionName, REPL_SESSION_RECORD_VERSION_OPTION, String(REPL_SESSION_RECORD_VERSION));
|
|
11031
|
+
version = readStudioReplTmuxOption(session.sessionName, REPL_SESSION_RECORD_VERSION_OPTION);
|
|
11032
|
+
}
|
|
11033
|
+
const recordPath = getReplSessionRecordPath(recordId);
|
|
11034
|
+
if (version !== String(REPL_SESSION_RECORD_VERSION)) {
|
|
11035
|
+
return {
|
|
11036
|
+
...session,
|
|
11037
|
+
recordId,
|
|
11038
|
+
recordPath,
|
|
11039
|
+
recordWarning: `Shared REPL record version ${version || "unknown"} is not supported by this Studio version.`,
|
|
11040
|
+
};
|
|
11041
|
+
}
|
|
11042
|
+
try {
|
|
11043
|
+
ensureReplSessionRecord(recordId, {
|
|
11044
|
+
sessionName: session.sessionName,
|
|
11045
|
+
tmuxSessionId: session.tmuxSessionId,
|
|
11046
|
+
tmuxSessionCreatedAt: session.tmuxSessionCreatedAt,
|
|
11047
|
+
runtime: session.runtime,
|
|
11048
|
+
});
|
|
11049
|
+
return { ...session, recordId, recordPath };
|
|
11050
|
+
} catch (error) {
|
|
11051
|
+
return {
|
|
11052
|
+
...session,
|
|
11053
|
+
recordId,
|
|
11054
|
+
recordPath,
|
|
11055
|
+
recordWarning: error instanceof Error ? error.message : String(error),
|
|
11056
|
+
};
|
|
11057
|
+
}
|
|
11058
|
+
}
|
|
11059
|
+
|
|
11060
|
+
function makeStudioReplSessionInfo(
|
|
11061
|
+
sessionName: string,
|
|
11062
|
+
tmuxSessionId = "",
|
|
11063
|
+
tmuxSessionCreatedRaw = "0",
|
|
11064
|
+
runtimeMetadata = "",
|
|
11065
|
+
recordIdHint = "",
|
|
11066
|
+
versionHint = "",
|
|
11067
|
+
): StudioReplSessionInfo {
|
|
11068
|
+
const inferred = inferStudioReplSessionRuntime(sessionName);
|
|
11069
|
+
const metadataRuntime = normalizeStudioReplRuntime(runtimeMetadata);
|
|
11070
|
+
const runtime = metadataRuntime || inferred.runtime;
|
|
11071
|
+
return attachStudioReplSessionRecord({
|
|
11072
|
+
sessionName,
|
|
11073
|
+
target: getStudioReplPaneTarget(tmuxSessionId || sessionName),
|
|
11074
|
+
tmuxSessionId,
|
|
11075
|
+
tmuxSessionCreatedAt: Math.max(0, Math.floor(Number(tmuxSessionCreatedRaw) || 0)),
|
|
11076
|
+
runtime,
|
|
11077
|
+
label: formatStudioReplSessionLabel(sessionName, runtime, inferred.source),
|
|
11078
|
+
source: inferred.source,
|
|
11079
|
+
}, recordIdHint, versionHint);
|
|
11080
|
+
}
|
|
11081
|
+
|
|
11082
|
+
function inspectStudioReplSession(sessionName: string): StudioReplSessionInfo | null {
|
|
11083
|
+
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return null;
|
|
11084
|
+
const result = runStudioTmux([
|
|
11085
|
+
"display-message",
|
|
11086
|
+
"-p",
|
|
11087
|
+
"-t",
|
|
11088
|
+
getStudioReplPaneTarget(sessionName),
|
|
11089
|
+
"#{session_name}\t#{session_id}\t#{session_created}\t#{@pi_repl_runtime}\t#{@pi_repl_record_id}\t#{@pi_repl_record_version}",
|
|
11090
|
+
], { timeout: 3_000 });
|
|
11091
|
+
if (!result.ok) return null;
|
|
11092
|
+
const [resolvedName, tmuxSessionId, createdAt, runtime, recordId, version] = result.stdout.trim().split("\t");
|
|
11093
|
+
return makeStudioReplSessionInfo(resolvedName || sessionName, tmuxSessionId, createdAt, runtime, recordId, version);
|
|
11094
|
+
}
|
|
11095
|
+
|
|
10880
11096
|
function listStudioReplSessions(): { tmuxAvailable: boolean; sessions: StudioReplSessionInfo[]; error?: string } {
|
|
10881
11097
|
if (!isTmuxAvailable()) return { tmuxAvailable: false, sessions: [], error: "tmux is not available." };
|
|
10882
|
-
const result = runStudioTmux([
|
|
11098
|
+
const result = runStudioTmux([
|
|
11099
|
+
"list-sessions",
|
|
11100
|
+
"-F",
|
|
11101
|
+
"#{session_name}\t#{session_id}\t#{session_created}\t#{@pi_repl_runtime}\t#{@pi_repl_record_id}\t#{@pi_repl_record_version}",
|
|
11102
|
+
], { timeout: 3_000 });
|
|
10883
11103
|
if (!result.ok) {
|
|
10884
11104
|
const message = result.message.toLowerCase().includes("no server running") ? "No tmux sessions are running." : result.message;
|
|
10885
11105
|
return { tmuxAvailable: true, sessions: [], error: message };
|
|
@@ -10888,30 +11108,18 @@ function listStudioReplSessions(): { tmuxAvailable: boolean; sessions: StudioRep
|
|
|
10888
11108
|
.split(/\r?\n/)
|
|
10889
11109
|
.map((line) => line.trim())
|
|
10890
11110
|
.filter(Boolean)
|
|
10891
|
-
.
|
|
10892
|
-
.
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
target: getStudioReplPaneTarget(sessionName),
|
|
10897
|
-
runtime: inferred.runtime,
|
|
10898
|
-
label: formatStudioReplSessionLabel(sessionName, inferred.runtime, inferred.source),
|
|
10899
|
-
source: inferred.source,
|
|
10900
|
-
};
|
|
10901
|
-
});
|
|
11111
|
+
.map((line) => line.split("\t"))
|
|
11112
|
+
.filter(([sessionName]) => Boolean(sessionName && shouldShowStudioReplTmuxSession(sessionName)))
|
|
11113
|
+
.map(([sessionName = "", tmuxSessionId = "", createdAt = "0", runtime = "", recordId = "", version = ""]) => (
|
|
11114
|
+
makeStudioReplSessionInfo(sessionName, tmuxSessionId, createdAt, runtime, recordId, version)
|
|
11115
|
+
));
|
|
10902
11116
|
return { tmuxAvailable: true, sessions };
|
|
10903
11117
|
}
|
|
10904
11118
|
|
|
10905
11119
|
function captureStudioReplSession(sessionName: string): { ok: true; transcript: string; session: StudioReplSessionInfo } | { ok: false; message: string } {
|
|
10906
11120
|
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
|
|
10907
|
-
const
|
|
10908
|
-
|
|
10909
|
-
sessionName,
|
|
10910
|
-
target: getStudioReplPaneTarget(sessionName),
|
|
10911
|
-
runtime: inferred.runtime,
|
|
10912
|
-
label: formatStudioReplSessionLabel(sessionName, inferred.runtime, inferred.source),
|
|
10913
|
-
source: inferred.source,
|
|
10914
|
-
};
|
|
11121
|
+
const session = inspectStudioReplSession(sessionName);
|
|
11122
|
+
if (!session) return { ok: false, message: `No tmux REPL session named ${sessionName}.` };
|
|
10915
11123
|
const result = runStudioTmux(["capture-pane", "-J", "-p", "-t", session.target, "-S", `-${STUDIO_REPL_CAPTURE_LINES}`], { timeout: 3_000 });
|
|
10916
11124
|
if (!result.ok) return { ok: false, message: result.message };
|
|
10917
11125
|
return { ok: true, transcript: String(result.stdout || "").replace(/[\t ]+$/gm, "").trimEnd(), session };
|
|
@@ -10923,31 +11131,26 @@ function startStudioReplSession(runtime: StudioReplRuntime, cwd: string, options
|
|
|
10923
11131
|
const sessionName = options?.newSession ? getNewStudioReplSessionName(runtime, commandOverride) : getStudioReplSessionName(runtime, commandOverride);
|
|
10924
11132
|
const existing = runStudioTmux(["has-session", "-t", sessionName], { timeout: 3_000 });
|
|
10925
11133
|
if (existing.ok) {
|
|
10926
|
-
const
|
|
11134
|
+
const session = inspectStudioReplSession(sessionName);
|
|
11135
|
+
if (!session) return { ok: false, message: `Could not inspect existing REPL session ${sessionName}.` };
|
|
10927
11136
|
return {
|
|
10928
11137
|
ok: true,
|
|
10929
|
-
session
|
|
10930
|
-
sessionName,
|
|
10931
|
-
target: getStudioReplPaneTarget(sessionName),
|
|
10932
|
-
runtime: inferred.runtime,
|
|
10933
|
-
label: formatStudioReplSessionLabel(sessionName, inferred.runtime, inferred.source),
|
|
10934
|
-
source: inferred.source,
|
|
10935
|
-
},
|
|
11138
|
+
session,
|
|
10936
11139
|
message: `${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL is already running.`,
|
|
10937
11140
|
};
|
|
10938
11141
|
}
|
|
10939
11142
|
const command = getStudioReplRuntimeCommand(runtime, commandOverride);
|
|
10940
11143
|
const result = runStudioTmux(buildStudioReplTmuxStartArgs(sessionName, cwd || process.cwd(), command), { timeout: 5_000 });
|
|
10941
11144
|
if (!result.ok) return { ok: false, message: result.message || `Failed to start ${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL.` };
|
|
11145
|
+
runStudioTmux(["set-option", "-q", "-t", sessionName, STUDIO_REPL_RUNTIME_OPTION, runtime], { timeout: 3_000 });
|
|
11146
|
+
const session = inspectStudioReplSession(sessionName);
|
|
11147
|
+
if (!session) {
|
|
11148
|
+
runStudioTmux(["kill-session", "-t", sessionName], { timeout: 3_000 });
|
|
11149
|
+
return { ok: false, message: `Started ${sessionName}, but could not initialize its shared session metadata.` };
|
|
11150
|
+
}
|
|
10942
11151
|
return {
|
|
10943
11152
|
ok: true,
|
|
10944
|
-
session
|
|
10945
|
-
sessionName,
|
|
10946
|
-
target: getStudioReplPaneTarget(sessionName),
|
|
10947
|
-
runtime,
|
|
10948
|
-
label: formatStudioReplSessionLabel(sessionName, runtime, "studio"),
|
|
10949
|
-
source: "studio",
|
|
10950
|
-
},
|
|
11153
|
+
session,
|
|
10951
11154
|
message: `Started ${options?.newSession ? "new " : ""}${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL${commandOverride ? ` with custom command: ${commandOverride}` : ""}.`,
|
|
10952
11155
|
};
|
|
10953
11156
|
}
|
|
@@ -10973,7 +11176,9 @@ type StudioReplPreparedSubmission = {
|
|
|
10973
11176
|
runtime: StudioReplRuntime | "unknown";
|
|
10974
11177
|
usedControlFile: boolean;
|
|
10975
11178
|
submissionText: string;
|
|
11179
|
+
completionLine?: string;
|
|
10976
11180
|
controlFiles?: StudioReplControlFiles;
|
|
11181
|
+
display?: ReplSubmissionDisplay;
|
|
10977
11182
|
};
|
|
10978
11183
|
|
|
10979
11184
|
type StudioReplSendSuccess = {
|
|
@@ -10982,10 +11187,22 @@ type StudioReplSendSuccess = {
|
|
|
10982
11187
|
runtime: StudioReplRuntime | "unknown";
|
|
10983
11188
|
usedControlFile: boolean;
|
|
10984
11189
|
submissionText: string;
|
|
11190
|
+
completionLine?: string;
|
|
10985
11191
|
controlFiles?: StudioReplControlFiles;
|
|
11192
|
+
display?: ReplSubmissionDisplay;
|
|
10986
11193
|
};
|
|
10987
11194
|
|
|
10988
|
-
type StudioReplSendFailure = {
|
|
11195
|
+
type StudioReplSendFailure = {
|
|
11196
|
+
ok: false;
|
|
11197
|
+
message: string;
|
|
11198
|
+
submissionStarted?: boolean;
|
|
11199
|
+
runtime?: StudioReplRuntime | "unknown";
|
|
11200
|
+
usedControlFile?: boolean;
|
|
11201
|
+
submissionText?: string;
|
|
11202
|
+
completionLine?: string;
|
|
11203
|
+
controlFiles?: StudioReplControlFiles;
|
|
11204
|
+
display?: ReplSubmissionDisplay;
|
|
11205
|
+
};
|
|
10989
11206
|
|
|
10990
11207
|
function sleep(ms: number): Promise<void> {
|
|
10991
11208
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
@@ -11000,31 +11217,44 @@ function shellQuote(value: string): string {
|
|
|
11000
11217
|
return `'${String(value || "").replace(/'/g, `'"'"'`)}'`;
|
|
11001
11218
|
}
|
|
11002
11219
|
|
|
11003
|
-
function
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11018
|
-
|
|
11019
|
-
|
|
11020
|
-
|
|
11220
|
+
function getStudioReplControlExtension(runtime: StudioReplRuntime): string {
|
|
11221
|
+
if (runtime === "julia") return "jl";
|
|
11222
|
+
if (runtime === "r") return "R";
|
|
11223
|
+
if (runtime === "ghci") return "ghci";
|
|
11224
|
+
if (runtime === "clojure") return "clj";
|
|
11225
|
+
if (runtime === "shell") return "sh";
|
|
11226
|
+
return "py";
|
|
11227
|
+
}
|
|
11228
|
+
|
|
11229
|
+
function buildStudioPythonDisplayStatements(display: ReplSubmissionDisplay, indent = ""): string[] {
|
|
11230
|
+
if (!display.enabled) return [];
|
|
11231
|
+
return display.prefixLines.map((line) => `${indent}__pi_studio_builtins.print(${JSON.stringify(line)})`);
|
|
11232
|
+
}
|
|
11233
|
+
|
|
11234
|
+
function buildStudioJuliaDisplayStatements(display: ReplSubmissionDisplay, indent = ""): string[] {
|
|
11235
|
+
if (!display.enabled) return [];
|
|
11236
|
+
return display.prefixLines.map((line) => `${indent}Base.println(${JSON.stringify(line)})`);
|
|
11237
|
+
}
|
|
11238
|
+
|
|
11239
|
+
function buildStudioRDisplayStatements(display: ReplSubmissionDisplay, indent = ""): string[] {
|
|
11240
|
+
if (!display.enabled) return [];
|
|
11241
|
+
return display.prefixLines.map((line) => `${indent}base::cat(${JSON.stringify(`${line}\n`)})`);
|
|
11021
11242
|
}
|
|
11022
11243
|
|
|
11023
|
-
function
|
|
11244
|
+
function buildStudioClojureDisplayStatements(display: ReplSubmissionDisplay, indent = ""): string[] {
|
|
11245
|
+
if (!display.enabled) return [];
|
|
11246
|
+
return display.prefixLines.map((line) => `${indent}(clojure.core/println ${JSON.stringify(line)})`);
|
|
11247
|
+
}
|
|
11248
|
+
|
|
11249
|
+
function buildStudioPythonControlSource(runtime: "python" | "ipython", code: string, doneFile: string, display: ReplSubmissionDisplay): string {
|
|
11250
|
+
const prefix = buildStudioPythonDisplayStatements(display);
|
|
11251
|
+
const completion = display.enabled ? [` __pi_studio_builtins.print(${JSON.stringify(display.endMarker)})`] : [];
|
|
11024
11252
|
if (runtime === "ipython") {
|
|
11025
11253
|
return [
|
|
11026
11254
|
"from pathlib import Path as __pi_studio_path",
|
|
11255
|
+
"import builtins as __pi_studio_builtins",
|
|
11027
11256
|
"import traceback as __pi_studio_traceback",
|
|
11257
|
+
...prefix,
|
|
11028
11258
|
"try:",
|
|
11029
11259
|
" __pi_studio_ip = get_ipython()",
|
|
11030
11260
|
" if __pi_studio_ip is None:",
|
|
@@ -11035,14 +11265,17 @@ function buildStudioPythonControlSource(runtime: "python" | "ipython", code: str
|
|
|
11035
11265
|
"except Exception:",
|
|
11036
11266
|
" __pi_studio_traceback.print_exc()",
|
|
11037
11267
|
"finally:",
|
|
11268
|
+
...completion,
|
|
11038
11269
|
` __pi_studio_path(${JSON.stringify(doneFile)}).write_text('done\\n', encoding='utf-8')`,
|
|
11039
11270
|
].join("\n");
|
|
11040
11271
|
}
|
|
11041
11272
|
|
|
11042
11273
|
return [
|
|
11043
11274
|
"from pathlib import Path as __pi_studio_path",
|
|
11275
|
+
"import builtins as __pi_studio_builtins",
|
|
11044
11276
|
"import traceback as __pi_studio_traceback",
|
|
11045
11277
|
`__pi_studio_code = ${JSON.stringify(code)}`,
|
|
11278
|
+
...prefix,
|
|
11046
11279
|
"try:",
|
|
11047
11280
|
" try:",
|
|
11048
11281
|
" __pi_studio_expr = compile(__pi_studio_code, '<pi-studio-repl>', 'eval')",
|
|
@@ -11055,12 +11288,15 @@ function buildStudioPythonControlSource(runtime: "python" | "ipython", code: str
|
|
|
11055
11288
|
"except Exception:",
|
|
11056
11289
|
" __pi_studio_traceback.print_exc()",
|
|
11057
11290
|
"finally:",
|
|
11291
|
+
...completion,
|
|
11058
11292
|
` __pi_studio_path(${JSON.stringify(doneFile)}).write_text('done\\n', encoding='utf-8')`,
|
|
11059
11293
|
].join("\n");
|
|
11060
11294
|
}
|
|
11061
11295
|
|
|
11062
|
-
function buildStudioJuliaControlSource(code: string, doneFile: string): string {
|
|
11296
|
+
function buildStudioJuliaControlSource(code: string, doneFile: string, display: ReplSubmissionDisplay): string {
|
|
11297
|
+
const completion = display.enabled ? [` Base.println(${JSON.stringify(display.endMarker)})`] : [];
|
|
11063
11298
|
return [
|
|
11299
|
+
...buildStudioJuliaDisplayStatements(display),
|
|
11064
11300
|
"try",
|
|
11065
11301
|
` local __pi_studio_result = Base.include_string(Main, ${JSON.stringify(code)}, "pi-studio-repl")`,
|
|
11066
11302
|
" if !isnothing(__pi_studio_result)",
|
|
@@ -11069,14 +11305,17 @@ function buildStudioJuliaControlSource(code: string, doneFile: string): string {
|
|
|
11069
11305
|
"catch e",
|
|
11070
11306
|
" Base.display_error(stderr, e, catch_backtrace())",
|
|
11071
11307
|
"finally",
|
|
11308
|
+
...completion,
|
|
11072
11309
|
` write(${JSON.stringify(doneFile)}, "done\\n")`,
|
|
11073
11310
|
"end",
|
|
11074
11311
|
].join("\n");
|
|
11075
11312
|
}
|
|
11076
11313
|
|
|
11077
|
-
function buildStudioRControlSource(code: string, doneFile: string): string {
|
|
11314
|
+
function buildStudioRControlSource(code: string, doneFile: string, display: ReplSubmissionDisplay): string {
|
|
11315
|
+
const completion = display.enabled ? [` base::cat(${JSON.stringify(`${display.endMarker}\n`)})`] : [];
|
|
11078
11316
|
return [
|
|
11079
11317
|
"local({",
|
|
11318
|
+
...buildStudioRDisplayStatements(display, " "),
|
|
11080
11319
|
` .__pi_studio_done_file <- ${JSON.stringify(doneFile)}`,
|
|
11081
11320
|
` .__pi_studio_code <- ${JSON.stringify(code)}`,
|
|
11082
11321
|
" tryCatch({",
|
|
@@ -11098,15 +11337,33 @@ function buildStudioRControlSource(code: string, doneFile: string): string {
|
|
|
11098
11337
|
" message(\"Error in \", .__pi_studio_call_text, \": \", conditionMessage(e))",
|
|
11099
11338
|
" }",
|
|
11100
11339
|
" }, finally = {",
|
|
11340
|
+
...completion,
|
|
11101
11341
|
" writeLines(\"done\", .__pi_studio_done_file)",
|
|
11102
11342
|
" })",
|
|
11103
11343
|
"})",
|
|
11104
11344
|
].join("\n");
|
|
11105
11345
|
}
|
|
11106
11346
|
|
|
11107
|
-
function
|
|
11347
|
+
function buildStudioGhciControlSource(code: string, display: ReplSubmissionDisplay): string {
|
|
11348
|
+
const prefix = display.enabled
|
|
11349
|
+
? display.prefixLines.map((line) => `:! command printf '%s\\n' ${shellQuote(line)}`)
|
|
11350
|
+
: [];
|
|
11351
|
+
return [
|
|
11352
|
+
...prefix,
|
|
11353
|
+
code.replace(/\r/g, "").trimEnd(),
|
|
11354
|
+
].filter(Boolean).join("\n") + "\n";
|
|
11355
|
+
}
|
|
11356
|
+
|
|
11357
|
+
function buildStudioGhciCompletionLine(doneFile: string, display: ReplSubmissionDisplay): string {
|
|
11358
|
+
const completion = display.enabled ? `command printf '%s\\n' ${shellQuote(display.endMarker)}; ` : "";
|
|
11359
|
+
return `:! ${completion}touch ${shellQuote(doneFile)}`;
|
|
11360
|
+
}
|
|
11361
|
+
|
|
11362
|
+
function buildStudioClojureControlSource(code: string, doneFile: string, display: ReplSubmissionDisplay): string {
|
|
11363
|
+
const completion = display.enabled ? [` (clojure.core/println ${JSON.stringify(display.endMarker)})`] : [];
|
|
11108
11364
|
return [
|
|
11109
11365
|
"(let [code " + JSON.stringify(code) + "]",
|
|
11366
|
+
...buildStudioClojureDisplayStatements(display, " "),
|
|
11110
11367
|
" (try",
|
|
11111
11368
|
" (let [rdr (clojure.lang.LineNumberingPushbackReader. (java.io.StringReader. code))]",
|
|
11112
11369
|
" (loop [last-val nil has-val false]",
|
|
@@ -11117,50 +11374,75 @@ function buildStudioClojureControlSource(code: string, doneFile: string): string
|
|
|
11117
11374
|
" (catch Throwable t",
|
|
11118
11375
|
" (#'clojure.main/repl-caught t))",
|
|
11119
11376
|
" (finally",
|
|
11377
|
+
...completion,
|
|
11120
11378
|
` (spit ${JSON.stringify(doneFile)} "done\\n"))))`,
|
|
11121
11379
|
].join("\n");
|
|
11122
11380
|
}
|
|
11123
11381
|
|
|
11124
|
-
function
|
|
11125
|
-
if (
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
|
|
11382
|
+
function buildStudioShellControlSource(code: string, display: ReplSubmissionDisplay): string {
|
|
11383
|
+
if (!display.enabled) return `${code.replace(/\r/g, "").trimEnd()}\n`;
|
|
11384
|
+
return [
|
|
11385
|
+
...display.prefixLines.map((line) => `command printf '%s\\n' ${shellQuote(line)}`),
|
|
11386
|
+
code.replace(/\r/g, "").trimEnd(),
|
|
11387
|
+
"__pi_studio_repl_status=$?",
|
|
11388
|
+
`command printf '%s\\n' ${shellQuote(display.endMarker)}`,
|
|
11389
|
+
"return \"$__pi_studio_repl_status\"",
|
|
11390
|
+
].filter(Boolean).join("\n") + "\n";
|
|
11391
|
+
}
|
|
11392
|
+
|
|
11393
|
+
function buildStudioReplControlSource(runtime: StudioReplRuntime, code: string, doneFile: string, display: ReplSubmissionDisplay): string | null {
|
|
11394
|
+
if (runtime === "python" || runtime === "ipython") return buildStudioPythonControlSource(runtime, code, doneFile, display);
|
|
11395
|
+
if (runtime === "julia") return buildStudioJuliaControlSource(code, doneFile, display);
|
|
11396
|
+
if (runtime === "r") return buildStudioRControlSource(code, doneFile, display);
|
|
11397
|
+
if (runtime === "ghci") return buildStudioGhciControlSource(code, display);
|
|
11398
|
+
if (runtime === "clojure") return buildStudioClojureControlSource(code, doneFile, display);
|
|
11399
|
+
if (runtime === "shell") return buildStudioShellControlSource(code, display);
|
|
11130
11400
|
return null;
|
|
11131
11401
|
}
|
|
11132
11402
|
|
|
11133
|
-
function buildStudioReplSubmissionLine(runtime: StudioReplRuntime, sourceFile: string): string {
|
|
11403
|
+
function buildStudioReplSubmissionLine(runtime: StudioReplRuntime, sourceFile: string, doneFile: string): string {
|
|
11134
11404
|
const quotedPath = JSON.stringify(sourceFile);
|
|
11135
11405
|
if (runtime === "julia") return `include(${quotedPath})`;
|
|
11136
11406
|
if (runtime === "r") return `source(${quotedPath}, local=.GlobalEnv)`;
|
|
11137
11407
|
if (runtime === "ghci") return `:script ${quotedPath}`;
|
|
11138
11408
|
if (runtime === "clojure") return `(do (load-file ${quotedPath}) :pi-studio/silent)`;
|
|
11409
|
+
if (runtime === "shell") return `. ${shellQuote(sourceFile)}; touch ${shellQuote(doneFile)}`;
|
|
11139
11410
|
return `exec(open(${quotedPath}, encoding="utf-8").read(), globals())`;
|
|
11140
11411
|
}
|
|
11141
11412
|
|
|
11142
|
-
function prepareStudioReplSubmission(
|
|
11413
|
+
function prepareStudioReplSubmission(
|
|
11414
|
+
sessionName: string,
|
|
11415
|
+
source: string,
|
|
11416
|
+
details: { submissionId: string; echoMode: ReplSubmissionEchoMode },
|
|
11417
|
+
runtimeHint?: StudioReplRuntime | "unknown",
|
|
11418
|
+
): StudioReplPreparedSubmission {
|
|
11143
11419
|
const normalizedSource = String(source || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
11144
|
-
const runtime = inferStudioReplSessionRuntime(sessionName).runtime;
|
|
11145
|
-
if (runtime !== "unknown"
|
|
11146
|
-
const
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
}
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11420
|
+
const runtime = runtimeHint && runtimeHint !== "unknown" ? runtimeHint : inferStudioReplSessionRuntime(sessionName).runtime;
|
|
11421
|
+
if (runtime !== "unknown") {
|
|
11422
|
+
const display = createReplSubmissionDisplay({
|
|
11423
|
+
entryId: details.submissionId,
|
|
11424
|
+
origin: "pi-studio",
|
|
11425
|
+
code: normalizedSource,
|
|
11426
|
+
mode: details.echoMode,
|
|
11427
|
+
});
|
|
11428
|
+
const controlFiles: StudioReplControlFiles = createPrivateReplControlFiles({
|
|
11429
|
+
extension: getStudioReplControlExtension(runtime),
|
|
11430
|
+
buildSource: ({ doneFile }: StudioReplControlFiles) => {
|
|
11431
|
+
const source = buildStudioReplControlSource(runtime, normalizedSource, doneFile, display);
|
|
11432
|
+
if (source === null) throw new Error(`No control-file wrapper is available for ${runtime}.`);
|
|
11433
|
+
return source;
|
|
11434
|
+
},
|
|
11435
|
+
});
|
|
11436
|
+
const submissionLine = buildStudioReplSubmissionLine(runtime, controlFiles.sourceFile, controlFiles.doneFile);
|
|
11437
|
+
const completionLine = runtime === "ghci" ? buildStudioGhciCompletionLine(controlFiles.doneFile, display) : undefined;
|
|
11438
|
+
return {
|
|
11439
|
+
runtime,
|
|
11440
|
+
usedControlFile: true,
|
|
11441
|
+
controlFiles,
|
|
11442
|
+
display,
|
|
11443
|
+
completionLine,
|
|
11444
|
+
submissionText: [submissionLine, completionLine].filter(Boolean).join("\n"),
|
|
11445
|
+
};
|
|
11164
11446
|
}
|
|
11165
11447
|
|
|
11166
11448
|
return {
|
|
@@ -11170,39 +11452,60 @@ function prepareStudioReplSubmission(sessionName: string, source: string): Studi
|
|
|
11170
11452
|
};
|
|
11171
11453
|
}
|
|
11172
11454
|
|
|
11173
|
-
function pasteTextToStudioReplPane(sessionName: string, text: string): { ok: true } | { ok: false; message: string } {
|
|
11455
|
+
function pasteTextToStudioReplPane(sessionName: string, text: string, paneTarget?: string): { ok: true } | { ok: false; message: string; submissionStarted: boolean } {
|
|
11174
11456
|
const bufferName = `pi-studio-repl-${randomUUID().replace(/-/g, "")}`;
|
|
11175
|
-
const target = getStudioReplPaneTarget(sessionName);
|
|
11457
|
+
const target = paneTarget || getStudioReplPaneTarget(sessionName);
|
|
11176
11458
|
const loadResult = runStudioTmux(["load-buffer", "-b", bufferName, "-"], { input: text, timeout: 5_000 });
|
|
11177
|
-
if (!loadResult.ok) return { ok: false, message: loadResult.message || "Failed to load text into tmux buffer." };
|
|
11459
|
+
if (!loadResult.ok) return { ok: false, message: loadResult.message || "Failed to load text into tmux buffer.", submissionStarted: false };
|
|
11178
11460
|
try {
|
|
11179
11461
|
const pasteResult = runStudioTmux(["paste-buffer", "-d", "-b", bufferName, "-t", target], { timeout: 5_000 });
|
|
11180
|
-
if (!pasteResult.ok) return { ok: false, message: pasteResult.message || "Failed to paste text into REPL session." };
|
|
11462
|
+
if (!pasteResult.ok) return { ok: false, message: pasteResult.message || "Failed to paste text into REPL session.", submissionStarted: false };
|
|
11181
11463
|
const enterResult = runStudioTmux(["send-keys", "-t", target, "C-m"], { timeout: 5_000 });
|
|
11182
|
-
if (!enterResult.ok) return { ok: false, message: enterResult.message || "Failed to send Enter to REPL session." };
|
|
11464
|
+
if (!enterResult.ok) return { ok: false, message: enterResult.message || "Failed to send Enter to REPL session.", submissionStarted: true };
|
|
11183
11465
|
return { ok: true };
|
|
11184
11466
|
} finally {
|
|
11185
11467
|
runStudioTmux(["delete-buffer", "-b", bufferName], { timeout: 2_000 });
|
|
11186
11468
|
}
|
|
11187
11469
|
}
|
|
11188
11470
|
|
|
11189
|
-
function sendTextToStudioReplSession(
|
|
11471
|
+
function sendTextToStudioReplSession(
|
|
11472
|
+
sessionName: string,
|
|
11473
|
+
text: string,
|
|
11474
|
+
paneTarget?: string,
|
|
11475
|
+
runtimeHint?: StudioReplRuntime | "unknown",
|
|
11476
|
+
options: { submissionId?: string; echoMode?: string } = {},
|
|
11477
|
+
): StudioReplSendSuccess | StudioReplSendFailure {
|
|
11190
11478
|
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
|
|
11191
11479
|
const source = String(text || "");
|
|
11192
11480
|
if (!source.trim()) return { ok: false, message: "Editor text is empty." };
|
|
11193
11481
|
if (source.length > STUDIO_REPL_SEND_MAX_CHARS) {
|
|
11194
11482
|
return { ok: false, message: `REPL input is too large (${source.length} chars; max ${STUDIO_REPL_SEND_MAX_CHARS}).` };
|
|
11195
11483
|
}
|
|
11196
|
-
const prepared = prepareStudioReplSubmission(sessionName, source
|
|
11197
|
-
|
|
11198
|
-
|
|
11484
|
+
const prepared = prepareStudioReplSubmission(sessionName, source, {
|
|
11485
|
+
submissionId: options.submissionId || `pi-studio:local:${randomUUID()}`,
|
|
11486
|
+
echoMode: normalizeReplSubmissionEchoMode(options.echoMode, STUDIO_REPL_TOOL_ECHO_MODE) as ReplSubmissionEchoMode,
|
|
11487
|
+
}, runtimeHint);
|
|
11488
|
+
const pasted = pasteTextToStudioReplPane(sessionName, prepared.submissionText, paneTarget);
|
|
11489
|
+
if (!pasted.ok) return {
|
|
11490
|
+
ok: false,
|
|
11491
|
+
message: pasted.message,
|
|
11492
|
+
submissionStarted: pasted.submissionStarted,
|
|
11493
|
+
runtime: prepared.runtime,
|
|
11494
|
+
usedControlFile: prepared.usedControlFile,
|
|
11495
|
+
submissionText: prepared.submissionText,
|
|
11496
|
+
completionLine: prepared.completionLine,
|
|
11497
|
+
controlFiles: prepared.controlFiles,
|
|
11498
|
+
display: prepared.display,
|
|
11499
|
+
};
|
|
11199
11500
|
return {
|
|
11200
11501
|
ok: true,
|
|
11201
11502
|
message: "Sent to REPL.",
|
|
11202
11503
|
runtime: prepared.runtime,
|
|
11203
11504
|
usedControlFile: prepared.usedControlFile,
|
|
11204
11505
|
submissionText: prepared.submissionText,
|
|
11506
|
+
completionLine: prepared.completionLine,
|
|
11205
11507
|
controlFiles: prepared.controlFiles,
|
|
11508
|
+
display: prepared.display,
|
|
11206
11509
|
};
|
|
11207
11510
|
}
|
|
11208
11511
|
|
|
@@ -11227,14 +11530,15 @@ function stripStudioReplSubmissionEcho(output: string): string {
|
|
|
11227
11530
|
let value = String(output || "").replace(/^\s+/, "");
|
|
11228
11531
|
// The raw tmux mirror should stay raw, but Studio/tool result output should not
|
|
11229
11532
|
// expose the temp-file wrapper used to submit multiline snippets safely. The
|
|
11230
|
-
//
|
|
11231
|
-
//
|
|
11533
|
+
// The root fragment catches both legacy long Studio paths and compact private
|
|
11534
|
+
// control paths when IPython wraps them across continuation prompt lines.
|
|
11232
11535
|
const submissionEchoPatterns = [
|
|
11233
|
-
/^.*exec\(open\([\s\S]*?pi-studio-re[\s\S]*?globals\(\)\)\s*$/gm,
|
|
11234
|
-
/^.*include\([\s\S]*?pi-studio-re[\s\S]*?\.jl"\)\s*$/gm,
|
|
11235
|
-
/^.*source\([\s\S]*?pi-studio-re[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
|
|
11236
|
-
/^.*:script\s+[\s\S]*?pi-studio-re[\s\S]*?\.ghci"?\s*$/gm,
|
|
11237
|
-
/^.*\(do\s+\(load-file\s+[\s\S]*?pi-studio-re[\s\S]*?:pi-studio\/silent\)\s*$/gm,
|
|
11536
|
+
/^.*exec\(open\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?globals\(\)\)\s*$/gm,
|
|
11537
|
+
/^.*include\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.jl"\)\s*$/gm,
|
|
11538
|
+
/^.*source\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
|
|
11539
|
+
/^.*:script\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.ghci"?\s*$/gm,
|
|
11540
|
+
/^.*\(do\s+\(load-file\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?:pi-studio\/silent\)\s*$/gm,
|
|
11541
|
+
/^.*\.\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.sh[\s\S]*?(?:done\.flag|[a-f0-9]{16}\.done).*$/gm,
|
|
11238
11542
|
];
|
|
11239
11543
|
for (const pattern of submissionEchoPatterns) value = value.replace(pattern, "");
|
|
11240
11544
|
return value.replace(/^(?:\s*\n)+/, "").replace(/[\t ]+$/gm, "").trimEnd();
|
|
@@ -11248,8 +11552,21 @@ function stripTrailingStudioReplPrompts(output: string): string {
|
|
|
11248
11552
|
return lines.join("\n").trimEnd();
|
|
11249
11553
|
}
|
|
11250
11554
|
|
|
11251
|
-
function
|
|
11252
|
-
|
|
11555
|
+
function stripStudioReplCompletionEcho(output: string, completionLine?: string): string {
|
|
11556
|
+
const value = String(output || "");
|
|
11557
|
+
if (!completionLine) return value;
|
|
11558
|
+
const completionIndex = value.lastIndexOf(completionLine);
|
|
11559
|
+
if (completionIndex < 0) return value;
|
|
11560
|
+
const lineStart = value.lastIndexOf("\n", completionIndex - 1) + 1;
|
|
11561
|
+
const newlineIndex = value.indexOf("\n", completionIndex + completionLine.length);
|
|
11562
|
+
const lineEnd = newlineIndex < 0 ? value.length : newlineIndex + 1;
|
|
11563
|
+
return value.slice(0, lineStart) + value.slice(lineEnd);
|
|
11564
|
+
}
|
|
11565
|
+
|
|
11566
|
+
function cleanStudioReplCapturedOutput(output: string, display?: ReplSubmissionDisplay, completionLine?: string): string {
|
|
11567
|
+
const completionCleaned = stripStudioReplCompletionEcho(output, completionLine);
|
|
11568
|
+
const displayCleaned = display ? stripReplSubmissionDisplay(completionCleaned, display) : completionCleaned;
|
|
11569
|
+
return stripTrailingStudioReplPrompts(stripStudioReplSubmissionEcho(displayCleaned));
|
|
11253
11570
|
}
|
|
11254
11571
|
|
|
11255
11572
|
function normalizeStudioReplJournalMode(mode: unknown): StudioReplJournalEntry["mode"] {
|
|
@@ -11257,18 +11574,27 @@ function normalizeStudioReplJournalMode(mode: unknown): StudioReplJournalEntry["
|
|
|
11257
11574
|
}
|
|
11258
11575
|
|
|
11259
11576
|
function normalizeStudioReplJournalStatus(status: unknown): StudioReplJournalEntry["status"] {
|
|
11260
|
-
return status === "
|
|
11577
|
+
return status === "sending"
|
|
11578
|
+
|| status === "captured"
|
|
11579
|
+
|| status === "timeout"
|
|
11580
|
+
|| status === "error"
|
|
11581
|
+
|| status === "note"
|
|
11582
|
+
? status
|
|
11583
|
+
: "sent";
|
|
11261
11584
|
}
|
|
11262
11585
|
|
|
11263
|
-
function makeStudioReplJournalEntry(details: Partial<StudioReplJournalEntry> & { sessionName: string; code
|
|
11586
|
+
function makeStudioReplJournalEntry(details: Partial<StudioReplJournalEntry> & { sessionName: string; code?: string }): StudioReplJournalEntry {
|
|
11264
11587
|
const now = Date.now();
|
|
11588
|
+
const createdAt = typeof details.createdAt === "number" && Number.isFinite(details.createdAt) ? details.createdAt : now;
|
|
11265
11589
|
return {
|
|
11266
11590
|
id: typeof details.id === "string" && details.id.trim() ? details.id.trim() : `repl-journal-${now.toString(36)}-${randomUUID().slice(0, 8)}`,
|
|
11267
11591
|
requestId: typeof details.requestId === "string" ? details.requestId : "",
|
|
11268
|
-
createdAt
|
|
11592
|
+
createdAt,
|
|
11269
11593
|
updatedAt: typeof details.updatedAt === "number" && Number.isFinite(details.updatedAt) ? details.updatedAt : now,
|
|
11594
|
+
completedAt: typeof details.completedAt === "number" && Number.isFinite(details.completedAt) ? details.completedAt : null,
|
|
11270
11595
|
sessionName: String(details.sessionName || ""),
|
|
11271
|
-
runtime: details.runtime || "unknown",
|
|
11596
|
+
runtime: normalizeStudioReplRuntime(details.runtime) || "unknown",
|
|
11597
|
+
origin: details.origin === "pi-repl" || details.origin === "pi-studio" ? details.origin : "unknown",
|
|
11272
11598
|
label: typeof details.label === "string" && details.label.trim() ? details.label.trim() : "REPL send",
|
|
11273
11599
|
mode: normalizeStudioReplJournalMode(details.mode),
|
|
11274
11600
|
prose: typeof details.prose === "string" ? details.prose : "",
|
|
@@ -11276,6 +11602,10 @@ function makeStudioReplJournalEntry(details: Partial<StudioReplJournalEntry> & {
|
|
|
11276
11602
|
output: typeof details.output === "string" ? details.output : "",
|
|
11277
11603
|
status: normalizeStudioReplJournalStatus(details.status),
|
|
11278
11604
|
skippedChunks: Math.max(0, Math.floor(Number(details.skippedChunks) || 0)),
|
|
11605
|
+
codeOmittedChars: Math.max(0, Math.floor(Number(details.codeOmittedChars) || 0)),
|
|
11606
|
+
proseOmittedChars: Math.max(0, Math.floor(Number(details.proseOmittedChars) || 0)),
|
|
11607
|
+
outputOmittedChars: Math.max(0, Math.floor(Number(details.outputOmittedChars) || 0)),
|
|
11608
|
+
sharedSynced: details.sharedSynced === true,
|
|
11279
11609
|
};
|
|
11280
11610
|
}
|
|
11281
11611
|
|
|
@@ -11298,47 +11628,206 @@ function upsertStudioReplJournalEntry(entry: StudioReplJournalEntry): StudioRepl
|
|
|
11298
11628
|
studioReplJournalEntries = studioReplJournalEntries
|
|
11299
11629
|
.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0))
|
|
11300
11630
|
.slice(-STUDIO_REPL_JOURNAL_MAX_ENTRIES);
|
|
11631
|
+
const retainedIds = new Set(studioReplJournalEntries.map((candidate) => candidate.id));
|
|
11632
|
+
for (const unsyncedId of studioReplUnsyncedJournalEntryIds) {
|
|
11633
|
+
if (!retainedIds.has(unsyncedId)) studioReplUnsyncedJournalEntryIds.delete(unsyncedId);
|
|
11634
|
+
}
|
|
11301
11635
|
return studioReplJournalEntries.find((candidate) => candidate.id === entry.id || (entry.requestId && candidate.requestId === entry.requestId)) || entry;
|
|
11302
11636
|
}
|
|
11303
11637
|
|
|
11304
|
-
function
|
|
11305
|
-
|
|
11638
|
+
function getStudioReplSessionRecordIdentity(session: StudioReplSessionInfo): {
|
|
11639
|
+
sessionName: string;
|
|
11640
|
+
tmuxSessionId: string;
|
|
11641
|
+
tmuxSessionCreatedAt: number;
|
|
11642
|
+
runtime: StudioReplRuntime | "unknown";
|
|
11643
|
+
} {
|
|
11644
|
+
return {
|
|
11645
|
+
sessionName: session.sessionName,
|
|
11646
|
+
tmuxSessionId: session.tmuxSessionId,
|
|
11647
|
+
tmuxSessionCreatedAt: session.tmuxSessionCreatedAt,
|
|
11648
|
+
runtime: session.runtime,
|
|
11649
|
+
};
|
|
11650
|
+
}
|
|
11651
|
+
|
|
11652
|
+
function isSameStudioReplSessionLifetime(left: StudioReplSessionInfo, right: StudioReplSessionInfo): boolean {
|
|
11653
|
+
return left.sessionName === right.sessionName
|
|
11654
|
+
&& left.tmuxSessionId === right.tmuxSessionId
|
|
11655
|
+
&& left.tmuxSessionCreatedAt === right.tmuxSessionCreatedAt;
|
|
11656
|
+
}
|
|
11657
|
+
|
|
11658
|
+
function recordStudioReplJournalEntry(
|
|
11659
|
+
details: Partial<StudioReplJournalEntry> & { sessionName: string; code?: string },
|
|
11660
|
+
knownSession?: StudioReplSessionInfo | null,
|
|
11661
|
+
): StudioReplJournalEntry {
|
|
11662
|
+
let entry = upsertStudioReplJournalEntry(makeStudioReplJournalEntry({
|
|
11663
|
+
...details,
|
|
11664
|
+
origin: details.origin || "pi-studio",
|
|
11665
|
+
sharedSynced: false,
|
|
11666
|
+
}));
|
|
11667
|
+
const localEntryId = entry.id;
|
|
11668
|
+
studioReplUnsyncedJournalEntryIds.add(localEntryId);
|
|
11669
|
+
// A previously validated exact-lifetime identity lets completion/error updates
|
|
11670
|
+
// reach the same sidecar even if that tmux session disappears before capture.
|
|
11671
|
+
const session = knownSession || inspectStudioReplSession(entry.sessionName);
|
|
11672
|
+
if (!session?.recordId || session.recordWarning || session.sessionName !== entry.sessionName) return entry;
|
|
11673
|
+
try {
|
|
11674
|
+
const recorded = upsertReplSessionRecordEntry(
|
|
11675
|
+
session.recordId,
|
|
11676
|
+
getStudioReplSessionRecordIdentity(session),
|
|
11677
|
+
entry,
|
|
11678
|
+
{ origin: entry.origin === "pi-repl" ? "pi-repl" : "pi-studio" },
|
|
11679
|
+
);
|
|
11680
|
+
entry = upsertStudioReplJournalEntry(makeStudioReplJournalEntry({
|
|
11681
|
+
...(recorded.entry as Partial<StudioReplJournalEntry> & { sessionName: string }),
|
|
11682
|
+
sharedSynced: true,
|
|
11683
|
+
}));
|
|
11684
|
+
studioReplUnsyncedJournalEntryIds.delete(localEntryId);
|
|
11685
|
+
studioReplUnsyncedJournalEntryIds.delete(entry.id);
|
|
11686
|
+
} catch {
|
|
11687
|
+
// Retain the in-memory entry as a standalone fallback when shared state is unavailable.
|
|
11688
|
+
}
|
|
11689
|
+
return entry;
|
|
11306
11690
|
}
|
|
11307
11691
|
|
|
11308
|
-
function
|
|
11692
|
+
function getStudioReplJournalEntries(sessionName: string | null | undefined): StudioReplJournalEntry[] {
|
|
11693
|
+
const normalizedSessionName = String(sessionName || "").trim();
|
|
11694
|
+
const sessions = normalizedSessionName
|
|
11695
|
+
? [inspectStudioReplSession(normalizedSessionName)].filter((session): session is StudioReplSessionInfo => Boolean(session))
|
|
11696
|
+
: listStudioReplSessions().sessions;
|
|
11697
|
+
for (const session of sessions) {
|
|
11698
|
+
if (!session.recordId || session.recordWarning) continue;
|
|
11699
|
+
try {
|
|
11700
|
+
const record = readReplSessionRecord(session.recordId, getStudioReplSessionRecordIdentity(session));
|
|
11701
|
+
if (record) {
|
|
11702
|
+
const unsynced = studioReplJournalEntries.filter((entry) => (
|
|
11703
|
+
entry.sessionName === session.sessionName && studioReplUnsyncedJournalEntryIds.has(entry.id)
|
|
11704
|
+
));
|
|
11705
|
+
const sharedEntries = record.entries.map((sharedEntry: Partial<StudioReplJournalEntry> & { sessionName: string }) => (
|
|
11706
|
+
makeStudioReplJournalEntry({ ...sharedEntry, sharedSynced: true })
|
|
11707
|
+
));
|
|
11708
|
+
for (const sharedEntry of sharedEntries) studioReplUnsyncedJournalEntryIds.delete(sharedEntry.id);
|
|
11709
|
+
studioReplJournalEntries = [
|
|
11710
|
+
...studioReplJournalEntries.filter((entry) => entry.sessionName !== session.sessionName),
|
|
11711
|
+
...sharedEntries,
|
|
11712
|
+
...unsynced.filter((entry) => !sharedEntries.some((sharedEntry: StudioReplJournalEntry) => sharedEntry.id === entry.id)),
|
|
11713
|
+
].sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0)).slice(-STUDIO_REPL_JOURNAL_MAX_ENTRIES);
|
|
11714
|
+
}
|
|
11715
|
+
} catch {
|
|
11716
|
+
// The raw tmux mirror and in-memory Studio record remain independently usable.
|
|
11717
|
+
}
|
|
11718
|
+
}
|
|
11719
|
+
const entries = normalizedSessionName
|
|
11720
|
+
? studioReplJournalEntries.filter((entry) => entry.sessionName === normalizedSessionName)
|
|
11721
|
+
: studioReplJournalEntries;
|
|
11722
|
+
return entries.slice(-STUDIO_REPL_JOURNAL_MAX_ENTRIES).map((entry) => ({
|
|
11723
|
+
...entry,
|
|
11724
|
+
sharedSynced: !studioReplUnsyncedJournalEntryIds.has(entry.id) && entry.sharedSynced === true,
|
|
11725
|
+
}));
|
|
11726
|
+
}
|
|
11727
|
+
|
|
11728
|
+
function updateStudioReplJournalEntryOutput(
|
|
11729
|
+
requestId: string,
|
|
11730
|
+
sessionName: string,
|
|
11731
|
+
output: string,
|
|
11732
|
+
status: StudioReplJournalEntry["status"],
|
|
11733
|
+
knownSession?: StudioReplSessionInfo | null,
|
|
11734
|
+
): void {
|
|
11309
11735
|
const normalizedRequestId = String(requestId || "");
|
|
11310
11736
|
const normalizedSessionName = String(sessionName || "");
|
|
11311
|
-
const existing =
|
|
11737
|
+
const existing = getStudioReplJournalEntries(normalizedSessionName).slice().reverse().find((entry) => (
|
|
11312
11738
|
(normalizedRequestId && entry.requestId === normalizedRequestId)
|
|
11313
|
-
|| (!normalizedRequestId &&
|
|
11739
|
+
|| (!normalizedRequestId && entry.status === "sent")
|
|
11314
11740
|
));
|
|
11315
11741
|
if (!existing) return;
|
|
11316
|
-
|
|
11742
|
+
recordStudioReplJournalEntry({
|
|
11317
11743
|
...existing,
|
|
11318
11744
|
output: String(output || ""),
|
|
11319
11745
|
status,
|
|
11746
|
+
completedAt: Date.now(),
|
|
11320
11747
|
updatedAt: Date.now(),
|
|
11321
|
-
});
|
|
11748
|
+
}, knownSession);
|
|
11322
11749
|
}
|
|
11323
11750
|
|
|
11324
|
-
function
|
|
11751
|
+
function clearStudioReplJournal(sessionName: string): void {
|
|
11325
11752
|
const normalizedSessionName = String(sessionName || "").trim();
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11753
|
+
if (!normalizedSessionName) return;
|
|
11754
|
+
const clearedIds = studioReplJournalEntries.filter((entry) => entry.sessionName === normalizedSessionName).map((entry) => entry.id);
|
|
11755
|
+
studioReplJournalEntries = studioReplJournalEntries.filter((entry) => entry.sessionName !== normalizedSessionName);
|
|
11756
|
+
for (const id of clearedIds) studioReplUnsyncedJournalEntryIds.delete(id);
|
|
11757
|
+
const session = inspectStudioReplSession(normalizedSessionName);
|
|
11758
|
+
if (!session?.recordId || session.recordWarning) return;
|
|
11759
|
+
clearReplSessionRecord(session.recordId, getStudioReplSessionRecordIdentity(session));
|
|
11330
11760
|
}
|
|
11331
11761
|
|
|
11332
|
-
async function waitForStudioReplDoneFile(doneFile: string | undefined, timeoutMs: number): Promise<boolean> {
|
|
11762
|
+
async function waitForStudioReplDoneFile(doneFile: string | undefined, timeoutMs: number, signal?: AbortSignal): Promise<boolean> {
|
|
11333
11763
|
if (!doneFile) return false;
|
|
11334
11764
|
const deadline = Date.now() + clampStudioReplSendTimeout(timeoutMs);
|
|
11335
11765
|
while (Date.now() < deadline) {
|
|
11336
11766
|
if (existsSync(doneFile)) return true;
|
|
11767
|
+
if (signal?.aborted) return false;
|
|
11337
11768
|
await sleep(100);
|
|
11338
11769
|
}
|
|
11339
11770
|
return existsSync(doneFile);
|
|
11340
11771
|
}
|
|
11341
11772
|
|
|
11773
|
+
function sleepWithoutKeepingStudioProcessAlive(ms: number): Promise<void> {
|
|
11774
|
+
return new Promise((resolveSleep) => {
|
|
11775
|
+
const timer = setTimeout(resolveSleep, ms);
|
|
11776
|
+
timer.unref?.();
|
|
11777
|
+
});
|
|
11778
|
+
}
|
|
11779
|
+
|
|
11780
|
+
function retainStudioReplSubmissionUntilSettled(
|
|
11781
|
+
session: StudioReplSessionInfo,
|
|
11782
|
+
controlFiles: StudioReplControlFiles,
|
|
11783
|
+
lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>> | null,
|
|
11784
|
+
): void {
|
|
11785
|
+
// A caller timeout or abort does not stop code already submitted to tmux.
|
|
11786
|
+
// Keep the private control files (and any shared lease) until the runtime
|
|
11787
|
+
// wrapper reports completion, or until this exact tmux lifetime disappears.
|
|
11788
|
+
void (async () => {
|
|
11789
|
+
let nextIdentityCheck = 0;
|
|
11790
|
+
let missingChecks = 0;
|
|
11791
|
+
try {
|
|
11792
|
+
while (!existsSync(controlFiles.doneFile)) {
|
|
11793
|
+
if (!existsSync(controlFiles.sourceFile)) return;
|
|
11794
|
+
if (Date.now() >= nextIdentityCheck) {
|
|
11795
|
+
try {
|
|
11796
|
+
const current = inspectStudioReplSession(session.sessionName);
|
|
11797
|
+
if (current && isSameStudioReplSessionLifetime(session, current)) {
|
|
11798
|
+
missingChecks = 0;
|
|
11799
|
+
} else {
|
|
11800
|
+
missingChecks += 1;
|
|
11801
|
+
if (missingChecks >= 3) return;
|
|
11802
|
+
}
|
|
11803
|
+
} catch {
|
|
11804
|
+
// A transient inspection failure must not make overlapping sends safe.
|
|
11805
|
+
missingChecks = 0;
|
|
11806
|
+
}
|
|
11807
|
+
nextIdentityCheck = Date.now() + 1_000;
|
|
11808
|
+
}
|
|
11809
|
+
await sleepWithoutKeepingStudioProcessAlive(100);
|
|
11810
|
+
}
|
|
11811
|
+
} finally {
|
|
11812
|
+
cleanupPrivateReplControlFiles(controlFiles);
|
|
11813
|
+
await lease?.release().catch(() => undefined);
|
|
11814
|
+
}
|
|
11815
|
+
})();
|
|
11816
|
+
}
|
|
11817
|
+
|
|
11818
|
+
async function releaseOrRetainStudioReplSubmission(
|
|
11819
|
+
lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>> | null,
|
|
11820
|
+
session: StudioReplSessionInfo | null,
|
|
11821
|
+
controlFiles: StudioReplControlFiles | undefined,
|
|
11822
|
+
): Promise<void> {
|
|
11823
|
+
if (session && controlFiles && !existsSync(controlFiles.doneFile)) {
|
|
11824
|
+
retainStudioReplSubmissionUntilSettled(session, controlFiles, lease);
|
|
11825
|
+
return;
|
|
11826
|
+
}
|
|
11827
|
+
cleanupPrivateReplControlFiles(controlFiles);
|
|
11828
|
+
await lease?.release().catch(() => undefined);
|
|
11829
|
+
}
|
|
11830
|
+
|
|
11342
11831
|
function interruptStudioReplSession(sessionName: string): { ok: true; message: string } | { ok: false; message: string } {
|
|
11343
11832
|
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
|
|
11344
11833
|
const result = runStudioTmux(["send-keys", "-t", getStudioReplPaneTarget(sessionName), "C-c"], { timeout: 5_000 });
|
|
@@ -11945,6 +12434,11 @@ ${cssVarsBlock}
|
|
|
11945
12434
|
<option value="raw" selected>Send mode: Raw</option>
|
|
11946
12435
|
<option value="literate">Send mode: Literate</option>
|
|
11947
12436
|
</select>
|
|
12437
|
+
<select id="replEchoModeSelect" class="studio-flat-select" hidden aria-label="REPL submission echo" title="Choose how much submitted code Studio displays in the raw REPL pane.">
|
|
12438
|
+
<option value="off" selected>Pane echo: Off</option>
|
|
12439
|
+
<option value="summary">Pane echo: Summary</option>
|
|
12440
|
+
<option value="full">Pane echo: Full (raw code)</option>
|
|
12441
|
+
</select>
|
|
11948
12442
|
</div>
|
|
11949
12443
|
<div class="source-actions-row">
|
|
11950
12444
|
<button id="copyDraftBtn" type="button" title="Copy the current editor text to the clipboard.">Copy</button>
|
|
@@ -12479,6 +12973,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12479
12973
|
parameters: STUDIO_REPL_STATUS_TOOL_PARAMS,
|
|
12480
12974
|
async execute(_toolCallId, params) {
|
|
12481
12975
|
const selected = selectStudioReplSessionForTool({ sessionName: params.sessionName, target: params.target });
|
|
12976
|
+
const recordEntries = selected.session ? getStudioReplJournalEntries(selected.session.sessionName) : [];
|
|
12482
12977
|
const lines = [
|
|
12483
12978
|
`Active Studio REPL: ${studioReplActiveSessionName || "none"}`,
|
|
12484
12979
|
`tmux sessions visible to Studio: ${selected.sessions.length}`,
|
|
@@ -12486,6 +12981,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
12486
12981
|
if (selected.error) lines.push(`Selection: ${selected.error}`);
|
|
12487
12982
|
if (selected.session) {
|
|
12488
12983
|
lines.push(`Selected: ${selected.session.sessionName} (${selected.session.runtime}, ${selected.session.source})`);
|
|
12984
|
+
if (selected.session.recordId && !selected.session.recordWarning) {
|
|
12985
|
+
lines.push(`Shared clean record: ${recordEntries.length} entries (${selected.session.recordId})`);
|
|
12986
|
+
}
|
|
12987
|
+
if (selected.session.recordWarning) lines.push(`Shared record warning: ${selected.session.recordWarning}`);
|
|
12489
12988
|
}
|
|
12490
12989
|
for (const session of selected.sessions) {
|
|
12491
12990
|
lines.push(`- ${session.sessionName} | runtime=${session.runtime} | source=${session.source} | target=${session.target}`);
|
|
@@ -12495,6 +12994,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12495
12994
|
details: {
|
|
12496
12995
|
activeSessionName: studioReplActiveSessionName,
|
|
12497
12996
|
selectedSession: selected.session,
|
|
12997
|
+
selectedRecordEntries: recordEntries,
|
|
12498
12998
|
sessions: selected.sessions,
|
|
12499
12999
|
} as Record<string, unknown>,
|
|
12500
13000
|
};
|
|
@@ -12509,11 +13009,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
12509
13009
|
promptGuidelines: [
|
|
12510
13010
|
"Use studio_repl_send when the user asks to run code in the active Studio REPL.",
|
|
12511
13011
|
"Do not improvise tmux paste-buffer commands for Studio REPL code; studio_repl_send handles multiline quoting and runtime-specific submission.",
|
|
13012
|
+
"Submitted-code display is off by default; use echoMode='summary' or echoMode='full' only when the user asks to show code and alignment anchors in the raw pane.",
|
|
12512
13013
|
"If several REPL sessions of the same runtime are running, use studio_repl_status first or pass the exact sessionName when known.",
|
|
12513
13014
|
],
|
|
12514
13015
|
parameters: STUDIO_REPL_SEND_TOOL_PARAMS,
|
|
12515
13016
|
executionMode: "sequential",
|
|
12516
|
-
async execute(toolCallId, params) {
|
|
13017
|
+
async execute(toolCallId, params, signal) {
|
|
12517
13018
|
const selected = selectStudioReplSessionForTool({ sessionName: params.sessionName, target: params.target });
|
|
12518
13019
|
if (!selected.session) {
|
|
12519
13020
|
return {
|
|
@@ -12522,75 +13023,152 @@ export default function (pi: ExtensionAPI) {
|
|
|
12522
13023
|
};
|
|
12523
13024
|
}
|
|
12524
13025
|
|
|
12525
|
-
const before = captureStudioReplSession(selected.session.sessionName);
|
|
12526
|
-
const beforeTranscript = before.ok ? before.transcript : "";
|
|
12527
|
-
const sent = sendTextToStudioReplSession(selected.session.sessionName, params.code);
|
|
12528
|
-
if (!sent.ok) {
|
|
12529
|
-
return {
|
|
12530
|
-
content: [{ type: "text", text: sent.message }],
|
|
12531
|
-
details: { ok: false, error: sent.message, session: selected.session, sessions: selected.sessions } as Record<string, unknown>,
|
|
12532
|
-
};
|
|
12533
|
-
}
|
|
12534
|
-
studioReplActiveSessionName = selected.session.sessionName;
|
|
12535
|
-
|
|
12536
13026
|
const timeoutMs = clampStudioReplSendTimeout(params.timeoutMs);
|
|
12537
|
-
let
|
|
12538
|
-
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
sessionName:
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
13027
|
+
let lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>> | null = null;
|
|
13028
|
+
let submittedSession: StudioReplSessionInfo | null = null;
|
|
13029
|
+
let submittedControlFiles: StudioReplControlFiles | undefined;
|
|
13030
|
+
let journalEntry: StudioReplJournalEntry | null = null;
|
|
13031
|
+
try {
|
|
13032
|
+
if (selected.session.recordId && !selected.session.recordWarning) {
|
|
13033
|
+
lease = await acquireReplSessionSendLease(selected.session.recordId, {
|
|
13034
|
+
owner: `pi-studio:tool:${toolCallId}`,
|
|
13035
|
+
waitMs: timeoutMs,
|
|
13036
|
+
signal,
|
|
13037
|
+
});
|
|
13038
|
+
}
|
|
13039
|
+
const currentSession = inspectStudioReplSession(selected.session.sessionName);
|
|
13040
|
+
if (!currentSession || !isSameStudioReplSessionLifetime(selected.session, currentSession)) {
|
|
13041
|
+
throw new Error(`REPL session ${selected.session.sessionName} changed while Studio was waiting to send.`);
|
|
13042
|
+
}
|
|
13043
|
+
if (lease && currentSession.recordId !== selected.session.recordId) {
|
|
13044
|
+
throw new Error(`The shared record for ${selected.session.sessionName} changed while Studio was waiting to send.`);
|
|
13045
|
+
}
|
|
13046
|
+
const before = captureStudioReplSession(currentSession.sessionName);
|
|
13047
|
+
if (!before.ok) throw new Error(`Could not capture ${currentSession.sessionName} before sending: ${before.message}`);
|
|
13048
|
+
if (!isSameStudioReplSessionLifetime(currentSession, before.session)) {
|
|
13049
|
+
throw new Error(`REPL session ${currentSession.sessionName} changed before Studio could capture it.`);
|
|
13050
|
+
}
|
|
13051
|
+
const beforeTranscript = before.transcript;
|
|
13052
|
+
journalEntry = recordStudioReplJournalEntry({
|
|
13053
|
+
id: `pi-studio:tool:${toolCallId}`,
|
|
13054
|
+
requestId: `tool:${toolCallId}`,
|
|
13055
|
+
sessionName: currentSession.sessionName,
|
|
13056
|
+
runtime: currentSession.runtime,
|
|
13057
|
+
origin: "pi-studio",
|
|
13058
|
+
label: "Pi",
|
|
13059
|
+
mode: "agent",
|
|
13060
|
+
code: params.code,
|
|
13061
|
+
status: "sending",
|
|
13062
|
+
}, currentSession);
|
|
13063
|
+
const sent = sendTextToStudioReplSession(currentSession.sessionName, params.code, currentSession.target, currentSession.runtime, {
|
|
13064
|
+
submissionId: journalEntry.id,
|
|
13065
|
+
echoMode: params.echoMode || STUDIO_REPL_TOOL_ECHO_MODE,
|
|
13066
|
+
});
|
|
13067
|
+
submittedControlFiles = sent.controlFiles;
|
|
13068
|
+
if (!sent.ok) {
|
|
13069
|
+
if (sent.submissionStarted) submittedSession = currentSession;
|
|
13070
|
+
journalEntry = recordStudioReplJournalEntry({
|
|
13071
|
+
...journalEntry,
|
|
13072
|
+
output: sent.message,
|
|
13073
|
+
status: "error",
|
|
13074
|
+
completedAt: Date.now(),
|
|
13075
|
+
}, currentSession);
|
|
13076
|
+
return {
|
|
13077
|
+
content: [{ type: "text", text: sent.message }],
|
|
13078
|
+
details: { ok: false, error: sent.message, session: selected.session, sessions: selected.sessions, recordEntryId: journalEntry.id } as Record<string, unknown>,
|
|
13079
|
+
};
|
|
13080
|
+
}
|
|
13081
|
+
submittedSession = currentSession;
|
|
13082
|
+
studioReplActiveSessionName = selected.session.sessionName;
|
|
13083
|
+
|
|
13084
|
+
let completed = false;
|
|
13085
|
+
if (sent.controlFiles?.doneFile) {
|
|
13086
|
+
completed = await waitForStudioReplDoneFile(sent.controlFiles.doneFile, timeoutMs, signal);
|
|
13087
|
+
if (signal?.aborted && !completed) {
|
|
13088
|
+
throw new Error("studio_repl_send was aborted after submission; the shared session remains busy until the running code settles.");
|
|
13089
|
+
}
|
|
13090
|
+
} else {
|
|
13091
|
+
await sleep(Math.min(750, timeoutMs));
|
|
13092
|
+
}
|
|
13093
|
+
const after = captureStudioReplSession(currentSession.sessionName);
|
|
13094
|
+
if (!after.ok) throw new Error(`Could not capture ${currentSession.sessionName} after sending: ${after.message}`);
|
|
13095
|
+
if (!isSameStudioReplSessionLifetime(currentSession, after.session)) {
|
|
13096
|
+
throw new Error(`REPL session ${currentSession.sessionName} changed before Studio captured the result.`);
|
|
13097
|
+
}
|
|
13098
|
+
const afterTranscript = after.transcript;
|
|
13099
|
+
const rawOutput = extractStudioReplTranscriptDelta(beforeTranscript, afterTranscript);
|
|
13100
|
+
const output = cleanStudioReplCapturedOutput(rawOutput, sent.display, sent.completionLine);
|
|
13101
|
+
const status: StudioReplJournalEntry["status"] = sent.controlFiles?.doneFile
|
|
13102
|
+
? (completed ? "captured" : "timeout")
|
|
13103
|
+
: (output.trim() ? "captured" : "sent");
|
|
13104
|
+
journalEntry = recordStudioReplJournalEntry({
|
|
13105
|
+
...journalEntry,
|
|
13106
|
+
runtime: sent.runtime === "unknown" ? selected.session.runtime : sent.runtime,
|
|
13107
|
+
output,
|
|
13108
|
+
status,
|
|
13109
|
+
completedAt: Date.now(),
|
|
13110
|
+
}, currentSession);
|
|
13111
|
+
const statusLine = sent.controlFiles?.doneFile
|
|
13112
|
+
? (completed ? "Completed." : `Timed out after ${timeoutMs} ms waiting for completion marker.`)
|
|
13113
|
+
: "Submitted.";
|
|
13114
|
+
const text = [
|
|
13115
|
+
`${statusLine} ${sent.message}`,
|
|
13116
|
+
output ? "" : undefined,
|
|
13117
|
+
output || undefined,
|
|
13118
|
+
].filter(Boolean).join("\n");
|
|
13119
|
+
broadcastStudioReplToolSend({
|
|
13120
|
+
toolCallId,
|
|
13121
|
+
sessionName: selected.session.sessionName,
|
|
13122
|
+
runtime: sent.runtime === "unknown" ? selected.session.runtime : sent.runtime,
|
|
13123
|
+
code: params.code,
|
|
13124
|
+
label: "Pi",
|
|
13125
|
+
output,
|
|
13126
|
+
sharedSynced: journalEntry.sharedSynced === true,
|
|
12582
13127
|
completed,
|
|
12583
13128
|
timedOut: Boolean(sent.controlFiles?.doneFile && !completed),
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
|
|
13129
|
+
transcript: afterTranscript,
|
|
13130
|
+
capturedAt: Date.now(),
|
|
13131
|
+
journalEntries: getStudioReplJournalEntries(selected.session.sessionName),
|
|
13132
|
+
});
|
|
13133
|
+
return {
|
|
13134
|
+
content: [{ type: "text", text }],
|
|
13135
|
+
details: {
|
|
13136
|
+
ok: true,
|
|
13137
|
+
completed,
|
|
13138
|
+
timedOut: Boolean(sent.controlFiles?.doneFile && !completed),
|
|
13139
|
+
timeoutMs,
|
|
13140
|
+
session: selected.session,
|
|
13141
|
+
sessions: selected.sessions,
|
|
13142
|
+
runtime: sent.runtime,
|
|
13143
|
+
usedControlFile: sent.usedControlFile,
|
|
13144
|
+
submissionText: sent.submissionText,
|
|
13145
|
+
controlFiles: sent.controlFiles,
|
|
13146
|
+
echoMode: sent.display?.mode || "off",
|
|
13147
|
+
submissionAnchorId: sent.display?.enabled ? sent.display.anchorId : undefined,
|
|
13148
|
+
output,
|
|
13149
|
+
recordEntryId: journalEntry.id,
|
|
13150
|
+
recordPath: selected.session.recordPath,
|
|
13151
|
+
} as Record<string, unknown>,
|
|
13152
|
+
};
|
|
13153
|
+
} catch (error) {
|
|
13154
|
+
if (journalEntry) {
|
|
13155
|
+
try {
|
|
13156
|
+
recordStudioReplJournalEntry({
|
|
13157
|
+
...journalEntry,
|
|
13158
|
+
output: error instanceof Error ? error.message : String(error),
|
|
13159
|
+
status: error instanceof Error && /timed out/i.test(error.message) ? "timeout" : "error",
|
|
13160
|
+
completedAt: Date.now(),
|
|
13161
|
+
}, submittedSession || selected.session);
|
|
13162
|
+
} catch {
|
|
13163
|
+
// Preserve the execution error when record maintenance also fails.
|
|
13164
|
+
}
|
|
13165
|
+
}
|
|
13166
|
+
throw error;
|
|
13167
|
+
} finally {
|
|
13168
|
+
await releaseOrRetainStudioReplSubmission(lease, submittedSession, submittedControlFiles);
|
|
13169
|
+
lease = null;
|
|
13170
|
+
submittedControlFiles = undefined;
|
|
13171
|
+
}
|
|
12594
13172
|
},
|
|
12595
13173
|
});
|
|
12596
13174
|
|
|
@@ -15453,33 +16031,65 @@ export default function (pi: ExtensionAPI) {
|
|
|
15453
16031
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
15454
16032
|
return;
|
|
15455
16033
|
}
|
|
15456
|
-
const before = captureStudioReplSession(msg.sessionName);
|
|
15457
|
-
const beforeTranscript = before.ok ? before.transcript : "";
|
|
15458
|
-
const sent = sendTextToStudioReplSession(msg.sessionName, msg.text);
|
|
15459
|
-
if (!sent.ok) {
|
|
15460
|
-
sendToClient(client, { type: "error", requestId: msg.requestId, message: sent.message });
|
|
15461
|
-
sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId, replError: sent.message });
|
|
15462
|
-
return;
|
|
15463
|
-
}
|
|
15464
|
-
studioReplActiveSessionName = msg.sessionName;
|
|
15465
|
-
recordStudioReplJournalEntry({
|
|
15466
|
-
requestId: msg.requestId,
|
|
15467
|
-
sessionName: msg.sessionName,
|
|
15468
|
-
runtime: sent.runtime,
|
|
15469
|
-
label: "Studio",
|
|
15470
|
-
mode: "raw",
|
|
15471
|
-
code: msg.text,
|
|
15472
|
-
status: "sent",
|
|
15473
|
-
});
|
|
15474
|
-
sendToClient(client, {
|
|
15475
|
-
type: "repl_send_ack",
|
|
15476
|
-
requestId: msg.requestId,
|
|
15477
|
-
sessionName: msg.sessionName,
|
|
15478
|
-
message: sent.message,
|
|
15479
|
-
journalEntries: getStudioReplJournalEntries(msg.sessionName),
|
|
15480
|
-
});
|
|
15481
16034
|
void (async () => {
|
|
16035
|
+
const session = inspectStudioReplSession(msg.sessionName);
|
|
16036
|
+
let journalEntry: StudioReplJournalEntry | null = null;
|
|
16037
|
+
let lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>> | null = null;
|
|
16038
|
+
let submittedSession: StudioReplSessionInfo | null = null;
|
|
16039
|
+
let submittedControlFiles: StudioReplControlFiles | undefined;
|
|
15482
16040
|
try {
|
|
16041
|
+
if (!session) throw new Error(`No tmux REPL session named ${msg.sessionName}.`);
|
|
16042
|
+
if (session.recordId && !session.recordWarning) {
|
|
16043
|
+
lease = await acquireReplSessionSendLease(session.recordId, {
|
|
16044
|
+
owner: `pi-studio:browser:${msg.requestId}`,
|
|
16045
|
+
waitMs: STUDIO_REPL_SEND_DEFAULT_TIMEOUT_MS,
|
|
16046
|
+
});
|
|
16047
|
+
}
|
|
16048
|
+
const currentSession = inspectStudioReplSession(msg.sessionName);
|
|
16049
|
+
if (!currentSession || !isSameStudioReplSessionLifetime(session, currentSession)) {
|
|
16050
|
+
throw new Error(`REPL session ${msg.sessionName} changed while Studio was waiting to send.`);
|
|
16051
|
+
}
|
|
16052
|
+
if (lease && currentSession.recordId !== session.recordId) {
|
|
16053
|
+
throw new Error(`The shared record for ${msg.sessionName} changed while Studio was waiting to send.`);
|
|
16054
|
+
}
|
|
16055
|
+
const before = captureStudioReplSession(msg.sessionName);
|
|
16056
|
+
if (!before.ok) throw new Error(`Could not capture ${msg.sessionName} before sending: ${before.message}`);
|
|
16057
|
+
if (!isSameStudioReplSessionLifetime(currentSession, before.session)) {
|
|
16058
|
+
throw new Error(`REPL session ${msg.sessionName} changed before Studio could capture it.`);
|
|
16059
|
+
}
|
|
16060
|
+
const beforeTranscript = before.transcript;
|
|
16061
|
+
journalEntry = recordStudioReplJournalEntry({
|
|
16062
|
+
id: msg.journalEntryId,
|
|
16063
|
+
requestId: msg.requestId,
|
|
16064
|
+
createdAt: msg.createdAt,
|
|
16065
|
+
sessionName: msg.sessionName,
|
|
16066
|
+
runtime: session.runtime,
|
|
16067
|
+
origin: "pi-studio",
|
|
16068
|
+
label: msg.label || "Studio",
|
|
16069
|
+
mode: msg.mode || "raw",
|
|
16070
|
+
prose: msg.prose || "",
|
|
16071
|
+
code: msg.text,
|
|
16072
|
+
status: "sending",
|
|
16073
|
+
skippedChunks: msg.skippedChunks,
|
|
16074
|
+
}, currentSession);
|
|
16075
|
+
const sent = sendTextToStudioReplSession(msg.sessionName, msg.text, currentSession.target, currentSession.runtime, {
|
|
16076
|
+
submissionId: journalEntry.id,
|
|
16077
|
+
echoMode: msg.echoMode,
|
|
16078
|
+
});
|
|
16079
|
+
submittedControlFiles = sent.controlFiles;
|
|
16080
|
+
if (!sent.ok) {
|
|
16081
|
+
if (sent.submissionStarted) submittedSession = currentSession;
|
|
16082
|
+
throw new Error(sent.message);
|
|
16083
|
+
}
|
|
16084
|
+
submittedSession = currentSession;
|
|
16085
|
+
studioReplActiveSessionName = msg.sessionName;
|
|
16086
|
+
sendToClient(client, {
|
|
16087
|
+
type: "repl_send_ack",
|
|
16088
|
+
requestId: msg.requestId,
|
|
16089
|
+
sessionName: msg.sessionName,
|
|
16090
|
+
message: sent.message,
|
|
16091
|
+
journalEntries: getStudioReplJournalEntries(msg.sessionName),
|
|
16092
|
+
});
|
|
15483
16093
|
const timeoutMs = STUDIO_REPL_SEND_DEFAULT_TIMEOUT_MS;
|
|
15484
16094
|
let completed = false;
|
|
15485
16095
|
if (sent.controlFiles?.doneFile) {
|
|
@@ -15488,24 +16098,108 @@ export default function (pi: ExtensionAPI) {
|
|
|
15488
16098
|
await sleep(Math.min(750, timeoutMs));
|
|
15489
16099
|
}
|
|
15490
16100
|
const after = captureStudioReplSession(msg.sessionName);
|
|
15491
|
-
|
|
16101
|
+
if (!after.ok) throw new Error(`Could not capture ${msg.sessionName} after sending: ${after.message}`);
|
|
16102
|
+
if (!isSameStudioReplSessionLifetime(currentSession, after.session)) {
|
|
16103
|
+
throw new Error(`REPL session ${msg.sessionName} changed before Studio captured the result.`);
|
|
16104
|
+
}
|
|
16105
|
+
const afterTranscript = after.transcript;
|
|
15492
16106
|
const rawOutput = extractStudioReplTranscriptDelta(beforeTranscript, afterTranscript);
|
|
15493
|
-
const output = cleanStudioReplCapturedOutput(rawOutput);
|
|
16107
|
+
const output = cleanStudioReplCapturedOutput(rawOutput, sent.display, sent.completionLine);
|
|
15494
16108
|
updateStudioReplJournalEntryOutput(
|
|
15495
16109
|
msg.requestId,
|
|
15496
16110
|
msg.sessionName,
|
|
15497
16111
|
output,
|
|
15498
|
-
sent.controlFiles?.doneFile
|
|
16112
|
+
sent.controlFiles?.doneFile ? (completed ? "captured" : "timeout") : (output.trim() ? "captured" : "sent"),
|
|
16113
|
+
currentSession,
|
|
15499
16114
|
);
|
|
15500
16115
|
sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId });
|
|
15501
16116
|
} catch (error) {
|
|
15502
|
-
|
|
15503
|
-
|
|
16117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16118
|
+
if (journalEntry) updateStudioReplJournalEntryOutput(
|
|
16119
|
+
msg.requestId,
|
|
16120
|
+
msg.sessionName,
|
|
16121
|
+
message,
|
|
16122
|
+
"error",
|
|
16123
|
+
submittedSession || session,
|
|
16124
|
+
);
|
|
16125
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message });
|
|
16126
|
+
sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId, replError: message });
|
|
16127
|
+
} finally {
|
|
16128
|
+
await releaseOrRetainStudioReplSubmission(lease, submittedSession, submittedControlFiles);
|
|
16129
|
+
lease = null;
|
|
16130
|
+
submittedControlFiles = undefined;
|
|
15504
16131
|
}
|
|
15505
16132
|
})();
|
|
15506
16133
|
return;
|
|
15507
16134
|
}
|
|
15508
16135
|
|
|
16136
|
+
if (msg.type === "repl_journal_upsert_request") {
|
|
16137
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
16138
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
16139
|
+
return;
|
|
16140
|
+
}
|
|
16141
|
+
try {
|
|
16142
|
+
if (!inspectStudioReplSession(msg.sessionName)) throw new Error(`No tmux REPL session named ${msg.sessionName}.`);
|
|
16143
|
+
recordStudioReplJournalEntry({
|
|
16144
|
+
...msg.entry,
|
|
16145
|
+
sessionName: msg.sessionName,
|
|
16146
|
+
origin: "pi-studio",
|
|
16147
|
+
});
|
|
16148
|
+
sendToClient(client, {
|
|
16149
|
+
type: "repl_journal_ack",
|
|
16150
|
+
requestId: msg.requestId,
|
|
16151
|
+
sessionName: msg.sessionName,
|
|
16152
|
+
journalEntries: getStudioReplJournalEntries(msg.sessionName),
|
|
16153
|
+
});
|
|
16154
|
+
} catch (error) {
|
|
16155
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: error instanceof Error ? error.message : String(error) });
|
|
16156
|
+
}
|
|
16157
|
+
return;
|
|
16158
|
+
}
|
|
16159
|
+
|
|
16160
|
+
if (msg.type === "repl_journal_import_request") {
|
|
16161
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
16162
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
16163
|
+
return;
|
|
16164
|
+
}
|
|
16165
|
+
try {
|
|
16166
|
+
if (!inspectStudioReplSession(msg.sessionName)) throw new Error(`No tmux REPL session named ${msg.sessionName}.`);
|
|
16167
|
+
for (const entry of msg.entries) {
|
|
16168
|
+
recordStudioReplJournalEntry({ ...entry, sessionName: msg.sessionName, origin: "pi-studio" });
|
|
16169
|
+
}
|
|
16170
|
+
sendToClient(client, {
|
|
16171
|
+
type: "repl_journal_ack",
|
|
16172
|
+
requestId: msg.requestId,
|
|
16173
|
+
sessionName: msg.sessionName,
|
|
16174
|
+
journalEntries: getStudioReplJournalEntries(msg.sessionName),
|
|
16175
|
+
});
|
|
16176
|
+
} catch (error) {
|
|
16177
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: error instanceof Error ? error.message : String(error) });
|
|
16178
|
+
}
|
|
16179
|
+
return;
|
|
16180
|
+
}
|
|
16181
|
+
|
|
16182
|
+
if (msg.type === "repl_journal_clear_request") {
|
|
16183
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
16184
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
16185
|
+
return;
|
|
16186
|
+
}
|
|
16187
|
+
try {
|
|
16188
|
+
if (!inspectStudioReplSession(msg.sessionName)) throw new Error(`No tmux REPL session named ${msg.sessionName}.`);
|
|
16189
|
+
clearStudioReplJournal(msg.sessionName);
|
|
16190
|
+
sendToClient(client, {
|
|
16191
|
+
type: "repl_journal_ack",
|
|
16192
|
+
requestId: msg.requestId,
|
|
16193
|
+
sessionName: msg.sessionName,
|
|
16194
|
+
journalEntries: [],
|
|
16195
|
+
cleared: true,
|
|
16196
|
+
});
|
|
16197
|
+
} catch (error) {
|
|
16198
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: error instanceof Error ? error.message : String(error) });
|
|
16199
|
+
}
|
|
16200
|
+
return;
|
|
16201
|
+
}
|
|
16202
|
+
|
|
15509
16203
|
if (msg.type === "repl_interrupt_request") {
|
|
15510
16204
|
if (!isValidRequestId(msg.requestId)) {
|
|
15511
16205
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|