pi-studio 0.9.56 → 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/index.ts CHANGED
@@ -55,6 +55,15 @@ import {
55
55
  readReplSessionRecord,
56
56
  upsertReplSessionRecordEntry,
57
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";
58
67
  import { buildStudioForwardingHint, buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
59
68
  import {
60
69
  buildStudioPendingPage,
@@ -127,6 +136,8 @@ type TerminalActivityPhase = "idle" | "running" | "tool" | "responding";
127
136
  type StudioPromptMode = "response" | "run" | "effective";
128
137
  type StudioPromptTriggerKind = "run" | "steer";
129
138
  type StudioReplRuntime = "shell" | "python" | "ipython" | "julia" | "r" | "ghci" | "clojure";
139
+ type ReplSubmissionEchoMode = "off" | "summary" | "full";
140
+ type ReplSubmissionDisplay = ReturnType<typeof createReplSubmissionDisplay>;
130
141
  type StudioQuizAngle = "general" | "scientist" | "mathematician" | "statistician" | "developer" | "reviewer";
131
142
  type StudioQuizScope = "selection" | "editor" | "file" | "folder" | "repo";
132
143
  type StudioQuizThinking = "off" | "minimal" | "low" | "medium" | "high";
@@ -746,6 +757,7 @@ interface ReplSendRequestMessage {
746
757
  requestId: string;
747
758
  sessionName: string;
748
759
  text: string;
760
+ echoMode?: ReplSubmissionEchoMode;
749
761
  journalEntryId?: string;
750
762
  createdAt?: number;
751
763
  label?: string;
@@ -968,7 +980,6 @@ const STUDIO_REPL_SEND_DEFAULT_TIMEOUT_MS = 20_000;
968
980
  const STUDIO_REPL_SEND_MAX_TIMEOUT_MS = 120_000;
969
981
  const STUDIO_REPL_JOURNAL_MAX_ENTRIES = 300;
970
982
  const STUDIO_REPL_RUNTIME_OPTION = "@pi_repl_runtime";
971
- const STUDIO_REPL_CONTROL_ROOT = join(tmpdir(), "pi-studio-repl");
972
983
  const STUDIO_SUBPROCESS_OUTPUT_MAX_BYTES = 2_000_000;
973
984
  const STUDIO_PANDOC_TIMEOUT_MS = readStudioPositiveEnvMs("PI_STUDIO_PANDOC_TIMEOUT_MS", 120_000, 5_000, 15 * 60_000);
974
985
  const STUDIO_LATEX_TIMEOUT_MS = readStudioPositiveEnvMs("PI_STUDIO_LATEX_TIMEOUT_MS", 120_000, 5_000, 15 * 60_000);
@@ -988,7 +999,12 @@ const STUDIO_REPL_SEND_TOOL_PARAMS = Type.Object({
988
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." })),
989
1000
  target: Type.Optional(Type.String({ description: "Optional runtime target: shell, python, ipython, julia, r, ghci, or clojure. Used when sessionName is omitted." })),
990
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
+ )),
991
1006
  });
1007
+ const STUDIO_REPL_TOOL_ECHO_MODE = normalizeReplSubmissionEchoMode(process.env.PI_STUDIO_REPL_ECHO_MODE) as ReplSubmissionEchoMode;
992
1008
  const STUDIO_REPL_STATUS_TOOL_PARAMS = Type.Object({
993
1009
  sessionName: Type.Optional(Type.String({ description: "Exact Studio/pi-repl tmux session name to inspect." })),
994
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." })),
@@ -10064,6 +10080,7 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
10064
10080
  requestId: msg.requestId,
10065
10081
  sessionName: msg.sessionName,
10066
10082
  text: msg.text,
10083
+ echoMode: normalizeReplSubmissionEchoMode(msg.echoMode) as ReplSubmissionEchoMode,
10067
10084
  journalEntryId: typeof msg.journalEntryId === "string" ? msg.journalEntryId.slice(0, 240) : undefined,
10068
10085
  createdAt: typeof msg.createdAt === "number" && Number.isFinite(msg.createdAt) ? msg.createdAt : undefined,
10069
10086
  label: typeof msg.label === "string" ? msg.label.slice(0, 240) : undefined,
@@ -11159,7 +11176,9 @@ type StudioReplPreparedSubmission = {
11159
11176
  runtime: StudioReplRuntime | "unknown";
11160
11177
  usedControlFile: boolean;
11161
11178
  submissionText: string;
11179
+ completionLine?: string;
11162
11180
  controlFiles?: StudioReplControlFiles;
11181
+ display?: ReplSubmissionDisplay;
11163
11182
  };
11164
11183
 
11165
11184
  type StudioReplSendSuccess = {
@@ -11168,7 +11187,9 @@ type StudioReplSendSuccess = {
11168
11187
  runtime: StudioReplRuntime | "unknown";
11169
11188
  usedControlFile: boolean;
11170
11189
  submissionText: string;
11190
+ completionLine?: string;
11171
11191
  controlFiles?: StudioReplControlFiles;
11192
+ display?: ReplSubmissionDisplay;
11172
11193
  };
11173
11194
 
11174
11195
  type StudioReplSendFailure = {
@@ -11178,7 +11199,9 @@ type StudioReplSendFailure = {
11178
11199
  runtime?: StudioReplRuntime | "unknown";
11179
11200
  usedControlFile?: boolean;
11180
11201
  submissionText?: string;
11202
+ completionLine?: string;
11181
11203
  controlFiles?: StudioReplControlFiles;
11204
+ display?: ReplSubmissionDisplay;
11182
11205
  };
11183
11206
 
11184
11207
  function sleep(ms: number): Promise<void> {
@@ -11194,33 +11217,44 @@ function shellQuote(value: string): string {
11194
11217
  return `'${String(value || "").replace(/'/g, `'"'"'`)}'`;
11195
11218
  }
11196
11219
 
11197
- function getStudioReplControlFiles(sessionName: string, runtime: StudioReplRuntime | "unknown"): StudioReplControlFiles {
11198
- const safeSession = sessionName.replace(/[^-_.A-Za-z0-9]+/g, "_");
11199
- const safeRuntime = String(runtime || "repl").replace(/[^-_.A-Za-z0-9]+/g, "_");
11200
- const dir = join(STUDIO_REPL_CONTROL_ROOT, safeSession, randomUUID().replace(/-/g, ""));
11201
- const extension = runtime === "julia"
11202
- ? "jl"
11203
- : runtime === "r"
11204
- ? "R"
11205
- : runtime === "ghci"
11206
- ? "ghci"
11207
- : runtime === "clojure"
11208
- ? "clj"
11209
- : runtime === "shell"
11210
- ? "sh"
11211
- : "py";
11212
- return {
11213
- dir,
11214
- sourceFile: join(dir, `studio-repl-${safeRuntime}.${extension}`),
11215
- doneFile: join(dir, "done.flag"),
11216
- };
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`)})`);
11242
+ }
11243
+
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)})`);
11217
11247
  }
11218
11248
 
11219
- function buildStudioPythonControlSource(runtime: "python" | "ipython", code: string, doneFile: string): string {
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)})`] : [];
11220
11252
  if (runtime === "ipython") {
11221
11253
  return [
11222
11254
  "from pathlib import Path as __pi_studio_path",
11255
+ "import builtins as __pi_studio_builtins",
11223
11256
  "import traceback as __pi_studio_traceback",
11257
+ ...prefix,
11224
11258
  "try:",
11225
11259
  " __pi_studio_ip = get_ipython()",
11226
11260
  " if __pi_studio_ip is None:",
@@ -11231,14 +11265,17 @@ function buildStudioPythonControlSource(runtime: "python" | "ipython", code: str
11231
11265
  "except Exception:",
11232
11266
  " __pi_studio_traceback.print_exc()",
11233
11267
  "finally:",
11268
+ ...completion,
11234
11269
  ` __pi_studio_path(${JSON.stringify(doneFile)}).write_text('done\\n', encoding='utf-8')`,
11235
11270
  ].join("\n");
11236
11271
  }
11237
11272
 
11238
11273
  return [
11239
11274
  "from pathlib import Path as __pi_studio_path",
11275
+ "import builtins as __pi_studio_builtins",
11240
11276
  "import traceback as __pi_studio_traceback",
11241
11277
  `__pi_studio_code = ${JSON.stringify(code)}`,
11278
+ ...prefix,
11242
11279
  "try:",
11243
11280
  " try:",
11244
11281
  " __pi_studio_expr = compile(__pi_studio_code, '<pi-studio-repl>', 'eval')",
@@ -11251,12 +11288,15 @@ function buildStudioPythonControlSource(runtime: "python" | "ipython", code: str
11251
11288
  "except Exception:",
11252
11289
  " __pi_studio_traceback.print_exc()",
11253
11290
  "finally:",
11291
+ ...completion,
11254
11292
  ` __pi_studio_path(${JSON.stringify(doneFile)}).write_text('done\\n', encoding='utf-8')`,
11255
11293
  ].join("\n");
11256
11294
  }
11257
11295
 
11258
- 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)})`] : [];
11259
11298
  return [
11299
+ ...buildStudioJuliaDisplayStatements(display),
11260
11300
  "try",
11261
11301
  ` local __pi_studio_result = Base.include_string(Main, ${JSON.stringify(code)}, "pi-studio-repl")`,
11262
11302
  " if !isnothing(__pi_studio_result)",
@@ -11265,14 +11305,17 @@ function buildStudioJuliaControlSource(code: string, doneFile: string): string {
11265
11305
  "catch e",
11266
11306
  " Base.display_error(stderr, e, catch_backtrace())",
11267
11307
  "finally",
11308
+ ...completion,
11268
11309
  ` write(${JSON.stringify(doneFile)}, "done\\n")`,
11269
11310
  "end",
11270
11311
  ].join("\n");
11271
11312
  }
11272
11313
 
11273
- 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`)})`] : [];
11274
11316
  return [
11275
11317
  "local({",
11318
+ ...buildStudioRDisplayStatements(display, " "),
11276
11319
  ` .__pi_studio_done_file <- ${JSON.stringify(doneFile)}`,
11277
11320
  ` .__pi_studio_code <- ${JSON.stringify(code)}`,
11278
11321
  " tryCatch({",
@@ -11294,15 +11337,33 @@ function buildStudioRControlSource(code: string, doneFile: string): string {
11294
11337
  " message(\"Error in \", .__pi_studio_call_text, \": \", conditionMessage(e))",
11295
11338
  " }",
11296
11339
  " }, finally = {",
11340
+ ...completion,
11297
11341
  " writeLines(\"done\", .__pi_studio_done_file)",
11298
11342
  " })",
11299
11343
  "})",
11300
11344
  ].join("\n");
11301
11345
  }
11302
11346
 
11303
- function buildStudioClojureControlSource(code: string, doneFile: string): string {
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)})`] : [];
11304
11364
  return [
11305
11365
  "(let [code " + JSON.stringify(code) + "]",
11366
+ ...buildStudioClojureDisplayStatements(display, " "),
11306
11367
  " (try",
11307
11368
  " (let [rdr (clojure.lang.LineNumberingPushbackReader. (java.io.StringReader. code))]",
11308
11369
  " (loop [last-val nil has-val false]",
@@ -11313,17 +11374,29 @@ function buildStudioClojureControlSource(code: string, doneFile: string): string
11313
11374
  " (catch Throwable t",
11314
11375
  " (#'clojure.main/repl-caught t))",
11315
11376
  " (finally",
11377
+ ...completion,
11316
11378
  ` (spit ${JSON.stringify(doneFile)} "done\\n"))))`,
11317
11379
  ].join("\n");
