pi-landstrip 0.15.6 → 0.15.7
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 +103 -167
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
BashToolInput,
|
|
11
11
|
ExtensionAPI,
|
|
12
12
|
ExtensionContext,
|
|
13
|
+
Theme,
|
|
13
14
|
} from '@earendil-works/pi-coding-agent';
|
|
14
15
|
|
|
15
16
|
import { binaryPath } from '@landstrip/landstrip';
|
|
@@ -95,6 +96,7 @@ type LandstripTrap =
|
|
|
95
96
|
interface LandstripBashCallbacks {
|
|
96
97
|
onStderr?: (data: Buffer) => void;
|
|
97
98
|
onErrorFd?: (data: Buffer) => void;
|
|
99
|
+
promptOnBlock?: boolean;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
const LANDSTRIP_VERSION = [0, 15, 9] as const;
|
|
@@ -362,7 +364,9 @@ function matchesPattern(filePath: string, patterns: string[]): boolean {
|
|
|
362
364
|
const absPattern = pattern.includes('*') ? expandPath(pattern) : canonicalizePath(pattern);
|
|
363
365
|
|
|
364
366
|
if (pattern.includes('*')) {
|
|
365
|
-
const escaped = absPattern
|
|
367
|
+
const escaped = absPattern
|
|
368
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
369
|
+
.replace(/\*\*\/|\*\*|\*/g, (token) => (token === '**/' ? '(?:.*/)?' : '.*'));
|
|
366
370
|
return new RegExp(`^${escaped}$`).test(abs);
|
|
367
371
|
}
|
|
368
372
|
|
|
@@ -438,11 +442,6 @@ function extractNativeWriteDeniedPath(output: string, cwd: string): string | nul
|
|
|
438
442
|
);
|
|
439
443
|
if (match) return normalizePathMatch(match[1], cwd);
|
|
440
444
|
|
|
441
|
-
match = output.match(
|
|
442
|
-
/(?:\/bin\/bash|bash|sh): (?:line \d+: )?([^:\n]+): (?:Operation not permitted|Permission denied)/,
|
|
443
|
-
);
|
|
444
|
-
if (match) return normalizePathMatch(match[1], cwd);
|
|
445
|
-
|
|
446
445
|
match = output.match(
|
|
447
446
|
/^[a-zA-Z0-9_-]+: cannot create(?: directory)? '?([^'\n]+?)'?(?: for writing)?: (?:Operation not permitted|Permission denied)$/m,
|
|
448
447
|
);
|
|
@@ -580,6 +579,25 @@ function setTuiStatus(ctx: ExtensionContext, key: string, value: string | undefi
|
|
|
580
579
|
ctx.ui.setStatus(key, value);
|
|
581
580
|
}
|
|
582
581
|
|
|
582
|
+
function boxTop(theme: Theme, width: number, title: string): string {
|
|
583
|
+
const label = theme.fg('accent', ` ${title} `);
|
|
584
|
+
const fill = theme.fg('border', '─'.repeat(Math.max(0, width - 4 - visibleWidth(label))));
|
|
585
|
+
return `${theme.fg('border', '╭─')}${label}${fill}${theme.fg('border', '─╮')}`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function boxRow(theme: Theme, width: number, content = ''): string {
|
|
589
|
+
const innerW = Math.max(1, width - 4);
|
|
590
|
+
const border = theme.fg('border', '│');
|
|
591
|
+
const line = truncateToWidth(content, innerW);
|
|
592
|
+
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
593
|
+
return `${border} ${line}${' '.repeat(pad)} ${border}`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function boxBottom(theme: Theme, width: number): string {
|
|
597
|
+
const border = (s: string) => theme.fg('border', s);
|
|
598
|
+
return `${border('╰')}${border('─'.repeat(Math.max(0, width - 2)))}${border('╯')}`;
|
|
599
|
+
}
|
|
600
|
+
|
|
583
601
|
async function showPermissionPrompt(
|
|
584
602
|
ctx: ExtensionContext,
|
|
585
603
|
title: string,
|
|
@@ -598,28 +616,14 @@ async function showPermissionPrompt(
|
|
|
598
616
|
|
|
599
617
|
return {
|
|
600
618
|
render(width: number): string[] {
|
|
601
|
-
const innerW = width - 4;
|
|
619
|
+
const innerW = Math.max(1, width - 4);
|
|
602
620
|
const lines: string[] = [];
|
|
603
|
-
const border = theme.fg('border', '│');
|
|
604
621
|
const dim = (s: string) => theme.fg('dim', s);
|
|
605
|
-
const borderFg = (s: string) => theme.fg('border', s);
|
|
606
|
-
|
|
607
|
-
// Top border with title
|
|
608
|
-
const label = ' Sandbox ';
|
|
609
|
-
const topLeft = borderFg('╭─');
|
|
610
|
-
const topRight = borderFg('─╮');
|
|
611
|
-
const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(label))));
|
|
612
|
-
lines.push(`${topLeft}${theme.fg('accent', label)}${topFill}${topRight}`);
|
|
613
|
-
|
|
614
|
-
// Blank spacing
|
|
615
|
-
lines.push(`${border} ${' '.repeat(innerW)} ${border}`);
|
|
616
622
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
lines.push(
|
|
621
|
-
|
|
622
|
-
lines.push(`${border} ${' '.repeat(innerW)} ${border}`);
|
|
623
|
+
lines.push(boxTop(theme, width, 'Sandbox'));
|
|
624
|
+
lines.push(boxRow(theme, width));
|
|
625
|
+
lines.push(boxRow(theme, width, theme.fg('warning', title)));
|
|
626
|
+
lines.push(boxRow(theme, width));
|
|
623
627
|
|
|
624
628
|
// Options
|
|
625
629
|
for (let i = 0; i < options.length; i++) {
|
|
@@ -629,11 +633,11 @@ async function showPermissionPrompt(
|
|
|
629
633
|
|
|
630
634
|
// Section divider before the permanent options (index 2 and 3)
|
|
631
635
|
if (i === 2) {
|
|
632
|
-
lines.push(
|
|
636
|
+
lines.push(boxRow(theme, width));
|
|
633
637
|
const secLabel = ' Permanent ';
|
|
634
638
|
const secDash = '─'.repeat(Math.max(0, innerW - visibleWidth(secLabel)));
|
|
635
|
-
lines.push(
|
|
636
|
-
lines.push(
|
|
639
|
+
lines.push(boxRow(theme, width, dim(secDash + secLabel)));
|
|
640
|
+
lines.push(boxRow(theme, width));
|
|
637
641
|
}
|
|
638
642
|
|
|
639
643
|
// Key badge
|
|
@@ -668,25 +672,16 @@ async function showPermissionPrompt(
|
|
|
668
672
|
}
|
|
669
673
|
|
|
670
674
|
const fullLine = ` ${cursor} ${keyBadge} ${label}${hint}`;
|
|
671
|
-
|
|
672
|
-
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
673
|
-
lines.push(`${border} ${line}${' '.repeat(pad)} ${border}`);
|
|
675
|
+
lines.push(boxRow(theme, width, fullLine));
|
|
674
676
|
}
|
|
675
677
|
|
|
676
678
|
// Footer
|
|
677
|
-
lines.push(
|
|
679
|
+
lines.push(boxRow(theme, width));
|
|
678
680
|
const footerText = pendingAction
|
|
679
681
|
? '↑↓ navigate enter confirm esc cancel'
|
|
680
682
|
: '↑↓ navigate enter select esc dismiss';
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
lines.push(`${border} ${footerLine}${' '.repeat(footerPad)} ${border}`);
|
|
684
|
-
|
|
685
|
-
// Bottom border
|
|
686
|
-
const botLeft = borderFg('╰');
|
|
687
|
-
const botRight = borderFg('╯');
|
|
688
|
-
const botFill = borderFg('─'.repeat(width - 2));
|
|
689
|
-
lines.push(`${botLeft}${botFill}${botRight}`);
|
|
683
|
+
lines.push(boxRow(theme, width, dim(footerText)));
|
|
684
|
+
lines.push(boxBottom(theme, width));
|
|
690
685
|
|
|
691
686
|
return lines;
|
|
692
687
|
},
|
|
@@ -896,18 +891,15 @@ export function createLandstripIntegration(
|
|
|
896
891
|
sessionAllowedWritePaths.length = 0;
|
|
897
892
|
}
|
|
898
893
|
|
|
899
|
-
function getEffectiveAllowedDomains(
|
|
900
|
-
const config = loadConfig(cwd);
|
|
894
|
+
function getEffectiveAllowedDomains(config: SandboxConfig): string[] {
|
|
901
895
|
return [...config.network.allowedDomains, ...sessionAllowedDomains];
|
|
902
896
|
}
|
|
903
897
|
|
|
904
|
-
function getEffectiveAllowRead(
|
|
905
|
-
const config = loadConfig(cwd);
|
|
898
|
+
function getEffectiveAllowRead(config: SandboxConfig): string[] {
|
|
906
899
|
return [...config.filesystem.allowRead, ...sessionAllowedReadPaths];
|
|
907
900
|
}
|
|
908
901
|
|
|
909
|
-
function getEffectiveAllowWrite(
|
|
910
|
-
const config = loadConfig(cwd);
|
|
902
|
+
function getEffectiveAllowWrite(config: SandboxConfig): string[] {
|
|
911
903
|
return [...config.filesystem.allowWrite, ...sessionAllowedWritePaths];
|
|
912
904
|
}
|
|
913
905
|
|
|
@@ -952,7 +944,7 @@ export function createLandstripIntegration(
|
|
|
952
944
|
const config = loadConfig(cwd);
|
|
953
945
|
|
|
954
946
|
if (domainMatchesAny(domain, config.network.deniedDomains)) return false;
|
|
955
|
-
if (domainMatchesAny(domain, getEffectiveAllowedDomains(
|
|
947
|
+
if (domainMatchesAny(domain, getEffectiveAllowedDomains(config))) return true;
|
|
956
948
|
|
|
957
949
|
const choice = await promptDomainBlock(ctx, domain);
|
|
958
950
|
if (choice === 'abort') return false;
|
|
@@ -974,8 +966,8 @@ export function createLandstripIntegration(
|
|
|
974
966
|
},
|
|
975
967
|
filesystem: {
|
|
976
968
|
denyRead: config.filesystem.denyRead,
|
|
977
|
-
allowRead: getEffectiveAllowRead(
|
|
978
|
-
allowWrite: getEffectiveAllowWrite(
|
|
969
|
+
allowRead: getEffectiveAllowRead(config),
|
|
970
|
+
allowWrite: getEffectiveAllowWrite(config),
|
|
979
971
|
denyWrite: config.filesystem.denyWrite,
|
|
980
972
|
},
|
|
981
973
|
};
|
|
@@ -1219,7 +1211,6 @@ export function createLandstripIntegration(
|
|
|
1219
1211
|
}
|
|
1220
1212
|
|
|
1221
1213
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1222
|
-
const resolvedQueryIds = new Set<number>();
|
|
1223
1214
|
let stderrAcc = '';
|
|
1224
1215
|
let errorFdAcc = '';
|
|
1225
1216
|
|
|
@@ -1232,109 +1223,70 @@ export function createLandstripIntegration(
|
|
|
1232
1223
|
trapSocket.on('data', (data: Buffer) => {
|
|
1233
1224
|
errorFdAcc += data.toString('utf8');
|
|
1234
1225
|
callbacks.onErrorFd?.(data);
|
|
1235
|
-
// Process query traps in real-time.
|
|
1236
|
-
if (ctx.hasUI) {
|
|
1237
|
-
const traps = parseLandstripTraps(errorFdAcc);
|
|
1238
|
-
for (const trap of traps) {
|
|
1239
|
-
if (
|
|
1240
|
-
trap.kind === 'filesystem' &&
|
|
1241
|
-
(trap.operation === 'write' || trap.operation === 'read') &&
|
|
1242
|
-
'state' in trap &&
|
|
1243
|
-
(trap as any).state === 'query' &&
|
|
1244
|
-
'query_id' in trap
|
|
1245
|
-
) {
|
|
1246
|
-
const queryId = (trap as any).query_id as number;
|
|
1247
|
-
if (!resolvedQueryIds.has(queryId)) {
|
|
1248
|
-
resolvedQueryIds.add(queryId);
|
|
1249
|
-
handleFsQuery(trapSocket, queryId, trap.operation, trap.file, cwd, ctx).catch(
|
|
1250
|
-
() => {},
|
|
1251
|
-
);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
1226
|
});
|
|
1257
1227
|
|
|
1258
|
-
async function handleFsQuery(
|
|
1259
|
-
socket: NetSocket,
|
|
1260
|
-
queryId: number,
|
|
1261
|
-
operation: 'read' | 'write',
|
|
1262
|
-
file: string,
|
|
1263
|
-
cwd: string,
|
|
1264
|
-
ctx: ExtensionContext,
|
|
1265
|
-
): Promise<void> {
|
|
1266
|
-
const choice =
|
|
1267
|
-
operation === 'read'
|
|
1268
|
-
? await promptReadBlock(ctx, file)
|
|
1269
|
-
: await promptWriteBlock(ctx, file);
|
|
1270
|
-
if (choice !== 'abort') {
|
|
1271
|
-
if (operation === 'read') await applyReadChoice(choice, file, cwd);
|
|
1272
|
-
else await applyWriteChoice(choice, file, cwd);
|
|
1273
|
-
}
|
|
1274
|
-
const action = choice === 'abort' ? 'deny' : 'allow';
|
|
1275
|
-
const response = JSON.stringify({ query_id: queryId, action }) + '\n';
|
|
1276
|
-
if (!socket.destroyed) {
|
|
1277
|
-
socket.write(response);
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
1228
|
child.on('error', (error) => {
|
|
1282
1229
|
cleanup();
|
|
1283
1230
|
reject(error);
|
|
1284
1231
|
});
|
|
1285
1232
|
|
|
1286
|
-
child.on('close',
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
const errorOutput = errorFdAcc || stderrAcc;
|
|
1298
|
-
|
|
1299
|
-
const blockedPath =
|
|
1300
|
-
extractBlockedPath(errorOutput, cwd) ??
|
|
1301
|
-
(errorFdAcc ? extractBlockedPath(stderrAcc, cwd) : null);
|
|
1302
|
-
const blockedWritePath =
|
|
1303
|
-
extractBlockedWritePath(errorOutput, cwd) ??
|
|
1304
|
-
(errorFdAcc ? extractBlockedWritePath(stderrAcc, cwd) : null);
|
|
1305
|
-
if (blockedPath && ctx.hasUI) {
|
|
1306
|
-
const config = loadConfig(cwd);
|
|
1307
|
-
const isDeniedByDenyRead = matchesPattern(blockedPath, config.filesystem.denyRead);
|
|
1308
|
-
const isReadAllowed = matchesPattern(blockedPath, getEffectiveAllowRead(cwd));
|
|
1309
|
-
const isWriteAllowed = !shouldPromptForWrite(
|
|
1310
|
-
blockedPath,
|
|
1311
|
-
getEffectiveAllowWrite(cwd),
|
|
1312
|
-
matchesPattern,
|
|
1313
|
-
);
|
|
1233
|
+
child.on('close', (code) => {
|
|
1234
|
+
void (async () => {
|
|
1235
|
+
cleanup();
|
|
1236
|
+
if (signal?.aborted) {
|
|
1237
|
+
reject(new Error('aborted'));
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (timedOut) {
|
|
1241
|
+
reject(new Error(`timeout:${timeout}`));
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1314
1244
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1245
|
+
const errorOutput = errorFdAcc || stderrAcc;
|
|
1246
|
+
|
|
1247
|
+
const blockedPath =
|
|
1248
|
+
extractBlockedPath(errorOutput, cwd) ??
|
|
1249
|
+
(errorFdAcc ? extractBlockedPath(stderrAcc, cwd) : null);
|
|
1250
|
+
const blockedWritePath =
|
|
1251
|
+
extractBlockedWritePath(errorOutput, cwd) ??
|
|
1252
|
+
(errorFdAcc ? extractBlockedWritePath(stderrAcc, cwd) : null);
|
|
1253
|
+
if (blockedPath && ctx.hasUI && callbacks.promptOnBlock) {
|
|
1254
|
+
const config = loadConfig(cwd);
|
|
1255
|
+
const isDeniedByDenyRead = matchesPattern(
|
|
1321
1256
|
blockedPath,
|
|
1322
|
-
|
|
1257
|
+
config.filesystem.denyRead,
|
|
1323
1258
|
);
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1259
|
+
const isReadAllowed = matchesPattern(blockedPath, getEffectiveAllowRead(config));
|
|
1260
|
+
const isWriteAllowed = !shouldPromptForWrite(
|
|
1261
|
+
blockedPath,
|
|
1262
|
+
getEffectiveAllowWrite(config),
|
|
1263
|
+
matchesPattern,
|
|
1264
|
+
);
|
|
1265
|
+
|
|
1266
|
+
if (blockedWritePath === blockedPath && !isWriteAllowed) {
|
|
1267
|
+
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1268
|
+
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1269
|
+
} else if (isDeniedByDenyRead || !isReadAllowed) {
|
|
1270
|
+
const choice = await promptReadBlock(
|
|
1271
|
+
ctx,
|
|
1272
|
+
blockedPath,
|
|
1273
|
+
isDeniedByDenyRead ? 'granting allowRead will override it' : undefined,
|
|
1274
|
+
);
|
|
1275
|
+
if (choice !== 'abort') await applyReadChoice(choice, blockedPath, cwd);
|
|
1276
|
+
} else if (!isWriteAllowed) {
|
|
1277
|
+
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1278
|
+
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1279
|
+
}
|
|
1280
|
+
} else if (!blockedPath && ctx.hasUI) {
|
|
1281
|
+
const landstripErrors = parseLandstripTraps(errorOutput);
|
|
1282
|
+
if (landstripErrors.length > 0) {
|
|
1283
|
+
const formatted = formatLandstripTraps(landstripErrors);
|
|
1284
|
+
notify(ctx, `Sandbox blocked an operation: ${formatted}`, 'warning');
|
|
1285
|
+
}
|
|
1334
1286
|
}
|
|
1335
|
-
}
|
|
1336
1287
|
|
|
1337
|
-
|
|
1288
|
+
resolvePromise({ exitCode: code });
|
|
1289
|
+
})().catch(reject);
|
|
1338
1290
|
});
|
|
1339
1291
|
})().catch(reject);
|
|
1340
1292
|
});
|
|
@@ -1380,7 +1332,7 @@ export function createLandstripIntegration(
|
|
|
1380
1332
|
return null;
|
|
1381
1333
|
}
|
|
1382
1334
|
|
|
1383
|
-
if (shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(
|
|
1335
|
+
if (shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(config), matchesPattern)) {
|
|
1384
1336
|
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1385
1337
|
if (choice === 'abort') return null;
|
|
1386
1338
|
await applyWriteChoice(choice, blockedPath, ctx.cwd);
|
|
@@ -1412,8 +1364,8 @@ export function createLandstripIntegration(
|
|
|
1412
1364
|
): Promise<AgentToolResult<BashToolDetails | undefined> | null> => {
|
|
1413
1365
|
if (!ctx.hasUI) return null;
|
|
1414
1366
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1367
|
+
const config = loadConfig(ctx.cwd);
|
|
1368
|
+
if (!matchesPattern(blockedPath, getEffectiveAllowRead(config))) {
|
|
1417
1369
|
const choice = await promptReadBlock(
|
|
1418
1370
|
ctx,
|
|
1419
1371
|
blockedPath,
|
|
@@ -1648,7 +1600,7 @@ export function createLandstripIntegration(
|
|
|
1648
1600
|
}
|
|
1649
1601
|
}
|
|
1650
1602
|
|
|
1651
|
-
return { operations: createLandstripBashOps(ctx) };
|
|
1603
|
+
return { operations: createLandstripBashOps(ctx, { promptOnBlock: true }) };
|
|
1652
1604
|
});
|
|
1653
1605
|
|
|
1654
1606
|
pi.on('tool_call', async (event, ctx) => {
|
|
@@ -1672,7 +1624,7 @@ export function createLandstripIntegration(
|
|
|
1672
1624
|
|
|
1673
1625
|
if (isToolCallEventType('read', event)) {
|
|
1674
1626
|
const filePath = canonicalizePath(event.input.path);
|
|
1675
|
-
if (!matchesPattern(filePath, getEffectiveAllowRead(
|
|
1627
|
+
if (!matchesPattern(filePath, getEffectiveAllowRead(config))) {
|
|
1676
1628
|
const choice = await promptReadBlock(ctx, filePath);
|
|
1677
1629
|
if (choice === 'abort') {
|
|
1678
1630
|
return {
|
|
@@ -1696,7 +1648,7 @@ export function createLandstripIntegration(
|
|
|
1696
1648
|
};
|
|
1697
1649
|
}
|
|
1698
1650
|
|
|
1699
|
-
if (shouldPromptForWrite(filePath, getEffectiveAllowWrite(
|
|
1651
|
+
if (shouldPromptForWrite(filePath, getEffectiveAllowWrite(config), matchesPattern)) {
|
|
1700
1652
|
const choice = await promptWriteBlock(ctx, filePath);
|
|
1701
1653
|
if (choice === 'abort') {
|
|
1702
1654
|
return {
|
|
@@ -1742,7 +1694,6 @@ export function createLandstripIntegration(
|
|
|
1742
1694
|
const muted = (s: string) => theme.fg('muted', s);
|
|
1743
1695
|
const accent = (s: string) => theme.fg('accent', s);
|
|
1744
1696
|
const text = (s: string) => theme.fg('text', s);
|
|
1745
|
-
const borderFg = (s: string) => theme.fg('border', s);
|
|
1746
1697
|
|
|
1747
1698
|
function sandboxStatus(): { color: 'success' | 'warning'; label: string } {
|
|
1748
1699
|
if (noSandboxFlag) return { color: 'warning', label: 'Disabled (--no-sandbox)' };
|
|
@@ -1755,29 +1706,16 @@ export function createLandstripIntegration(
|
|
|
1755
1706
|
return v ? theme.fg('warning', 'yes') : theme.fg('success', 'no');
|
|
1756
1707
|
}
|
|
1757
1708
|
|
|
1758
|
-
function makeRow(content: string, innerW: number, border: string): string {
|
|
1759
|
-
const line = truncateToWidth(content, innerW);
|
|
1760
|
-
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
1761
|
-
return `${border} ${line}${' '.repeat(pad)} ${border}`;
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
1709
|
return {
|
|
1765
1710
|
render(width: number): string[] {
|
|
1766
1711
|
const innerW = Math.max(1, width - 4);
|
|
1767
|
-
const
|
|
1768
|
-
const row = (content: string) => makeRow(content, innerW, border);
|
|
1712
|
+
const row = (content = '') => boxRow(theme, width, content);
|
|
1769
1713
|
const lines: string[] = [];
|
|
1770
1714
|
const status = sandboxStatus();
|
|
1771
1715
|
const toggleValue = config.enabled
|
|
1772
1716
|
? theme.fg('success', 'enabled')
|
|
1773
1717
|
: theme.fg('warning', 'disabled');
|
|
1774
1718
|
|
|
1775
|
-
function topBorder(titleText: string): string {
|
|
1776
|
-
const title = accent(` ${titleText} `);
|
|
1777
|
-
const fill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
1778
|
-
return `${borderFg('╭─')}${title}${fill}${borderFg('─╮')}`;
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
1719
|
function section(titleText: string, detail?: string): void {
|
|
1782
1720
|
lines.push(row(''));
|
|
1783
1721
|
lines.push(row(`${accent(titleText)}${detail ? dim(` · ${detail}`) : ''}`));
|
|
@@ -1792,7 +1730,7 @@ export function createLandstripIntegration(
|
|
|
1792
1730
|
return text(truncateToWidth(value, Math.max(10, maxWidth)));
|
|
1793
1731
|
}
|
|
1794
1732
|
|
|
1795
|
-
lines.push(
|
|
1733
|
+
lines.push(boxTop(theme, width, 'Sandbox'));
|
|
1796
1734
|
|
|
1797
1735
|
const statusDot = theme.fg(status.color, '●');
|
|
1798
1736
|
const pathSnippet = text(truncateToWidth(binaryPath(), Math.max(20, innerW - 28)));
|
|
@@ -1834,9 +1772,7 @@ export function createLandstripIntegration(
|
|
|
1834
1772
|
`${dim('t')} ${muted('toggle persisted setting')} ${dim('esc')} ${muted('close')}`,
|
|
1835
1773
|
),
|
|
1836
1774
|
);
|
|
1837
|
-
lines.push(
|
|
1838
|
-
`${borderFg('╰')}${borderFg('─'.repeat(Math.max(0, width - 2)))}${borderFg('╯')}`,
|
|
1839
|
-
);
|
|
1775
|
+
lines.push(boxBottom(theme, width));
|
|
1840
1776
|
|
|
1841
1777
|
return lines;
|
|
1842
1778
|
},
|