pi-studio 0.9.11 → 0.9.12
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 +13 -0
- package/README.md +1 -1
- package/client/studio-client.js +554 -70
- package/client/studio.css +191 -32
- package/index.ts +322 -32
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { completeSimple, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
9
9
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
10
10
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -365,6 +365,7 @@ interface ReplStartRequestMessage {
|
|
|
365
365
|
requestId: string;
|
|
366
366
|
runtime: StudioReplRuntime;
|
|
367
367
|
newSession?: boolean;
|
|
368
|
+
command?: string;
|
|
368
369
|
}
|
|
369
370
|
|
|
370
371
|
interface ReplStopRequestMessage {
|
|
@@ -2364,11 +2365,69 @@ function buildStudioCompanionLabel(_label: string | undefined): string {
|
|
|
2364
2365
|
return "copy of editor text";
|
|
2365
2366
|
}
|
|
2366
2367
|
|
|
2368
|
+
const STUDIO_HTML_PREVIEW_RESOURCE_MAX_BYTES = 25 * 1024 * 1024;
|
|
2369
|
+
const STUDIO_HTML_PREVIEW_IMAGE_MIME_BY_EXT = new Map<string, string>([
|
|
2370
|
+
[".png", "image/png"],
|
|
2371
|
+
[".jpg", "image/jpeg"],
|
|
2372
|
+
[".jpeg", "image/jpeg"],
|
|
2373
|
+
[".gif", "image/gif"],
|
|
2374
|
+
[".webp", "image/webp"],
|
|
2375
|
+
]);
|
|
2376
|
+
|
|
2367
2377
|
function resolveStudioPdfResourcePath(pdfPath: string | undefined, sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
|
|
2368
2378
|
const baseDir = resolveStudioBaseDir(sourcePath, resourceDir, fallbackCwd);
|
|
2369
2379
|
return resolveStudioPdfResourceFile(pdfPath, baseDir);
|
|
2370
2380
|
}
|
|
2371
2381
|
|
|
2382
|
+
function stripStudioHtmlPreviewResourceUrlSuffix(resourcePath: string): string {
|
|
2383
|
+
const withoutHash = resourcePath.split("#")[0] ?? resourcePath;
|
|
2384
|
+
return withoutHash.split("?")[0] ?? withoutHash;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
function decodeStudioHtmlPreviewResourcePath(resourcePath: string): string {
|
|
2388
|
+
try {
|
|
2389
|
+
return decodeURIComponent(resourcePath);
|
|
2390
|
+
} catch {
|
|
2391
|
+
return resourcePath;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
function resolveStudioHtmlPreviewResourcePath(
|
|
2396
|
+
resourcePath: string | undefined,
|
|
2397
|
+
sourcePath: string | undefined,
|
|
2398
|
+
resourceDir: string | undefined,
|
|
2399
|
+
fallbackCwd: string,
|
|
2400
|
+
): { filePath: string; mimeType: string } {
|
|
2401
|
+
const rawPath = typeof resourcePath === "string" ? resourcePath.trim() : "";
|
|
2402
|
+
if (!rawPath) throw new Error("Missing HTML preview resource path.");
|
|
2403
|
+
if (/\0/.test(rawPath)) throw new Error("Invalid HTML preview resource path.");
|
|
2404
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(rawPath) && !/^[a-z]:[\\/]/i.test(rawPath)) {
|
|
2405
|
+
throw new Error("Only local HTML preview resources are supported.");
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
const baseDir = resolveStudioBaseDir(sourcePath, resourceDir, fallbackCwd);
|
|
2409
|
+
const cleanedPath = decodeStudioHtmlPreviewResourcePath(stripStudioHtmlPreviewResourceUrlSuffix(rawPath));
|
|
2410
|
+
const expandedPath = expandHome(cleanedPath);
|
|
2411
|
+
const candidate = isAbsolute(expandedPath) ? expandedPath : resolve(baseDir, expandedPath);
|
|
2412
|
+
const ext = extname(candidate).toLowerCase();
|
|
2413
|
+
const mimeType = STUDIO_HTML_PREVIEW_IMAGE_MIME_BY_EXT.get(ext);
|
|
2414
|
+
if (!mimeType) throw new Error("Only local PNG, JPEG, GIF, and WebP images can be embedded in HTML previews.");
|
|
2415
|
+
|
|
2416
|
+
const baseReal = realpathSync(baseDir);
|
|
2417
|
+
const candidateReal = realpathSync(candidate);
|
|
2418
|
+
const rel = relative(baseReal, candidateReal);
|
|
2419
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
2420
|
+
throw new Error("HTML preview resource path must stay within the current Studio resource directory.");
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
const stat = statSync(candidateReal);
|
|
2424
|
+
if (!stat.isFile()) throw new Error("HTML preview resource path does not refer to a file.");
|
|
2425
|
+
if (stat.size > STUDIO_HTML_PREVIEW_RESOURCE_MAX_BYTES) {
|
|
2426
|
+
throw new Error("HTML preview resource is too large to embed.");
|
|
2427
|
+
}
|
|
2428
|
+
return { filePath: candidateReal, mimeType };
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2372
2431
|
function resolveStudioPandocWorkingDir(baseDir: string | undefined): string | undefined {
|
|
2373
2432
|
const normalized = typeof baseDir === "string" ? baseDir.trim() : "";
|
|
2374
2433
|
if (!normalized) return undefined;
|
|
@@ -6279,6 +6338,23 @@ function respondPdfFile(req: IncomingMessage, res: ServerResponse, filePath: str
|
|
|
6279
6338
|
res.end(method === "HEAD" ? undefined : pdf);
|
|
6280
6339
|
}
|
|
6281
6340
|
|
|
6341
|
+
function respondHtmlPreviewResourceJson(req: IncomingMessage, res: ServerResponse, filePath: string, mimeType: string): void {
|
|
6342
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
6343
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
6344
|
+
res.setHeader("Allow", "GET, HEAD");
|
|
6345
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use GET." });
|
|
6346
|
+
return;
|
|
6347
|
+
}
|
|
6348
|
+
|
|
6349
|
+
const data = method === "HEAD" ? "" : readFileSync(filePath).toString("base64");
|
|
6350
|
+
respondJson(res, 200, {
|
|
6351
|
+
ok: true,
|
|
6352
|
+
mimeType,
|
|
6353
|
+
filename: basename(filePath),
|
|
6354
|
+
dataUrl: method === "HEAD" ? "" : `data:${mimeType};base64,${data}`,
|
|
6355
|
+
});
|
|
6356
|
+
}
|
|
6357
|
+
|
|
6282
6358
|
function openUrlInDefaultBrowser(url: string): Promise<void> {
|
|
6283
6359
|
const openCommand =
|
|
6284
6360
|
process.platform === "darwin"
|
|
@@ -7183,6 +7259,7 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
7183
7259
|
requestId: msg.requestId,
|
|
7184
7260
|
runtime,
|
|
7185
7261
|
newSession: Boolean(msg.newSession),
|
|
7262
|
+
command: typeof msg.command === "string" ? msg.command : undefined,
|
|
7186
7263
|
};
|
|
7187
7264
|
}
|
|
7188
7265
|
}
|
|
@@ -7326,37 +7403,184 @@ function isGenericToolActivityLabel(label: string | null | undefined): boolean {
|
|
|
7326
7403
|
|| normalized === "editing file";
|
|
7327
7404
|
}
|
|
7328
7405
|
|
|
7406
|
+
function splitStudioShellWords(segment: string): string[] {
|
|
7407
|
+
const words: string[] = [];
|
|
7408
|
+
let current = "";
|
|
7409
|
+
let quote: "'" | "\"" | null = null;
|
|
7410
|
+
let escaped = false;
|
|
7411
|
+
|
|
7412
|
+
for (const char of String(segment || "")) {
|
|
7413
|
+
if (escaped) {
|
|
7414
|
+
current += char;
|
|
7415
|
+
escaped = false;
|
|
7416
|
+
continue;
|
|
7417
|
+
}
|
|
7418
|
+
if (char === "\\" && quote !== "'") {
|
|
7419
|
+
escaped = true;
|
|
7420
|
+
continue;
|
|
7421
|
+
}
|
|
7422
|
+
if (quote) {
|
|
7423
|
+
if (char === quote) quote = null;
|
|
7424
|
+
else current += char;
|
|
7425
|
+
continue;
|
|
7426
|
+
}
|
|
7427
|
+
if (char === "'" || char === "\"") {
|
|
7428
|
+
quote = char;
|
|
7429
|
+
continue;
|
|
7430
|
+
}
|
|
7431
|
+
if (/\s/.test(char)) {
|
|
7432
|
+
if (current) {
|
|
7433
|
+
words.push(current);
|
|
7434
|
+
current = "";
|
|
7435
|
+
}
|
|
7436
|
+
continue;
|
|
7437
|
+
}
|
|
7438
|
+
current += char;
|
|
7439
|
+
}
|
|
7440
|
+
if (escaped) current += "\\";
|
|
7441
|
+
if (current) words.push(current);
|
|
7442
|
+
return words;
|
|
7443
|
+
}
|
|
7444
|
+
|
|
7445
|
+
function normalizeStudioShellCommandToken(token: string): string {
|
|
7446
|
+
let value = String(token || "").trim();
|
|
7447
|
+
if (!value) return "";
|
|
7448
|
+
const slashIndex = value.lastIndexOf("/");
|
|
7449
|
+
if (slashIndex >= 0) value = value.slice(slashIndex + 1);
|
|
7450
|
+
return value.toLowerCase();
|
|
7451
|
+
}
|
|
7452
|
+
|
|
7453
|
+
function isStudioShellAssignmentToken(token: string): boolean {
|
|
7454
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(String(token || ""));
|
|
7455
|
+
}
|
|
7456
|
+
|
|
7457
|
+
function getStudioShellSegmentCommand(segment: string): { name: string; words: string[]; commandIndex: number } | null {
|
|
7458
|
+
const words = splitStudioShellWords(segment);
|
|
7459
|
+
let index = 0;
|
|
7460
|
+
const skipAssignments = () => {
|
|
7461
|
+
while (index < words.length && isStudioShellAssignmentToken(words[index] || "")) index += 1;
|
|
7462
|
+
};
|
|
7463
|
+
|
|
7464
|
+
skipAssignments();
|
|
7465
|
+
let guard = 0;
|
|
7466
|
+
while (index < words.length && guard < 12) {
|
|
7467
|
+
guard += 1;
|
|
7468
|
+
const name = normalizeStudioShellCommandToken(words[index] || "");
|
|
7469
|
+
if (!name) {
|
|
7470
|
+
index += 1;
|
|
7471
|
+
continue;
|
|
7472
|
+
}
|
|
7473
|
+
if (name === "command" || name === "builtin" || name === "exec") {
|
|
7474
|
+
index += 1;
|
|
7475
|
+
skipAssignments();
|
|
7476
|
+
continue;
|
|
7477
|
+
}
|
|
7478
|
+
if (name === "time") {
|
|
7479
|
+
index += 1;
|
|
7480
|
+
while (index < words.length && String(words[index] || "").startsWith("-")) index += 1;
|
|
7481
|
+
skipAssignments();
|
|
7482
|
+
continue;
|
|
7483
|
+
}
|
|
7484
|
+
if (name === "env") {
|
|
7485
|
+
index += 1;
|
|
7486
|
+
while (index < words.length) {
|
|
7487
|
+
const token = String(words[index] || "");
|
|
7488
|
+
const lowerToken = token.toLowerCase();
|
|
7489
|
+
if (isStudioShellAssignmentToken(token)) {
|
|
7490
|
+
index += 1;
|
|
7491
|
+
continue;
|
|
7492
|
+
}
|
|
7493
|
+
if (lowerToken === "-u" || lowerToken === "--unset" || lowerToken === "-s" || lowerToken === "-S") {
|
|
7494
|
+
index += 2;
|
|
7495
|
+
continue;
|
|
7496
|
+
}
|
|
7497
|
+
if (lowerToken.startsWith("-")) {
|
|
7498
|
+
index += 1;
|
|
7499
|
+
continue;
|
|
7500
|
+
}
|
|
7501
|
+
break;
|
|
7502
|
+
}
|
|
7503
|
+
skipAssignments();
|
|
7504
|
+
continue;
|
|
7505
|
+
}
|
|
7506
|
+
if (name === "sudo") {
|
|
7507
|
+
index += 1;
|
|
7508
|
+
while (index < words.length) {
|
|
7509
|
+
const option = String(words[index] || "");
|
|
7510
|
+
if (!option.startsWith("-")) break;
|
|
7511
|
+
const lowerOption = option.toLowerCase();
|
|
7512
|
+
index += 1;
|
|
7513
|
+
if (["-c", "-g", "-h", "-p", "-t", "-u"].includes(lowerOption) && index < words.length) {
|
|
7514
|
+
index += 1;
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
skipAssignments();
|
|
7518
|
+
continue;
|
|
7519
|
+
}
|
|
7520
|
+
return { name, words, commandIndex: index };
|
|
7521
|
+
}
|
|
7522
|
+
return null;
|
|
7523
|
+
}
|
|
7524
|
+
|
|
7525
|
+
function getStudioGitSubcommand(args: string[]): string {
|
|
7526
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
7527
|
+
const token = String(args[index] || "").toLowerCase();
|
|
7528
|
+
if (!token) continue;
|
|
7529
|
+
if (["-c", "-C", "--git-dir", "--work-tree", "--namespace", "--exec-path"].map((value) => value.toLowerCase()).includes(token)) {
|
|
7530
|
+
index += 1;
|
|
7531
|
+
continue;
|
|
7532
|
+
}
|
|
7533
|
+
if (/^--(?:git-dir|work-tree|namespace|exec-path)=/.test(token)) continue;
|
|
7534
|
+
if (token.startsWith("-")) continue;
|
|
7535
|
+
return normalizeStudioShellCommandToken(token);
|
|
7536
|
+
}
|
|
7537
|
+
return "";
|
|
7538
|
+
}
|
|
7539
|
+
|
|
7329
7540
|
function deriveBashActivityLabel(command: string): string | null {
|
|
7330
7541
|
const normalized = String(command || "").trim();
|
|
7331
7542
|
if (!normalized) return null;
|
|
7332
|
-
const lower = normalized.toLowerCase();
|
|
7333
7543
|
|
|
7334
|
-
const segments =
|
|
7544
|
+
const segments = normalized
|
|
7335
7545
|
.split(/(?:&&|\|\||;|\n)+/g)
|
|
7336
7546
|
.map((segment) => segment.trim())
|
|
7337
7547
|
.filter((segment) => segment.length > 0);
|
|
7338
7548
|
|
|
7339
7549
|
let hasPwd = false;
|
|
7550
|
+
let hasLs = false;
|
|
7340
7551
|
let hasLsCurrent = false;
|
|
7341
7552
|
let hasLsParent = false;
|
|
7342
7553
|
let hasFind = false;
|
|
7343
7554
|
let hasFindCurrentListing = false;
|
|
7344
7555
|
let hasFindParentListing = false;
|
|
7556
|
+
let hasTextSearch = false;
|
|
7557
|
+
let hasFileRead = false;
|
|
7558
|
+
let hasGit = false;
|
|
7559
|
+
let hasGitStatus = false;
|
|
7560
|
+
let hasGitDiff = false;
|
|
7561
|
+
let hasNpm = false;
|
|
7562
|
+
let hasPython = false;
|
|
7563
|
+
let hasNode = false;
|
|
7345
7564
|
|
|
7346
7565
|
for (const segment of segments) {
|
|
7347
|
-
|
|
7566
|
+
const commandInfo = getStudioShellSegmentCommand(segment);
|
|
7567
|
+
if (!commandInfo) continue;
|
|
7568
|
+
const commandName = commandInfo.name;
|
|
7569
|
+
const args = commandInfo.words.slice(commandInfo.commandIndex + 1);
|
|
7348
7570
|
|
|
7349
|
-
if (
|
|
7350
|
-
|
|
7571
|
+
if (commandName === "pwd") hasPwd = true;
|
|
7572
|
+
|
|
7573
|
+
if (commandName === "ls") {
|
|
7574
|
+
hasLs = true;
|
|
7575
|
+
if (args.some((arg) => arg === ".." || arg === "../" || arg.startsWith("../"))) hasLsParent = true;
|
|
7351
7576
|
else hasLsCurrent = true;
|
|
7352
7577
|
}
|
|
7353
7578
|
|
|
7354
|
-
if (
|
|
7579
|
+
if (commandName === "find") {
|
|
7355
7580
|
hasFind = true;
|
|
7356
|
-
const
|
|
7357
|
-
const
|
|
7358
|
-
const
|
|
7359
|
-
const listingLike = /-maxdepth\s+\d+\b/.test(segment) && !hasSelector;
|
|
7581
|
+
const pathToken = args.find((arg) => arg && !String(arg).startsWith("-")) || "";
|
|
7582
|
+
const hasSelector = /-(?:name|iname|regex|path|ipath|newer|mtime|mmin|size|user|group)\b/i.test(segment);
|
|
7583
|
+
const listingLike = /-maxdepth\s+\d+\b/i.test(segment) && !hasSelector;
|
|
7360
7584
|
|
|
7361
7585
|
if (listingLike) {
|
|
7362
7586
|
if (pathToken === ".." || pathToken === "../") {
|
|
@@ -7366,6 +7590,18 @@ function deriveBashActivityLabel(command: string): string | null {
|
|
|
7366
7590
|
}
|
|
7367
7591
|
}
|
|
7368
7592
|
}
|
|
7593
|
+
|
|
7594
|
+
if (commandName === "rg" || commandName === "grep") hasTextSearch = true;
|
|
7595
|
+
if (commandName === "cat" || commandName === "sed" || commandName === "awk") hasFileRead = true;
|
|
7596
|
+
if (commandName === "git") {
|
|
7597
|
+
hasGit = true;
|
|
7598
|
+
const subcommand = getStudioGitSubcommand(args);
|
|
7599
|
+
if (subcommand === "status") hasGitStatus = true;
|
|
7600
|
+
if (subcommand === "diff") hasGitDiff = true;
|
|
7601
|
+
}
|
|
7602
|
+
if (commandName === "npm") hasNpm = true;
|
|
7603
|
+
if (/^python(?:3(?:\.\d+)?)?$/.test(commandName)) hasPython = true;
|
|
7604
|
+
if (commandName === "node") hasNode = true;
|
|
7369
7605
|
}
|
|
7370
7606
|
|
|
7371
7607
|
const hasCurrentListing = hasLsCurrent || hasFindCurrentListing;
|
|
@@ -7380,34 +7616,34 @@ function deriveBashActivityLabel(command: string): string | null {
|
|
|
7380
7616
|
if (hasParentListing) {
|
|
7381
7617
|
return "Listing parent directory files";
|
|
7382
7618
|
}
|
|
7383
|
-
if (hasCurrentListing ||
|
|
7619
|
+
if (hasCurrentListing || hasLs) {
|
|
7384
7620
|
return "Listing directory files";
|
|
7385
7621
|
}
|
|
7386
|
-
if (hasFind
|
|
7622
|
+
if (hasFind) {
|
|
7387
7623
|
return "Searching files";
|
|
7388
7624
|
}
|
|
7389
|
-
if (
|
|
7625
|
+
if (hasTextSearch) {
|
|
7390
7626
|
return "Searching text in files";
|
|
7391
7627
|
}
|
|
7392
|
-
if (
|
|
7628
|
+
if (hasFileRead) {
|
|
7393
7629
|
return "Reading file content";
|
|
7394
7630
|
}
|
|
7395
|
-
if (
|
|
7631
|
+
if (hasGitStatus) {
|
|
7396
7632
|
return "Checking git status";
|
|
7397
7633
|
}
|
|
7398
|
-
if (
|
|
7634
|
+
if (hasGitDiff) {
|
|
7399
7635
|
return "Reviewing git changes";
|
|
7400
7636
|
}
|
|
7401
|
-
if (
|
|
7637
|
+
if (hasGit) {
|
|
7402
7638
|
return "Running git command";
|
|
7403
7639
|
}
|
|
7404
|
-
if (
|
|
7640
|
+
if (hasNpm) {
|
|
7405
7641
|
return "Running npm command";
|
|
7406
7642
|
}
|
|
7407
|
-
if (
|
|
7643
|
+
if (hasPython) {
|
|
7408
7644
|
return "Running Python command";
|
|
7409
7645
|
}
|
|
7410
|
-
if (
|
|
7646
|
+
if (hasNode) {
|
|
7411
7647
|
return "Running Node.js command";
|
|
7412
7648
|
}
|
|
7413
7649
|
return "Running shell command";
|
|
@@ -7749,7 +7985,7 @@ function normalizeStudioReplRuntime(value: unknown): StudioReplRuntime | null {
|
|
|
7749
7985
|
return isStudioReplRuntime(normalized) ? normalized : null;
|
|
7750
7986
|
}
|
|
7751
7987
|
|
|
7752
|
-
function
|
|
7988
|
+
function getDefaultStudioReplRuntimeCommand(runtime: StudioReplRuntime): string {
|
|
7753
7989
|
if (runtime === "shell") return String(process.env.SHELL || "bash").trim() || "bash";
|
|
7754
7990
|
if (runtime === "python") return "python3";
|
|
7755
7991
|
if (runtime === "ipython") return "ipython";
|
|
@@ -7759,13 +7995,31 @@ function getStudioReplRuntimeCommand(runtime: StudioReplRuntime): string {
|
|
|
7759
7995
|
return "clojure";
|
|
7760
7996
|
}
|
|
7761
7997
|
|
|
7762
|
-
function
|
|
7763
|
-
|
|
7998
|
+
function normalizeStudioReplCommandOverride(runtime: StudioReplRuntime, command: string | undefined): string | undefined {
|
|
7999
|
+
const normalized = String(command ?? "").replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim();
|
|
8000
|
+
if (!normalized) return undefined;
|
|
8001
|
+
if (normalized.length > 240) return undefined;
|
|
8002
|
+
if (normalized === getDefaultStudioReplRuntimeCommand(runtime)) return undefined;
|
|
8003
|
+
return normalized;
|
|
8004
|
+
}
|
|
8005
|
+
|
|
8006
|
+
function getStudioReplRuntimeCommand(runtime: StudioReplRuntime, command?: string): string {
|
|
8007
|
+
return normalizeStudioReplCommandOverride(runtime, command) || getDefaultStudioReplRuntimeCommand(runtime);
|
|
8008
|
+
}
|
|
8009
|
+
|
|
8010
|
+
function getStudioReplCommandSessionSuffix(runtime: StudioReplRuntime, command?: string): string {
|
|
8011
|
+
const normalized = normalizeStudioReplCommandOverride(runtime, command);
|
|
8012
|
+
if (!normalized) return "";
|
|
8013
|
+
return `-${createHash("sha1").update(`${runtime}\n${normalized}`).digest("hex").slice(0, 8)}`;
|
|
8014
|
+
}
|
|
8015
|
+
|
|
8016
|
+
function getStudioReplSessionName(runtime: StudioReplRuntime, command?: string): string {
|
|
8017
|
+
return `pi-studio-repl-${runtime}${getStudioReplCommandSessionSuffix(runtime, command)}`;
|
|
7764
8018
|
}
|
|
7765
8019
|
|
|
7766
|
-
function getNewStudioReplSessionName(runtime: StudioReplRuntime): string {
|
|
8020
|
+
function getNewStudioReplSessionName(runtime: StudioReplRuntime, command?: string): string {
|
|
7767
8021
|
const suffix = `${Date.now().toString(36)}-${randomUUID().slice(0, 6)}`;
|
|
7768
|
-
return `pi-studio-repl-${runtime}-${suffix}`;
|
|
8022
|
+
return `pi-studio-repl-${runtime}${getStudioReplCommandSessionSuffix(runtime, command)}-${suffix}`;
|
|
7769
8023
|
}
|
|
7770
8024
|
|
|
7771
8025
|
function getStudioReplPaneTarget(sessionName: string): string {
|
|
@@ -7865,9 +8119,10 @@ function captureStudioReplSession(sessionName: string): { ok: true; transcript:
|
|
|
7865
8119
|
return { ok: true, transcript: String(result.stdout || "").replace(/[\t ]+$/gm, "").trimEnd(), session };
|
|
7866
8120
|
}
|
|
7867
8121
|
|
|
7868
|
-
function startStudioReplSession(runtime: StudioReplRuntime, cwd: string, options?: { newSession?: boolean }): { ok: true; session: StudioReplSessionInfo; message: string } | { ok: false; message: string } {
|
|
8122
|
+
function startStudioReplSession(runtime: StudioReplRuntime, cwd: string, options?: { newSession?: boolean; command?: string }): { ok: true; session: StudioReplSessionInfo; message: string } | { ok: false; message: string } {
|
|
7869
8123
|
if (!isTmuxAvailable()) return { ok: false, message: "tmux is not available. Install tmux to use Studio REPL sessions." };
|
|
7870
|
-
const
|
|
8124
|
+
const commandOverride = normalizeStudioReplCommandOverride(runtime, options?.command);
|
|
8125
|
+
const sessionName = options?.newSession ? getNewStudioReplSessionName(runtime, commandOverride) : getStudioReplSessionName(runtime, commandOverride);
|
|
7871
8126
|
const existing = runStudioTmux(["has-session", "-t", sessionName], { timeout: 3_000 });
|
|
7872
8127
|
if (existing.ok) {
|
|
7873
8128
|
const inferred = inferStudioReplSessionRuntime(sessionName);
|
|
@@ -7883,7 +8138,7 @@ function startStudioReplSession(runtime: StudioReplRuntime, cwd: string, options
|
|
|
7883
8138
|
message: `${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL is already running.`,
|
|
7884
8139
|
};
|
|
7885
8140
|
}
|
|
7886
|
-
const command = getStudioReplRuntimeCommand(runtime);
|
|
8141
|
+
const command = getStudioReplRuntimeCommand(runtime, commandOverride);
|
|
7887
8142
|
const result = runStudioTmux(["new-session", "-d", "-s", sessionName, "-c", cwd || process.cwd(), command], { timeout: 5_000 });
|
|
7888
8143
|
if (!result.ok) return { ok: false, message: result.message || `Failed to start ${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL.` };
|
|
7889
8144
|
return {
|
|
@@ -7895,7 +8150,7 @@ function startStudioReplSession(runtime: StudioReplRuntime, cwd: string, options
|
|
|
7895
8150
|
label: formatStudioReplSessionLabel(sessionName, runtime, "studio"),
|
|
7896
8151
|
source: "studio",
|
|
7897
8152
|
},
|
|
7898
|
-
message: `Started ${options?.newSession ? "new " : ""}${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL.`,
|
|
8153
|
+
message: `Started ${options?.newSession ? "new " : ""}${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL${commandOverride ? ` with custom command: ${commandOverride}` : ""}.`,
|
|
7899
8154
|
};
|
|
7900
8155
|
}
|
|
7901
8156
|
|
|
@@ -8937,6 +9192,8 @@ ${cssVarsBlock}
|
|
|
8937
9192
|
</div>
|
|
8938
9193
|
</section>
|
|
8939
9194
|
|
|
9195
|
+
<div id="paneResizeHandle" class="pane-resize-handle" role="separator" aria-label="Resize editor and response panes" aria-orientation="vertical" tabindex="0" title="Drag to resize panes. Double-click or press Enter/Space to reset; drag very close to the middle to snap to 50/50."></div>
|
|
9196
|
+
|
|
8940
9197
|
<section id="rightPane">
|
|
8941
9198
|
<div id="rightSectionHeader" class="section-header">
|
|
8942
9199
|
<div class="section-header-main">
|
|
@@ -9045,6 +9302,14 @@ ${cssVarsBlock}
|
|
|
9045
9302
|
<div><dt>Tab / Shift+Tab</dt><dd>Indent or unindent selected editor text</dd></div>
|
|
9046
9303
|
</dl>
|
|
9047
9304
|
</section>
|
|
9305
|
+
<section class="shortcuts-group">
|
|
9306
|
+
<h3>Response</h3>
|
|
9307
|
+
<dl>
|
|
9308
|
+
<div><dt>Alt/Option+←</dt><dd>Previous response when not editing text</dd></div>
|
|
9309
|
+
<div><dt>Alt/Option+→</dt><dd>Next response when not editing text</dd></div>
|
|
9310
|
+
<div><dt>Alt/Option+l</dt><dd>Latest response when not editing text</dd></div>
|
|
9311
|
+
</dl>
|
|
9312
|
+
</section>
|
|
9048
9313
|
<section class="shortcuts-group">
|
|
9049
9314
|
<h3>REPL</h3>
|
|
9050
9315
|
<dl>
|
|
@@ -9998,7 +10263,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
9998
10263
|
}
|
|
9999
10264
|
lastSpecificToolActivityLabel = baseLabel;
|
|
10000
10265
|
} else {
|
|
10001
|
-
|
|
10266
|
+
// Generic shell/tool labels such as "Running git command" are often
|
|
10267
|
+
// stale or too broad once the model has moved on. Keep the precise
|
|
10268
|
+
// Working trace entry, but do not promote generic labels into the
|
|
10269
|
+
// live Studio/terminal status line.
|
|
10270
|
+
nextLabel = null;
|
|
10002
10271
|
}
|
|
10003
10272
|
} else {
|
|
10004
10273
|
nextLabel = null;
|
|
@@ -10759,7 +11028,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
10759
11028
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
10760
11029
|
return;
|
|
10761
11030
|
}
|
|
10762
|
-
const started = startStudioReplSession(msg.runtime, studioCwd, { newSession: msg.newSession });
|
|
11031
|
+
const started = startStudioReplSession(msg.runtime, studioCwd, { newSession: msg.newSession, command: msg.command });
|
|
10763
11032
|
if (!started.ok) {
|
|
10764
11033
|
sendReplStateToClient(client, { requestId: msg.requestId, replError: started.message });
|
|
10765
11034
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: started.message });
|
|
@@ -12112,6 +12381,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
12112
12381
|
return;
|
|
12113
12382
|
}
|
|
12114
12383
|
|
|
12384
|
+
if (requestUrl.pathname === "/html-preview-resource") {
|
|
12385
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
12386
|
+
if (token !== serverState.token) {
|
|
12387
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
12388
|
+
return;
|
|
12389
|
+
}
|
|
12390
|
+
|
|
12391
|
+
try {
|
|
12392
|
+
const resource = resolveStudioHtmlPreviewResourcePath(
|
|
12393
|
+
requestUrl.searchParams.get("path") ?? "",
|
|
12394
|
+
requestUrl.searchParams.get("sourcePath") ?? undefined,
|
|
12395
|
+
requestUrl.searchParams.get("resourceDir") ?? undefined,
|
|
12396
|
+
studioCwd,
|
|
12397
|
+
);
|
|
12398
|
+
respondHtmlPreviewResourceJson(req, res, resource.filePath, resource.mimeType);
|
|
12399
|
+
} catch (error) {
|
|
12400
|
+
respondJson(res, 404, { ok: false, error: `HTML preview resource unavailable: ${error instanceof Error ? error.message : String(error)}` });
|
|
12401
|
+
}
|
|
12402
|
+
return;
|
|
12403
|
+
}
|
|
12404
|
+
|
|
12115
12405
|
if (requestUrl.pathname !== "/") {
|
|
12116
12406
|
respondText(res, 404, "Not found");
|
|
12117
12407
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.12",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|