@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.1
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 +136 -0
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/dry-balance-cli.d.ts +15 -1
- package/dist/types/cli/gallery-cli.d.ts +43 -0
- package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
- package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
- package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
- package/dist/types/cli/gallery-screenshot.d.ts +35 -0
- package/dist/types/commands/gallery.d.ts +47 -0
- package/dist/types/commit/analysis/conventional.d.ts +2 -2
- package/dist/types/commit/analysis/summary.d.ts +2 -2
- package/dist/types/commit/changelog/generate.d.ts +2 -2
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/index.d.ts +3 -3
- package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
- package/dist/types/commit/model-selection.d.ts +10 -4
- package/dist/types/config/api-key-resolver.d.ts +34 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-id-affixes.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +25 -2
- package/dist/types/config/settings-schema.d.ts +41 -6
- package/dist/types/dap/config.d.ts +14 -1
- package/dist/types/dap/types.d.ts +10 -0
- package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
- package/dist/types/lsp/types.d.ts +10 -0
- package/dist/types/lsp/utils.d.ts +3 -2
- package/dist/types/main.d.ts +3 -2
- package/dist/types/memory-backend/index.d.ts +2 -1
- package/dist/types/memory-backend/resolve.d.ts +1 -1
- package/dist/types/memory-backend/types.d.ts +1 -1
- package/dist/types/modes/components/chat-block.d.ts +64 -0
- package/dist/types/modes/components/custom-editor.d.ts +5 -1
- package/dist/types/modes/components/overlay-box.d.ts +17 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
- package/dist/types/modes/components/plan-toc.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +18 -0
- package/dist/types/modes/components/transcript-container.d.ts +11 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
- package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
- package/dist/types/modes/index.d.ts +5 -4
- package/dist/types/modes/interactive-mode.d.ts +16 -6
- package/dist/types/modes/setup-version.d.ts +11 -0
- package/dist/types/modes/setup-wizard/index.d.ts +2 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +19 -6
- package/dist/types/modes/utils/copy-targets.d.ts +21 -1
- package/dist/types/plan-mode/approved-plan.d.ts +27 -8
- package/dist/types/plan-mode/plan-protection.d.ts +4 -4
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/session/messages.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/slash-commands/types.d.ts +4 -6
- package/dist/types/task/executor.d.ts +14 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/render.d.ts +3 -2
- package/dist/types/telemetry-export.d.ts +1 -1
- package/dist/types/tools/archive-reader.d.ts +5 -0
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/eval-render.d.ts +1 -8
- package/dist/types/tools/fetch.d.ts +15 -7
- package/dist/types/tools/find.d.ts +8 -4
- package/dist/types/tools/grouped-file-output.d.ts +95 -12
- package/dist/types/tools/memory-render.d.ts +4 -1
- package/dist/types/tools/plan-mode-guard.d.ts +8 -9
- package/dist/types/tools/render-utils.d.ts +13 -9
- package/dist/types/tools/renderers.d.ts +16 -2
- package/dist/types/tools/search.d.ts +5 -1
- package/dist/types/tools/sqlite-reader.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +3 -2
- package/dist/types/tools/write.d.ts +5 -0
- package/dist/types/tui/output-block.d.ts +16 -4
- package/dist/types/tui/status-line.d.ts +3 -0
- package/dist/types/utils/enhanced-paste.d.ts +20 -0
- package/dist/types/web/scrapers/github.d.ts +22 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -1
- package/dist/types/web/search/providers/perplexity.d.ts +8 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +9 -9
- package/scripts/dev-launch +42 -0
- package/scripts/dev-launch-preload.ts +19 -0
- package/src/auto-thinking/classifier.ts +5 -1
- package/src/cli/args.ts +2 -2
- package/src/cli/dry-balance-cli.ts +52 -17
- package/src/cli/gallery-cli.ts +226 -0
- package/src/cli/gallery-fixtures/agentic.ts +292 -0
- package/src/cli/gallery-fixtures/codeintel.ts +188 -0
- package/src/cli/gallery-fixtures/edit.ts +194 -0
- package/src/cli/gallery-fixtures/fs.ts +153 -0
- package/src/cli/gallery-fixtures/index.ts +40 -0
- package/src/cli/gallery-fixtures/interaction.ts +49 -0
- package/src/cli/gallery-fixtures/memory.ts +81 -0
- package/src/cli/gallery-fixtures/misc.ts +250 -0
- package/src/cli/gallery-fixtures/search.ts +213 -0
- package/src/cli/gallery-fixtures/shell.ts +167 -0
- package/src/cli/gallery-fixtures/types.ts +41 -0
- package/src/cli/gallery-fixtures/web.ts +158 -0
- package/src/cli/gallery-screenshot.ts +279 -0
- package/src/cli-commands.ts +1 -0
- package/src/commands/gallery.ts +52 -0
- package/src/commands/launch.ts +1 -1
- package/src/commit/analysis/conventional.ts +2 -2
- package/src/commit/analysis/summary.ts +2 -2
- package/src/commit/changelog/generate.ts +2 -2
- package/src/commit/changelog/index.ts +2 -2
- package/src/commit/map-reduce/index.ts +3 -3
- package/src/commit/map-reduce/map-phase.ts +2 -2
- package/src/commit/map-reduce/reduce-phase.ts +2 -2
- package/src/commit/model-selection.ts +33 -9
- package/src/commit/pipeline.ts +4 -4
- package/src/config/api-key-resolver.ts +58 -0
- package/src/config/keybindings.ts +15 -6
- package/src/config/model-equivalence.ts +35 -12
- package/src/config/model-id-affixes.ts +39 -22
- package/src/config/model-registry.ts +41 -18
- package/src/config/settings-schema.ts +28 -5
- package/src/config/settings.ts +31 -2
- package/src/dap/client.ts +14 -16
- package/src/dap/config.ts +41 -2
- package/src/dap/defaults.json +1 -0
- package/src/dap/session.ts +1 -0
- package/src/dap/types.ts +10 -0
- package/src/debug/index.ts +40 -54
- package/src/edit/renderer.ts +111 -119
- package/src/eval/__tests__/agent-bridge.test.ts +75 -32
- package/src/eval/__tests__/llm-bridge.test.ts +90 -31
- package/src/eval/agent-bridge.ts +34 -7
- package/src/eval/llm-bridge.ts +8 -3
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/plugins/doctor.ts +0 -1
- package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
- package/src/goals/tools/goal-tool.ts +37 -27
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/lsp/client.ts +104 -55
- package/src/lsp/types.ts +10 -0
- package/src/lsp/utils.ts +3 -2
- package/src/main.ts +53 -56
- package/src/memories/index.ts +12 -5
- package/src/memory-backend/index.ts +13 -1
- package/src/memory-backend/resolve.ts +3 -5
- package/src/memory-backend/types.ts +1 -1
- package/src/mnemopi/backend.ts +5 -1
- package/src/modes/acp/acp-agent.ts +33 -26
- package/src/modes/components/assistant-message.ts +2 -9
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/copy-selector.ts +1 -44
- package/src/modes/components/custom-editor.ts +33 -1
- package/src/modes/components/custom-message.ts +1 -3
- package/src/modes/components/execution-shared.ts +1 -2
- package/src/modes/components/hook-message.ts +1 -3
- package/src/modes/components/overlay-box.ts +108 -0
- package/src/modes/components/plan-review-overlay.ts +799 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/read-tool-group.ts +20 -4
- package/src/modes/components/skill-message.ts +0 -1
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tips.txt +1 -0
- package/src/modes/components/todo-reminder.ts +0 -2
- package/src/modes/components/tool-execution.ts +115 -90
- package/src/modes/components/transcript-container.ts +84 -24
- package/src/modes/components/user-message.ts +1 -2
- package/src/modes/controllers/command-controller-shared.ts +7 -6
- package/src/modes/controllers/command-controller.ts +70 -57
- package/src/modes/controllers/event-controller.ts +41 -40
- package/src/modes/controllers/extension-ui-controller.ts +10 -73
- package/src/modes/controllers/input-controller.ts +135 -122
- package/src/modes/controllers/mcp-command-controller.ts +69 -60
- package/src/modes/controllers/selector-controller.ts +25 -27
- package/src/modes/controllers/streaming-reveal.ts +212 -0
- package/src/modes/controllers/tan-command-controller.ts +173 -0
- package/src/modes/index.ts +5 -4
- package/src/modes/interactive-mode.ts +171 -82
- package/src/modes/setup-version.ts +11 -0
- package/src/modes/setup-wizard/index.ts +3 -2
- package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/theme-schema.json +1 -1
- package/src/modes/theme/theme.ts +8 -4
- package/src/modes/types.ts +19 -8
- package/src/modes/utils/context-usage.ts +10 -6
- package/src/modes/utils/copy-targets.ts +133 -27
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +44 -46
- package/src/plan-mode/approved-plan.ts +66 -43
- package/src/plan-mode/plan-protection.ts +4 -4
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/plan-mode-active.md +67 -58
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/sdk.ts +32 -60
- package/src/session/agent-session.ts +89 -13
- package/src/session/messages.ts +26 -0
- package/src/session/session-manager.ts +13 -5
- package/src/slash-commands/builtin-registry.ts +37 -10
- package/src/slash-commands/helpers/usage-report.ts +2 -0
- package/src/slash-commands/types.ts +4 -6
- package/src/task/executor.ts +25 -4
- package/src/task/index.ts +4 -0
- package/src/task/render.ts +212 -148
- package/src/telemetry-export.ts +25 -7
- package/src/tools/archive-reader.ts +64 -0
- package/src/tools/ask.ts +119 -164
- package/src/tools/ast-edit.ts +98 -71
- package/src/tools/ast-grep.ts +37 -43
- package/src/tools/bash.ts +50 -6
- package/src/tools/debug.ts +20 -8
- package/src/tools/eval-backends.ts +6 -17
- package/src/tools/eval-render.ts +21 -18
- package/src/tools/eval.ts +5 -4
- package/src/tools/fetch.ts +391 -91
- package/src/tools/find.ts +44 -30
- package/src/tools/gh-renderer.ts +81 -42
- package/src/tools/grouped-file-output.ts +272 -48
- package/src/tools/image-gen.ts +150 -103
- package/src/tools/inspect-image-renderer.ts +63 -41
- package/src/tools/inspect-image.ts +8 -1
- package/src/tools/job.ts +3 -4
- package/src/tools/memory-render.ts +4 -1
- package/src/tools/plan-mode-guard.ts +21 -39
- package/src/tools/read.ts +23 -16
- package/src/tools/render-utils.ts +38 -40
- package/src/tools/renderers.ts +16 -1
- package/src/tools/report-tool-issue.ts +1 -1
- package/src/tools/resolve.ts +14 -0
- package/src/tools/search-tool-bm25.ts +36 -23
- package/src/tools/search.ts +189 -95
- package/src/tools/sqlite-reader.ts +9 -12
- package/src/tools/todo.ts +138 -59
- package/src/tools/write.ts +100 -60
- package/src/tui/output-block.ts +60 -13
- package/src/tui/status-line.ts +5 -1
- package/src/utils/commit-message-generator.ts +9 -1
- package/src/utils/enhanced-paste.ts +202 -0
- package/src/utils/title-generator.ts +2 -1
- package/src/web/scrapers/github.ts +255 -3
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/anthropic.ts +25 -19
- package/src/web/search/providers/exa.ts +11 -3
- package/src/web/search/providers/kimi.ts +28 -17
- package/src/web/search/providers/parallel.ts +35 -24
- package/src/web/search/providers/perplexity.ts +199 -51
- package/src/web/search/providers/synthetic.ts +8 -6
- package/src/web/search/providers/tavily.ts +9 -8
- package/src/web/search/providers/zai.ts +8 -6
- package/src/web/search/render.ts +39 -54
- package/src/web/search/types.ts +5 -1
- package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
- package/src/eval/__tests__/shared-executors.test.ts +0 -609
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import {
|
|
3
4
|
type Agent,
|
|
@@ -62,7 +63,7 @@ import { MCPManager } from "../../mcp/manager";
|
|
|
62
63
|
import type { MCPServerConfig } from "../../mcp/types";
|
|
63
64
|
import { loadAllExtensions } from "../../modes/components/extensions/state-manager";
|
|
64
65
|
import { theme } from "../../modes/theme/theme";
|
|
65
|
-
import { type PlanApprovalDetails,
|
|
66
|
+
import { type PlanApprovalDetails, resolveApprovedPlan } from "../../plan-mode/approved-plan";
|
|
66
67
|
import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
|
|
67
68
|
import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
|
|
68
69
|
import {
|
|
@@ -1425,24 +1426,16 @@ export class AcpAgent implements Agent {
|
|
|
1425
1426
|
if (!state?.enabled) {
|
|
1426
1427
|
throw new ToolError("Plan mode is not active.");
|
|
1427
1428
|
}
|
|
1428
|
-
const planFilePath =
|
|
1429
|
-
const planContent = await this.#readAcpPlanFile(session, planFilePath);
|
|
1430
|
-
if (planContent === null) {
|
|
1431
|
-
throw new ToolError(
|
|
1432
|
-
`Plan file not found at ${planFilePath}. Write the finalized plan to ${planFilePath} before requesting approval.`,
|
|
1433
|
-
);
|
|
1434
|
-
}
|
|
1435
|
-
const normalized = resolvePlanTitle({
|
|
1429
|
+
const { planFilePath, planContent, title } = await resolveApprovedPlan({
|
|
1436
1430
|
suppliedTitle: extra?.title,
|
|
1437
|
-
|
|
1438
|
-
|
|
1431
|
+
statePlanFilePath: state.planFilePath,
|
|
1432
|
+
readPlan: url => this.#readAcpPlanFile(session, url),
|
|
1433
|
+
listPlanFiles: () => this.#listAcpLocalPlanFiles(session),
|
|
1439
1434
|
});
|
|
1440
|
-
const
|
|
1441
|
-
const approved = await this.#requestAcpPlanApprovalChoice(session.sessionId, normalized.title, planContent);
|
|
1435
|
+
const approved = await this.#requestAcpPlanApprovalChoice(session.sessionId, title, planContent);
|
|
1442
1436
|
const details: PlanApprovalDetails = {
|
|
1443
1437
|
planFilePath,
|
|
1444
|
-
|
|
1445
|
-
title: normalized.title,
|
|
1438
|
+
title,
|
|
1446
1439
|
planExists: true,
|
|
1447
1440
|
};
|
|
1448
1441
|
if (!approved) {
|
|
@@ -1458,16 +1451,10 @@ export class AcpAgent implements Agent {
|
|
|
1458
1451
|
details,
|
|
1459
1452
|
};
|
|
1460
1453
|
}
|
|
1461
|
-
// Approved.
|
|
1462
|
-
//
|
|
1463
|
-
//
|
|
1464
|
-
|
|
1465
|
-
planFilePath,
|
|
1466
|
-
finalPlanFilePath,
|
|
1467
|
-
getArtifactsDir: () => session.sessionManager.getArtifactsDir(),
|
|
1468
|
-
getSessionId: () => session.sessionManager.getSessionId(),
|
|
1469
|
-
});
|
|
1470
|
-
session.setPlanReferencePath(finalPlanFilePath);
|
|
1454
|
+
// Approved. Set the plan reference so the next turn injects the plan
|
|
1455
|
+
// content as context (the file keeps its agent-chosen name — no
|
|
1456
|
+
// rename), then exit plan mode so the agent regains full tools.
|
|
1457
|
+
session.setPlanReferencePath(planFilePath);
|
|
1471
1458
|
session.setStandingResolveHandler?.(null);
|
|
1472
1459
|
session.setPlanModeState(undefined);
|
|
1473
1460
|
try {
|
|
@@ -1486,7 +1473,7 @@ export class AcpAgent implements Agent {
|
|
|
1486
1473
|
content: [
|
|
1487
1474
|
{
|
|
1488
1475
|
type: "text" as const,
|
|
1489
|
-
text: `Plan approved at ${
|
|
1476
|
+
text: `Plan approved at ${planFilePath}. Plan mode exited; proceed with the implementation.`,
|
|
1490
1477
|
},
|
|
1491
1478
|
],
|
|
1492
1479
|
details,
|
|
@@ -1518,6 +1505,26 @@ export class AcpAgent implements Agent {
|
|
|
1518
1505
|
}
|
|
1519
1506
|
}
|
|
1520
1507
|
|
|
1508
|
+
/** `local://` URLs of plan files in the session-local root, newest first —
|
|
1509
|
+
* the `resolveApprovedPlan` fallback for a dropped `extra.title`. */
|
|
1510
|
+
async #listAcpLocalPlanFiles(session: AgentSession): Promise<string[]> {
|
|
1511
|
+
const localRoot = this.#resolveAcpPlanFilePath(session, "local://");
|
|
1512
|
+
try {
|
|
1513
|
+
const entries = await fs.readdir(localRoot, { withFileTypes: true });
|
|
1514
|
+
const plans = await Promise.all(
|
|
1515
|
+
entries
|
|
1516
|
+
.filter(entry => entry.isFile() && /plan\.md$/i.test(entry.name))
|
|
1517
|
+
.map(async entry => {
|
|
1518
|
+
const stat = await fs.stat(path.join(localRoot, entry.name)).catch(() => null);
|
|
1519
|
+
return { url: `local://${entry.name}`, mtime: stat?.mtimeMs ?? 0 };
|
|
1520
|
+
}),
|
|
1521
|
+
);
|
|
1522
|
+
return plans.sort((a, b) => b.mtime - a.mtime).map(plan => plan.url);
|
|
1523
|
+
} catch {
|
|
1524
|
+
return [];
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1521
1528
|
/**
|
|
1522
1529
|
* Ask the ACP client to confirm plan approval. Returns `true` only on an
|
|
1523
1530
|
* explicit `APPROVE_OPTION` selection. Refine, dismissal (`undefined`), or
|
|
@@ -4,7 +4,7 @@ import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
5
|
import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
|
|
6
6
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
7
|
-
import { isSilentAbort } from "../../session/messages";
|
|
7
|
+
import { isSilentAbort, resolveAbortLabel } from "../../session/messages";
|
|
8
8
|
import { resolveImageOptions } from "../../tools/render-utils";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -208,10 +208,6 @@ export class AssistantMessageComponent extends Container {
|
|
|
208
208
|
c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
|
|
209
209
|
);
|
|
210
210
|
|
|
211
|
-
if (hasVisibleContent) {
|
|
212
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
211
|
// Render content in order
|
|
216
212
|
let thinkingIndex = 0;
|
|
217
213
|
for (let i = 0; i < message.content.length; i++) {
|
|
@@ -257,10 +253,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
257
253
|
const hasToolCalls = message.content.some(c => c.type === "toolCall");
|
|
258
254
|
if (!hasToolCalls) {
|
|
259
255
|
if (message.stopReason === "aborted" && !isSilentAbort(message.errorMessage)) {
|
|
260
|
-
const abortMessage =
|
|
261
|
-
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
262
|
-
? message.errorMessage
|
|
263
|
-
: "Operation aborted";
|
|
256
|
+
const abortMessage = resolveAbortLabel(message.errorMessage);
|
|
264
257
|
if (hasVisibleContent) {
|
|
265
258
|
this.#contentContainer.addChild(new Spacer(1));
|
|
266
259
|
} else {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Container } from "@oh-my-pi/pi-tui";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Capabilities a mounted {@link ChatBlock} may use against its host transcript.
|
|
5
|
+
* Kept minimal so blocks never reach into the full TUI/InteractiveMode surface.
|
|
6
|
+
*/
|
|
7
|
+
export interface ChatBlockHost {
|
|
8
|
+
/** Schedule a repaint of the transcript. */
|
|
9
|
+
requestRender(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lifecycle-aware transcript block — the "return a block, let the host mount it"
|
|
14
|
+
* primitive, modelled on React/Svelte component lifecycles.
|
|
15
|
+
*
|
|
16
|
+
* Producers build and return a `ChatBlock` instead of poking `chatContainer` and
|
|
17
|
+
* `ui.requestRender()` directly. The host (`ctx.present`) appends it and calls
|
|
18
|
+
* {@link mount}, which runs {@link onMount}; effects started there register
|
|
19
|
+
* teardown via {@link onCleanup}. The block repaints through {@link requestRender}
|
|
20
|
+
* — never touching the TUI — and tears down exactly once on {@link finish}
|
|
21
|
+
* (self-complete: stop the animation, keep the final frame in the transcript) or
|
|
22
|
+
* {@link dispose} (host discards it, e.g. a transcript reset).
|
|
23
|
+
*
|
|
24
|
+
* While mounted and unfinished a block reports `isTranscriptBlockFinalized() ===
|
|
25
|
+
* false` so {@link "../components/transcript-container".TranscriptContainer}
|
|
26
|
+
* keeps it in the live, repaintable region on ED3-risk terminals; after
|
|
27
|
+
* `finish()`/`dispose()` it reports `true` and freezes at its final content.
|
|
28
|
+
*/
|
|
29
|
+
export abstract class ChatBlock extends Container {
|
|
30
|
+
#host: ChatBlockHost | undefined;
|
|
31
|
+
#cleanups: Array<() => void> = [];
|
|
32
|
+
#active = false;
|
|
33
|
+
#disposed = false;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run setup after the block is in the transcript: start timers/subscriptions
|
|
37
|
+
* and register their teardown with {@link onCleanup}. Default: no-op (a block
|
|
38
|
+
* whose content is fixed at construction needs no mount work).
|
|
39
|
+
*/
|
|
40
|
+
protected onMount(): void {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register a teardown to run on {@link finish}/{@link dispose}, à la a
|
|
44
|
+
* `useEffect` cleanup. If the block is already disposed the cleanup runs
|
|
45
|
+
* immediately so callers never leak.
|
|
46
|
+
*/
|
|
47
|
+
protected onCleanup(cleanup: () => void): void {
|
|
48
|
+
if (this.#disposed) {
|
|
49
|
+
cleanup();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.#cleanups.push(cleanup);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Ask the host to repaint. No-op before mount or after dispose. */
|
|
56
|
+
protected requestRender(): void {
|
|
57
|
+
this.#host?.requestRender();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True between {@link mount} and {@link finish}/{@link dispose}. */
|
|
61
|
+
protected get active(): boolean {
|
|
62
|
+
return this.#active;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Host-only: attach the host and run {@link onMount}. Idempotent — a second
|
|
67
|
+
* call (e.g. a transcript rebuild that re-presents the same instance) is a
|
|
68
|
+
* no-op.
|
|
69
|
+
*/
|
|
70
|
+
mount(host: ChatBlockHost): void {
|
|
71
|
+
if (this.#host || this.#disposed) return;
|
|
72
|
+
this.#host = host;
|
|
73
|
+
this.#active = true;
|
|
74
|
+
this.onMount();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Self-complete: stop ongoing effects and freeze the block at its current
|
|
79
|
+
* content, leaving it rendered in the transcript. Use when the operation the
|
|
80
|
+
* block represents finishes (connection resolved, download done).
|
|
81
|
+
*/
|
|
82
|
+
finish(): void {
|
|
83
|
+
if (!this.#active) return;
|
|
84
|
+
this.#active = false;
|
|
85
|
+
this.#runCleanups();
|
|
86
|
+
this.requestRender();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Host-only teardown: release everything and propagate to children. Called
|
|
91
|
+
* when the host permanently discards the block (transcript reset). Idempotent.
|
|
92
|
+
*/
|
|
93
|
+
override dispose(): void {
|
|
94
|
+
if (this.#disposed) return;
|
|
95
|
+
this.#disposed = true;
|
|
96
|
+
this.#active = false;
|
|
97
|
+
this.#runCleanups();
|
|
98
|
+
super.dispose();
|
|
99
|
+
this.#host = undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Live blocks stay repaintable; finished/disposed ones may freeze. */
|
|
103
|
+
isTranscriptBlockFinalized(): boolean {
|
|
104
|
+
return !this.#active;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#runCleanups(): void {
|
|
108
|
+
const cleanups = this.#cleanups.splice(0);
|
|
109
|
+
for (const cleanup of cleanups) cleanup();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
matchesSelectUp,
|
|
11
11
|
} from "../utils/keybinding-matchers";
|
|
12
12
|
import { keyHint, rawKeyHint } from "./keybinding-hints";
|
|
13
|
+
import { bottomBorder, divider, row, topBorder } from "./overlay-box";
|
|
13
14
|
|
|
14
15
|
/** Minimum rows reserved for the tree even on short terminals. */
|
|
15
16
|
const MIN_TREE_ROWS = 3;
|
|
@@ -32,50 +33,6 @@ interface FlatNode {
|
|
|
32
33
|
ancestorHasNext: boolean[];
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
/** Pad or truncate a (possibly ANSI-styled) string to exactly `width` columns. */
|
|
36
|
-
function fit(text: string, width: number): string {
|
|
37
|
-
if (width <= 0) return "";
|
|
38
|
-
const w = visibleWidth(text);
|
|
39
|
-
if (w === width) return text;
|
|
40
|
-
if (w < width) return text + padding(width - w);
|
|
41
|
-
const cut = truncateToWidth(text, width);
|
|
42
|
-
const cw = visibleWidth(cut);
|
|
43
|
-
return cw < width ? cut + padding(width - cw) : cut;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function paint(s: string): string {
|
|
47
|
-
return theme.fg("border", s);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function topBorder(width: number, title: string): string {
|
|
51
|
-
const box = theme.boxSharp;
|
|
52
|
-
const inner = Math.max(0, width - 2);
|
|
53
|
-
if (!title) return paint(box.topLeft + box.horizontal.repeat(inner) + box.topRight);
|
|
54
|
-
const shown = truncateToWidth(` ${title} `, Math.max(0, inner - 2));
|
|
55
|
-
const fillWidth = Math.max(0, inner - 1 - visibleWidth(shown));
|
|
56
|
-
return (
|
|
57
|
-
paint(box.topLeft + box.horizontal) +
|
|
58
|
-
theme.bold(theme.fg("accent", shown)) +
|
|
59
|
-
paint(box.horizontal.repeat(fillWidth) + box.topRight)
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function divider(width: number): string {
|
|
64
|
-
const box = theme.boxSharp;
|
|
65
|
-
return paint(box.teeRight + box.horizontal.repeat(Math.max(0, width - 2)) + box.teeLeft);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function bottomBorder(width: number): string {
|
|
69
|
-
const box = theme.boxSharp;
|
|
70
|
-
return paint(box.bottomLeft + box.horizontal.repeat(Math.max(0, width - 2)) + box.bottomRight);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Wrap pre-styled content in vertical borders with single-column insets. */
|
|
74
|
-
function row(content: string, width: number): string {
|
|
75
|
-
const box = theme.boxSharp;
|
|
76
|
-
return `${paint(box.vertical)} ${fit(content, Math.max(0, width - 4))} ${paint(box.vertical)}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
36
|
/** Render one tree connector as exactly three cells (e.g. "├─ ", "└─ ", "|--"). */
|
|
80
37
|
function connectorCells(symbol: string): string {
|
|
81
38
|
const chars = Array.from(symbol);
|
|
@@ -10,6 +10,7 @@ type ConfigurableEditorAction = Extract<
|
|
|
10
10
|
| "app.clear"
|
|
11
11
|
| "app.exit"
|
|
12
12
|
| "app.suspend"
|
|
13
|
+
| "app.display.reset"
|
|
13
14
|
| "app.thinking.cycle"
|
|
14
15
|
| "app.model.cycleForward"
|
|
15
16
|
| "app.model.cycleBackward"
|
|
@@ -30,10 +31,11 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
|
|
30
31
|
"app.clear": ["ctrl+c"],
|
|
31
32
|
"app.exit": ["ctrl+d"],
|
|
32
33
|
"app.suspend": ["ctrl+z"],
|
|
34
|
+
"app.display.reset": ["ctrl+l"],
|
|
33
35
|
"app.thinking.cycle": ["shift+tab"],
|
|
34
36
|
"app.model.cycleForward": ["ctrl+p"],
|
|
35
37
|
"app.model.cycleBackward": ["shift+ctrl+p"],
|
|
36
|
-
"app.model.select": ["
|
|
38
|
+
"app.model.select": ["alt+m"],
|
|
37
39
|
"app.model.selectTemporary": ["alt+p"],
|
|
38
40
|
"app.tools.expand": ["ctrl+o"],
|
|
39
41
|
"app.thinking.toggle": ["ctrl+t"],
|
|
@@ -45,6 +47,21 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
|
|
45
47
|
"app.clipboard.copyPrompt": ["alt+shift+c"],
|
|
46
48
|
};
|
|
47
49
|
|
|
50
|
+
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
51
|
+
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
52
|
+
const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
53
|
+
|
|
54
|
+
export function extractBracketedImagePastePath(data: string): string | undefined {
|
|
55
|
+
if (!data.startsWith(BRACKETED_PASTE_START)) return undefined;
|
|
56
|
+
const endIndex = data.indexOf(BRACKETED_PASTE_END, BRACKETED_PASTE_START.length);
|
|
57
|
+
if (endIndex === -1 || endIndex + BRACKETED_PASTE_END.length !== data.length) return undefined;
|
|
58
|
+
|
|
59
|
+
const pasted = data.slice(BRACKETED_PASTE_START.length, endIndex).trim();
|
|
60
|
+
if (!pasted || /[\r\n]/.test(pasted)) return undefined;
|
|
61
|
+
if (!BRACKETED_IMAGE_PATH_REGEX.test(pasted)) return undefined;
|
|
62
|
+
return pasted;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
/**
|
|
49
66
|
* Custom editor that handles configurable app-level shortcuts for coding-agent.
|
|
50
67
|
*/
|
|
@@ -65,6 +82,7 @@ export class CustomEditor extends Editor {
|
|
|
65
82
|
onEscape?: () => void;
|
|
66
83
|
onClear?: () => void;
|
|
67
84
|
onExit?: () => void;
|
|
85
|
+
onDisplayReset?: () => void;
|
|
68
86
|
onCycleThinkingLevel?: () => void;
|
|
69
87
|
onCycleModelForward?: () => void;
|
|
70
88
|
onCycleModelBackward?: () => void;
|
|
@@ -79,6 +97,8 @@ export class CustomEditor extends Editor {
|
|
|
79
97
|
onCopyPrompt?: () => void;
|
|
80
98
|
/** Called when the configured image-paste shortcut is pressed. */
|
|
81
99
|
onPasteImage?: () => Promise<boolean>;
|
|
100
|
+
/** Called when a bracketed paste contains exactly one image-file path. */
|
|
101
|
+
onPasteImagePath?: (path: string) => void;
|
|
82
102
|
/** Called when the configured raw text-paste shortcut is pressed. */
|
|
83
103
|
onPasteTextRaw?: () => void;
|
|
84
104
|
/** Called when the configured dequeue shortcut is pressed. */
|
|
@@ -134,6 +154,12 @@ export class CustomEditor extends Editor {
|
|
|
134
154
|
return;
|
|
135
155
|
}
|
|
136
156
|
|
|
157
|
+
const pastedImagePath = extractBracketedImagePastePath(data);
|
|
158
|
+
if (pastedImagePath && this.onPasteImagePath) {
|
|
159
|
+
this.onPasteImagePath(pastedImagePath);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
137
163
|
// Intercept configured image paste (async - fires and handles result)
|
|
138
164
|
if (this.#matchesAction(data, "app.clipboard.pasteImage") && this.onPasteImage) {
|
|
139
165
|
void this.onPasteImage();
|
|
@@ -158,6 +184,12 @@ export class CustomEditor extends Editor {
|
|
|
158
184
|
return;
|
|
159
185
|
}
|
|
160
186
|
|
|
187
|
+
// Intercept configured display reset shortcut
|
|
188
|
+
if (this.#matchesAction(data, "app.display.reset") && this.onDisplayReset) {
|
|
189
|
+
this.onDisplayReset();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
161
193
|
// Intercept configured suspend shortcut
|
|
162
194
|
if (this.#matchesAction(data, "app.suspend") && this.onSuspend) {
|
|
163
195
|
this.onSuspend();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2
|
-
import { Box, Container
|
|
2
|
+
import { Box, Container } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import type { MessageRenderer } from "../../extensibility/extensions/types";
|
|
4
4
|
import { theme } from "../../modes/theme/theme";
|
|
5
5
|
import type { CustomMessage } from "../../session/messages";
|
|
@@ -20,8 +20,6 @@ export class CustomMessageComponent extends Container {
|
|
|
20
20
|
) {
|
|
21
21
|
super();
|
|
22
22
|
|
|
23
|
-
this.addChild(new Spacer(1));
|
|
24
|
-
|
|
25
23
|
// Create box with custom background (used for default rendering)
|
|
26
24
|
this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
|
|
27
25
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* stay in their respective files.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { type Component, Container, Loader,
|
|
10
|
+
import { type Component, Container, Loader, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
11
11
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
12
12
|
import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
|
|
13
13
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -31,7 +31,6 @@ export function buildExecutionFrame(
|
|
|
31
31
|
): { contentContainer: Container; loader: Loader } {
|
|
32
32
|
const borderColor = (str: string) => theme.fg(colorKey, str);
|
|
33
33
|
|
|
34
|
-
parent.addChild(new Spacer(1));
|
|
35
34
|
parent.addChild(new DynamicBorder(borderColor));
|
|
36
35
|
|
|
37
36
|
const contentContainer = new Container();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2
|
-
import { Box, Container
|
|
2
|
+
import { Box, Container } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import type { HookMessageRenderer } from "../../extensibility/hooks/types";
|
|
4
4
|
import { theme } from "../../modes/theme/theme";
|
|
5
5
|
import type { HookMessage } from "../../session/messages";
|
|
@@ -23,8 +23,6 @@ export class HookMessageComponent extends Container {
|
|
|
23
23
|
) {
|
|
24
24
|
super();
|
|
25
25
|
|
|
26
|
-
this.addChild(new Spacer(1));
|
|
27
|
-
|
|
28
26
|
// Create box with purple background (used for default rendering)
|
|
29
27
|
this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
|
|
30
28
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared box-drawing chrome for fullscreen overlays (the `/copy` picker, the
|
|
3
|
+
* plan-review overlay, …). Every helper paints with `theme.boxSharp` glyphs and
|
|
4
|
+
* the `border`/`accent` theme colors so all outlined overlays read identically.
|
|
5
|
+
*/
|
|
6
|
+
import { padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
7
|
+
import { theme } from "../theme/theme";
|
|
8
|
+
|
|
9
|
+
/** Pad or truncate a (possibly ANSI-styled) string to exactly `width` columns. */
|
|
10
|
+
export function fit(text: string, width: number): string {
|
|
11
|
+
if (width <= 0) return "";
|
|
12
|
+
const w = visibleWidth(text);
|
|
13
|
+
if (w === width) return text;
|
|
14
|
+
if (w < width) return text + padding(width - w);
|
|
15
|
+
const cut = truncateToWidth(text, width);
|
|
16
|
+
const cw = visibleWidth(cut);
|
|
17
|
+
return cw < width ? cut + padding(width - cw) : cut;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function paint(s: string): string {
|
|
21
|
+
return theme.fg("border", s);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Top border with an optional accent-colored title inset into the rule. */
|
|
25
|
+
export function topBorder(width: number, title: string): string {
|
|
26
|
+
const box = theme.boxSharp;
|
|
27
|
+
const inner = Math.max(0, width - 2);
|
|
28
|
+
if (!title) return paint(box.topLeft + box.horizontal.repeat(inner) + box.topRight);
|
|
29
|
+
const shown = truncateToWidth(` ${title} `, Math.max(0, inner - 2));
|
|
30
|
+
const fillWidth = Math.max(0, inner - 1 - visibleWidth(shown));
|
|
31
|
+
return (
|
|
32
|
+
paint(box.topLeft + box.horizontal) +
|
|
33
|
+
theme.bold(theme.fg("accent", shown)) +
|
|
34
|
+
paint(box.horizontal.repeat(fillWidth) + box.topRight)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A horizontal rule with left/right tees, splitting overlay sections. */
|
|
39
|
+
export function divider(width: number): string {
|
|
40
|
+
const box = theme.boxSharp;
|
|
41
|
+
return paint(box.teeRight + box.horizontal.repeat(Math.max(0, width - 2)) + box.teeLeft);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function bottomBorder(width: number): string {
|
|
45
|
+
const box = theme.boxSharp;
|
|
46
|
+
return paint(box.bottomLeft + box.horizontal.repeat(Math.max(0, width - 2)) + box.bottomRight);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Wrap pre-styled content in vertical borders with single-column insets. */
|
|
50
|
+
export function row(content: string, width: number): string {
|
|
51
|
+
const box = theme.boxSharp;
|
|
52
|
+
return `${paint(box.vertical)} ${fit(content, Math.max(0, width - 4))} ${paint(box.vertical)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Column index (0-based) of the inner divider for a two-column layout whose
|
|
57
|
+
* sidebar content area is `sidebarWidth` columns wide. The layout is
|
|
58
|
+
* `│ sidebar │ body │` with a single-column inset on every side, so the divider
|
|
59
|
+
* vertical sits at `sidebarWidth + 3` and the body content area is
|
|
60
|
+
* {@link splitBodyWidth} columns.
|
|
61
|
+
*/
|
|
62
|
+
function splitDividerCol(sidebarWidth: number): number {
|
|
63
|
+
return sidebarWidth + 3;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Body content width for a two-column overlay of total `width`. */
|
|
67
|
+
export function splitBodyWidth(width: number, sidebarWidth: number): number {
|
|
68
|
+
return Math.max(0, width - sidebarWidth - 7);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Top border carrying the title, split by a `┬` over the column divider. */
|
|
72
|
+
export function topBorderSplit(width: number, title: string, sidebarWidth: number): string {
|
|
73
|
+
const box = theme.boxSharp;
|
|
74
|
+
const dividerCol = splitDividerCol(sidebarWidth);
|
|
75
|
+
const leftLen = Math.max(0, dividerCol - 1);
|
|
76
|
+
const rightLen = Math.max(0, width - 2 - dividerCol);
|
|
77
|
+
let left: string;
|
|
78
|
+
if (!title) {
|
|
79
|
+
left = paint(box.topLeft + box.horizontal.repeat(leftLen));
|
|
80
|
+
} else {
|
|
81
|
+
const shown = truncateToWidth(` ${title} `, Math.max(0, leftLen - 1));
|
|
82
|
+
const fillWidth = Math.max(0, leftLen - 1 - visibleWidth(shown));
|
|
83
|
+
left =
|
|
84
|
+
paint(box.topLeft + box.horizontal) +
|
|
85
|
+
theme.bold(theme.fg("accent", shown)) +
|
|
86
|
+
paint(box.horizontal.repeat(fillWidth));
|
|
87
|
+
}
|
|
88
|
+
return left + paint(box.teeDown + box.horizontal.repeat(rightLen) + box.topRight);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Section rule that closes the sidebar column with a `┴` over the divider. */
|
|
92
|
+
export function dividerSplit(width: number, sidebarWidth: number): string {
|
|
93
|
+
const box = theme.boxSharp;
|
|
94
|
+
const dividerCol = splitDividerCol(sidebarWidth);
|
|
95
|
+
const leftLen = Math.max(0, dividerCol - 1);
|
|
96
|
+
const rightLen = Math.max(0, width - 2 - dividerCol);
|
|
97
|
+
return paint(
|
|
98
|
+
box.teeRight + box.horizontal.repeat(leftLen) + box.teeUp + box.horizontal.repeat(rightLen) + box.teeLeft,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A two-column content row: `│ sidebar │ body │`, each inset by one column. */
|
|
103
|
+
export function splitRow(sidebar: string, body: string, width: number, sidebarWidth: number): string {
|
|
104
|
+
const box = theme.boxSharp;
|
|
105
|
+
const bodyWidth = splitBodyWidth(width, sidebarWidth);
|
|
106
|
+
const bar = paint(box.vertical);
|
|
107
|
+
return `${bar} ${fit(sidebar, sidebarWidth)} ${bar} ${fit(body, bodyWidth)} ${bar}`;
|
|
108
|
+
}
|