11318
11380
  }
11319
11381
 
11320
- function buildStudioReplControlSource(runtime: StudioReplRuntime, code: string, doneFile: string): string | null {
11321
- if (runtime === "python" || runtime === "ipython") return buildStudioPythonControlSource(runtime, code, doneFile);
11322
- if (runtime === "julia") return buildStudioJuliaControlSource(code, doneFile);
11323
- if (runtime === "r") return buildStudioRControlSource(code, doneFile);
11324
- if (runtime === "ghci") return `${code.replace(/\r/g, "").trimEnd()}\n:! touch ${shellQuote(doneFile)}\n`;
11325
- if (runtime === "clojure") return buildStudioClojureControlSource(code, doneFile);
11326
- if (runtime === "shell") return `${code.replace(/\r/g, "").trimEnd()}\n`;
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);
11327
11400
  return null;
11328
11401
  }
11329
11402
 
@@ -11340,29 +11413,36 @@ function buildStudioReplSubmissionLine(runtime: StudioReplRuntime, sourceFile: s
11340
11413
  function prepareStudioReplSubmission(
11341
11414
  sessionName: string,
11342
11415
  source: string,
11416
+ details: { submissionId: string; echoMode: ReplSubmissionEchoMode },
11343
11417
  runtimeHint?: StudioReplRuntime | "unknown",
11344
11418
  ): StudioReplPreparedSubmission {
11345
11419
  const normalizedSource = String(source || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
11346
11420
  const runtime = runtimeHint && runtimeHint !== "unknown" ? runtimeHint : inferStudioReplSessionRuntime(sessionName).runtime;
11347
11421
  if (runtime !== "unknown") {
11348
- const controlFiles = getStudioReplControlFiles(sessionName, runtime);
11349
- const controlSource = buildStudioReplControlSource(runtime, normalizedSource, controlFiles.doneFile);
11350
- if (controlSource) {
11351
- mkdirSync(controlFiles.dir, { recursive: true });
11352
- try {
11353
- unlinkSync(controlFiles.doneFile);
11354
- } catch {
11355
- // Ignore stale done file cleanup failures.
11356
- }
11357
- writeFileSync(controlFiles.sourceFile, controlSource, "utf-8");
11358
- const submissionLine = buildStudioReplSubmissionLine(runtime, controlFiles.sourceFile, controlFiles.doneFile);
11359
- return {
11360
- runtime,
11361
- usedControlFile: true,
11362
- controlFiles,
11363
- submissionText: submissionLine,
11364
- };
11365
- }
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
+ };
11366
11446
  }
11367
11447
 
11368
11448
  return {
@@ -11393,6 +11473,7 @@ function sendTextToStudioReplSession(
11393
11473
  text: string,
11394
11474
  paneTarget?: string,
11395
11475
  runtimeHint?: StudioReplRuntime | "unknown",
11476
+ options: { submissionId?: string; echoMode?: string } = {},
11396
11477
  ): StudioReplSendSuccess | StudioReplSendFailure {
11397
11478
  if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
11398
11479
  const source = String(text || "");
@@ -11400,7 +11481,10 @@ function sendTextToStudioReplSession(
11400
11481
  if (source.length > STUDIO_REPL_SEND_MAX_CHARS) {
11401
11482
  return { ok: false, message: `REPL input is too large (${source.length} chars; max ${STUDIO_REPL_SEND_MAX_CHARS}).` };
11402
11483
  }
11403
- const prepared = prepareStudioReplSubmission(sessionName, source, runtimeHint);
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);
11404
11488
  const pasted = pasteTextToStudioReplPane(sessionName, prepared.submissionText, paneTarget);
11405
11489
  if (!pasted.ok) return {
11406
11490
  ok: false,
@@ -11409,7 +11493,9 @@ function sendTextToStudioReplSession(
11409
11493
  runtime: prepared.runtime,
11410
11494
  usedControlFile: prepared.usedControlFile,
11411
11495
  submissionText: prepared.submissionText,
11496
+ completionLine: prepared.completionLine,
11412
11497
  controlFiles: prepared.controlFiles,
11498
+ display: prepared.display,
11413
11499
  };
11414
11500
  return {
11415
11501
  ok: true,
@@ -11417,7 +11503,9 @@ function sendTextToStudioReplSession(
11417
11503
  runtime: prepared.runtime,
11418
11504
  usedControlFile: prepared.usedControlFile,
11419
11505
  submissionText: prepared.submissionText,
11506
+ completionLine: prepared.completionLine,
11420
11507
  controlFiles: prepared.controlFiles,
11508
+ display: prepared.display,
11421
11509
  };
11422
11510
  }
11423
11511
 
@@ -11442,15 +11530,15 @@ function stripStudioReplSubmissionEcho(output: string): string {
11442
11530
  let value = String(output || "").replace(/^\s+/, "");
11443
11531
  // The raw tmux mirror should stay raw, but Studio/tool result output should not
11444
11532
  // expose the temp-file wrapper used to submit multiline snippets safely. The
11445
- // `pi-studio-re` fragment intentionally catches IPython's wrapped display of
11446
- // `pi-studio-repl/...` paths across continuation prompt lines.
11533
+ // The root fragment catches both legacy long Studio paths and compact private
11534
+ // control paths when IPython wraps them across continuation prompt lines.
11447
11535
  const submissionEchoPatterns = [
11448
- /^.*exec\(open\([\s\S]*?pi-studio-re[\s\S]*?globals\(\)\)\s*$/gm,
11449
- /^.*include\([\s\S]*?pi-studio-re[\s\S]*?\.jl"\)\s*$/gm,
11450
- /^.*source\([\s\S]*?pi-studio-re[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
11451
- /^.*:script\s+[\s\S]*?pi-studio-re[\s\S]*?\.ghci"?\s*$/gm,
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,
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,
11454
11542
  ];
11455
11543
  for (const pattern of submissionEchoPatterns) value = value.replace(pattern, "");
11456
11544
  return value.replace(/^(?:\s*\n)+/, "").replace(/[\t ]+$/gm, "").trimEnd();
@@ -11464,8 +11552,21 @@ function stripTrailingStudioReplPrompts(output: string): string {
11464
11552
  return lines.join("\n").trimEnd();
11465
11553
  }
11466
11554
 
11467
- function cleanStudioReplCapturedOutput(output: string): string {
11468
- return stripTrailingStudioReplPrompts(stripStudioReplSubmissionEcho(output));
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));
11469
11570
  }
11470
11571
 
11471
11572
  function normalizeStudioReplJournalMode(mode: unknown): StudioReplJournalEntry["mode"] {
@@ -11554,7 +11655,10 @@ function isSameStudioReplSessionLifetime(left: StudioReplSessionInfo, right: Stu
11554
11655
  && left.tmuxSessionCreatedAt === right.tmuxSessionCreatedAt;
11555
11656
  }
11556
11657
 
11557
- function recordStudioReplJournalEntry(details: Partial<StudioReplJournalEntry> & { sessionName: string; code?: string }): StudioReplJournalEntry {
11658
+ function recordStudioReplJournalEntry(
11659
+ details: Partial<StudioReplJournalEntry> & { sessionName: string; code?: string },
11660
+ knownSession?: StudioReplSessionInfo | null,
11661
+ ): StudioReplJournalEntry {
11558
11662
  let entry = upsertStudioReplJournalEntry(makeStudioReplJournalEntry({
11559
11663
  ...details,
11560
11664
  origin: details.origin || "pi-studio",
@@ -11562,8 +11666,10 @@ function recordStudioReplJournalEntry(details: Partial<StudioReplJournalEntry> &
11562
11666
  }));
11563
11667
  const localEntryId = entry.id;
11564
11668
  studioReplUnsyncedJournalEntryIds.add(localEntryId);
11565
- const session = inspectStudioReplSession(entry.sessionName);
11566
- if (!session?.recordId || session.recordWarning) return entry;
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;
11567
11673
  try {
11568
11674
  const recorded = upsertReplSessionRecordEntry(
11569
11675
  session.recordId,
@@ -11619,7 +11725,13 @@ function getStudioReplJournalEntries(sessionName: string | null | undefined): St
11619
11725
  }));
11620
11726
  }
11621
11727
 
11622
- function updateStudioReplJournalEntryOutput(requestId: string, sessionName: string, output: string, status: StudioReplJournalEntry["status"]): void {
11728
+ function updateStudioReplJournalEntryOutput(
11729
+ requestId: string,
11730
+ sessionName: string,
11731
+ output: string,
11732
+ status: StudioReplJournalEntry["status"],
11733
+ knownSession?: StudioReplSessionInfo | null,
11734
+ ): void {
11623
11735
  const normalizedRequestId = String(requestId || "");
11624
11736
  const normalizedSessionName = String(sessionName || "");
11625
11737
  const existing = getStudioReplJournalEntries(normalizedSessionName).slice().reverse().find((entry) => (
@@ -11633,7 +11745,7 @@ function updateStudioReplJournalEntryOutput(requestId: string, sessionName: stri
11633
11745
  status,
11634
11746
  completedAt: Date.now(),
11635
11747
  updatedAt: Date.now(),
11636
- });
11748
+ }, knownSession);
11637
11749
  }
11638
11750
 
11639
11751
  function clearStudioReplJournal(sessionName: string): void {
@@ -11665,19 +11777,20 @@ function sleepWithoutKeepingStudioProcessAlive(ms: number): Promise<void> {
11665
11777
  });
11666
11778
  }
11667
11779
 
11668
- function retainStudioReplSendLeaseUntilSubmissionSettles(
11780
+ function retainStudioReplSubmissionUntilSettled(
11669
11781
  session: StudioReplSessionInfo,
11670
- doneFile: string,
11671
- lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>>,
11782
+ controlFiles: StudioReplControlFiles,
11783
+ lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>> | null,
11672
11784
  ): void {
11673
11785
  // 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.
11786
+ // Keep the private control files (and any shared lease) until the runtime
11787
+ // wrapper reports completion, or until this exact tmux lifetime disappears.
11676
11788
  void (async () => {
11677
11789
  let nextIdentityCheck = 0;
11678
11790
  let missingChecks = 0;
11679
11791
  try {
11680
- while (!existsSync(doneFile)) {
11792
+ while (!existsSync(controlFiles.doneFile)) {
11793
+ if (!existsSync(controlFiles.sourceFile)) return;
11681
11794
  if (Date.now() >= nextIdentityCheck) {
11682
11795
  try {
11683
11796
  const current = inspectStudioReplSession(session.sessionName);
@@ -11696,21 +11809,23 @@ function retainStudioReplSendLeaseUntilSubmissionSettles(
11696
11809
  await sleepWithoutKeepingStudioProcessAlive(100);
11697
11810
  }
11698
11811
  } finally {
11699
- await lease.release().catch(() => undefined);
11812
+ cleanupPrivateReplControlFiles(controlFiles);
11813
+ await lease?.release().catch(() => undefined);
11700
11814
  }
11701
11815
  })();
11702
11816
  }
11703
11817
 
11704
- async function releaseOrRetainStudioReplSendLease(
11705
- lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>>,
11818
+ async function releaseOrRetainStudioReplSubmission(
11819
+ lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>> | null,
11706
11820
  session: StudioReplSessionInfo | null,
11707
- doneFile: string | undefined,
11821
+ controlFiles: StudioReplControlFiles | undefined,
11708
11822
  ): Promise<void> {
11709
- if (session && doneFile && !existsSync(doneFile)) {
11710
- retainStudioReplSendLeaseUntilSubmissionSettles(session, doneFile, lease);
11823
+ if (session && controlFiles && !existsSync(controlFiles.doneFile)) {
11824
+ retainStudioReplSubmissionUntilSettled(session, controlFiles, lease);
11711
11825
  return;
11712
11826
  }
11713
- await lease.release().catch(() => undefined);
11827
+ cleanupPrivateReplControlFiles(controlFiles);
11828
+ await lease?.release().catch(() => undefined);
11714
11829
  }
11715
11830
 
11716
11831
  function interruptStudioReplSession(sessionName: string): { ok: true; message: string } | { ok: false; message: string } {
@@ -12319,6 +12434,11 @@ ${cssVarsBlock}
12319
12434
  <option value="raw" selected>Send mode: Raw</option>
12320
12435
  <option value="literate">Send mode: Literate</option>
12321
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>
12322
12442
  </div>
12323
12443
  <div class="source-actions-row">
12324
12444
  <button id="copyDraftBtn" type="button" title="Copy the current editor text to the clipboard.">Copy</button>
@@ -12889,6 +13009,7 @@ export default function (pi: ExtensionAPI) {
12889
13009
  promptGuidelines: [
12890
13010
  "Use studio_repl_send when the user asks to run code in the active Studio REPL.",
12891
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.",
12892
13013
  "If several REPL sessions of the same runtime are running, use studio_repl_status first or pass the exact sessionName when known.",
12893
13014
  ],
12894
13015
  parameters: STUDIO_REPL_SEND_TOOL_PARAMS,
@@ -12905,7 +13026,7 @@ export default function (pi: ExtensionAPI) {
12905
13026
  const timeoutMs = clampStudioReplSendTimeout(params.timeoutMs);
12906
13027
  let lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>> | null = null;
12907
13028
  let submittedSession: StudioReplSessionInfo | null = null;
12908
- let submittedDoneFile: string | undefined;
13029
+ let submittedControlFiles: StudioReplControlFiles | undefined;
12909
13030
  let journalEntry: StudioReplJournalEntry | null = null;
12910
13031
  try {
12911
13032
  if (selected.session.recordId && !selected.session.recordWarning) {
@@ -12938,26 +13059,26 @@ export default function (pi: ExtensionAPI) {
12938
13059
  mode: "agent",
12939
13060
  code: params.code,
12940
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,
12941
13066
  });
12942
- const sent = sendTextToStudioReplSession(currentSession.sessionName, params.code, currentSession.target, currentSession.runtime);
13067
+ submittedControlFiles = sent.controlFiles;
12943
13068
  if (!sent.ok) {
12944
- if (sent.submissionStarted) {
12945
- submittedSession = currentSession;
12946
- submittedDoneFile = sent.controlFiles?.doneFile;
12947
- }
13069
+ if (sent.submissionStarted) submittedSession = currentSession;
12948
13070
  journalEntry = recordStudioReplJournalEntry({
12949
13071
  ...journalEntry,
12950
13072
  output: sent.message,
12951
13073
  status: "error",
12952
13074
  completedAt: Date.now(),
12953
- });
13075
+ }, currentSession);
12954
13076
  return {
12955
13077
  content: [{ type: "text", text: sent.message }],
12956
13078
  details: { ok: false, error: sent.message, session: selected.session, sessions: selected.sessions, recordEntryId: journalEntry.id } as Record<string, unknown>,
12957
13079
  };
12958
13080
  }
12959
13081
  submittedSession = currentSession;
12960
- submittedDoneFile = sent.controlFiles?.doneFile;
12961
13082
  studioReplActiveSessionName = selected.session.sessionName;
12962
13083
 
12963
13084
  let completed = false;
@@ -12976,7 +13097,7 @@ export default function (pi: ExtensionAPI) {
12976
13097
  }
12977
13098
  const afterTranscript = after.transcript;
12978
13099
  const rawOutput = extractStudioReplTranscriptDelta(beforeTranscript, afterTranscript);
12979
- const output = cleanStudioReplCapturedOutput(rawOutput);
13100
+ const output = cleanStudioReplCapturedOutput(rawOutput, sent.display, sent.completionLine);
12980
13101
  const status: StudioReplJournalEntry["status"] = sent.controlFiles?.doneFile
12981
13102
  ? (completed ? "captured" : "timeout")
12982
13103
  : (output.trim() ? "captured" : "sent");
@@ -12986,7 +13107,7 @@ export default function (pi: ExtensionAPI) {
12986
13107
  output,
12987
13108
  status,
12988
13109
  completedAt: Date.now(),
12989
- });
13110
+ }, currentSession);
12990
13111
  const statusLine = sent.controlFiles?.doneFile
12991
13112
  ? (completed ? "Completed." : `Timed out after ${timeoutMs} ms waiting for completion marker.`)
12992
13113
  : "Submitted.";
@@ -13022,6 +13143,8 @@ export default function (pi: ExtensionAPI) {
13022
13143
  usedControlFile: sent.usedControlFile,
13023
13144
  submissionText: sent.submissionText,
13024
13145
  controlFiles: sent.controlFiles,
13146
+ echoMode: sent.display?.mode || "off",
13147
+ submissionAnchorId: sent.display?.enabled ? sent.display.anchorId : undefined,
13025
13148
  output,
13026
13149
  recordEntryId: journalEntry.id,
13027
13150
  recordPath: selected.session.recordPath,
@@ -13035,17 +13158,16 @@ export default function (pi: ExtensionAPI) {
13035
13158
  output: error instanceof Error ? error.message : String(error),
13036
13159
  status: error instanceof Error && /timed out/i.test(error.message) ? "timeout" : "error",
13037
13160
  completedAt: Date.now(),
13038
- });
13161
+ }, submittedSession || selected.session);
13039
13162
  } catch {
13040
13163
  // Preserve the execution error when record maintenance also fails.
13041
13164
  }
13042
13165
  }
13043
13166
  throw error;
13044
13167
  } finally {
13045
- if (lease) {
13046
- await releaseOrRetainStudioReplSendLease(lease, submittedSession, submittedDoneFile);
13047
- lease = null;
13048
- }
13168
+ await releaseOrRetainStudioReplSubmission(lease, submittedSession, submittedControlFiles);
13169
+ lease = null;
13170
+ submittedControlFiles = undefined;
13049
13171
  }
13050
13172
  },
13051
13173
  });
@@ -15914,7 +16036,7 @@ export default function (pi: ExtensionAPI) {
15914
16036
  let journalEntry: StudioReplJournalEntry | null = null;
15915
16037
  let lease: Awaited<ReturnType<typeof acquireReplSessionSendLease>> | null = null;
15916
16038
  let submittedSession: StudioReplSessionInfo | null = null;
15917
- let submittedDoneFile: string | undefined;
16039
+ let submittedControlFiles: StudioReplControlFiles | undefined;
15918
16040
  try {
15919
16041
  if (!session) throw new Error(`No tmux REPL session named ${msg.sessionName}.`);
15920
16042
  if (session.recordId && !session.recordWarning) {
@@ -15949,17 +16071,17 @@ export default function (pi: ExtensionAPI) {
15949
16071
  code: msg.text,
15950
16072
  status: "sending",
15951
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,
15952
16078
  });
15953
- const sent = sendTextToStudioReplSession(msg.sessionName, msg.text, currentSession.target, currentSession.runtime);
16079
+ submittedControlFiles = sent.controlFiles;
15954
16080
  if (!sent.ok) {
15955
- if (sent.submissionStarted) {
15956
- submittedSession = currentSession;
15957
- submittedDoneFile = sent.controlFiles?.doneFile;
15958
- }
16081
+ if (sent.submissionStarted) submittedSession = currentSession;
15959
16082
  throw new Error(sent.message);
15960
16083
  }
15961
16084
  submittedSession = currentSession;
15962
- submittedDoneFile = sent.controlFiles?.doneFile;
15963
16085
  studioReplActiveSessionName = msg.sessionName;
15964
16086
  sendToClient(client, {
15965
16087
  type: "repl_send_ack",
@@ -15982,29 +16104,30 @@ export default function (pi: ExtensionAPI) {
15982
16104
  }
15983
16105
  const afterTranscript = after.transcript;
15984
16106
  const rawOutput = extractStudioReplTranscriptDelta(beforeTranscript, afterTranscript);
15985
- const output = cleanStudioReplCapturedOutput(rawOutput);
16107
+ const output = cleanStudioReplCapturedOutput(rawOutput, sent.display, sent.completionLine);
15986
16108
  updateStudioReplJournalEntryOutput(
15987
16109
  msg.requestId,
15988
16110
  msg.sessionName,
15989
16111
  output,
15990
16112
  sent.controlFiles?.doneFile ? (completed ? "captured" : "timeout") : (output.trim() ? "captured" : "sent"),
16113
+ currentSession,
15991
16114
  );
15992
- if (lease) {
15993
- await releaseOrRetainStudioReplSendLease(lease, submittedSession, submittedDoneFile);
15994
- lease = null;
15995
- }
15996
16115
  sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId });
15997
16116
  } catch (error) {
15998
16117
  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
- }
16118
+ if (journalEntry) updateStudioReplJournalEntryOutput(
16119
+ msg.requestId,
16120
+ msg.sessionName,
16121
+ message,
16122
+ "error",
16123
+ submittedSession || session,
16124
+ );
16004
16125
  sendToClient(client, { type: "error", requestId: msg.requestId, message });
16005
16126
  sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId, replError: message });
16006
16127
  } finally {
16007
- await lease?.release().catch(() => undefined);
16128
+ await releaseOrRetainStudioReplSubmission(lease, submittedSession, submittedControlFiles);
16129
+ lease = null;
16130
+ submittedControlFiles = undefined;
16008
16131
  }
16009
16132
  })();
16010
16133
  return;