one-code-extension 0.1.0
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/LICENSE +32 -0
- package/README.md +202 -0
- package/agents/explore.md +24 -0
- package/agents/general-purpose.md +21 -0
- package/agents/plan.md +22 -0
- package/docs/decisions/auto-mode.md +526 -0
- package/docs/decisions/branding.md +127 -0
- package/docs/decisions/code-review-remediation.md +106 -0
- package/docs/decisions/compaction.md +140 -0
- package/docs/decisions/distribution.md +133 -0
- package/docs/decisions/hooks.md +64 -0
- package/docs/decisions/lsp.md +58 -0
- package/docs/decisions/mcp.md +104 -0
- package/docs/decisions/memory-state.md +170 -0
- package/docs/decisions/model-policy.md +126 -0
- package/docs/decisions/modes.md +178 -0
- package/docs/decisions/skills-plugins.md +56 -0
- package/docs/decisions/subagents-workflows.md +225 -0
- package/docs/decisions/system-prompt.md +98 -0
- package/docs/decisions/tools.md +407 -0
- package/docs/decisions/tui.md +202 -0
- package/docs/decisions.md +148 -0
- package/extensions/ask-user/index.ts +124 -0
- package/extensions/ask-user/questions.ts +60 -0
- package/extensions/auto-mode/actions.ts +78 -0
- package/extensions/auto-mode/classifier.ts +388 -0
- package/extensions/auto-mode/config.ts +237 -0
- package/extensions/auto-mode/decision-log.ts +60 -0
- package/extensions/auto-mode/defaults.ts +65 -0
- package/extensions/auto-mode/instructions.ts +74 -0
- package/extensions/auto-mode/model-picker.ts +225 -0
- package/extensions/auto-mode/model-select.ts +178 -0
- package/extensions/auto-mode/paths.ts +109 -0
- package/extensions/auto-mode/pause.ts +96 -0
- package/extensions/auto-mode/prompt.ts +307 -0
- package/extensions/auto-mode/rules.ts +96 -0
- package/extensions/auto-mode/safety-floor.ts +100 -0
- package/extensions/auto-mode/sensitive.ts +137 -0
- package/extensions/auto-mode/shell-analysis.ts +702 -0
- package/extensions/background/index.ts +405 -0
- package/extensions/background/registry.ts +81 -0
- package/extensions/background/wakeup.ts +41 -0
- package/extensions/bash/background.ts +136 -0
- package/extensions/bash/index.ts +149 -0
- package/extensions/branding/assistant-marker.ts +53 -0
- package/extensions/branding/index.ts +366 -0
- package/extensions/branding/prompt-editor.ts +42 -0
- package/extensions/branding/prompt-marker.ts +29 -0
- package/extensions/branding/startup.ts +152 -0
- package/extensions/claude-compat/index.ts +50 -0
- package/extensions/claude-context/index.ts +79 -0
- package/extensions/compaction/index.ts +231 -0
- package/extensions/compaction/prompt.ts +191 -0
- package/extensions/context-management/index.ts +147 -0
- package/extensions/effort/index.ts +216 -0
- package/extensions/effort/slider.ts +253 -0
- package/extensions/exit/index.ts +28 -0
- package/extensions/file-tracker/index.ts +103 -0
- package/extensions/file-tracker/tracker.ts +145 -0
- package/extensions/hooks/debug.ts +53 -0
- package/extensions/hooks/executor.ts +132 -0
- package/extensions/hooks/index.ts +310 -0
- package/extensions/hooks/matcher.ts +72 -0
- package/extensions/hooks/plugin-hooks.ts +79 -0
- package/extensions/hooks/protocol.ts +151 -0
- package/extensions/hooks/settings.ts +163 -0
- package/extensions/hooks/trust.ts +151 -0
- package/extensions/lib/branch-restore.ts +35 -0
- package/extensions/lib/claude-context.ts +148 -0
- package/extensions/lib/deferred.ts +119 -0
- package/extensions/lib/git.ts +14 -0
- package/extensions/lib/memory.ts +219 -0
- package/extensions/lib/model-policy.ts +436 -0
- package/extensions/lib/model-tier.ts +99 -0
- package/extensions/lib/notifications.ts +18 -0
- package/extensions/lib/paths.ts +26 -0
- package/extensions/lib/per-cwd.ts +21 -0
- package/extensions/lib/persisted-output.ts +70 -0
- package/extensions/lib/pi-version.ts +54 -0
- package/extensions/lib/plugins.ts +227 -0
- package/extensions/lib/reminders.ts +212 -0
- package/extensions/lib/scratchpad.ts +68 -0
- package/extensions/lib/tui-render.ts +522 -0
- package/extensions/lsp/client.ts +278 -0
- package/extensions/lsp/format.ts +46 -0
- package/extensions/lsp/index.ts +193 -0
- package/extensions/lsp/protocol.ts +64 -0
- package/extensions/lsp/servers.ts +79 -0
- package/extensions/mcp/client.ts +205 -0
- package/extensions/mcp/config.ts +199 -0
- package/extensions/mcp/index.ts +373 -0
- package/extensions/mcp/schema.ts +161 -0
- package/extensions/memory/index.ts +90 -0
- package/extensions/notebook/index.ts +77 -0
- package/extensions/notebook/notebook.ts +108 -0
- package/extensions/permissions/index.ts +876 -0
- package/extensions/permissions/matcher.ts +455 -0
- package/extensions/permissions/modes.ts +107 -0
- package/extensions/permissions/protected-paths.ts +125 -0
- package/extensions/permissions/settings.ts +98 -0
- package/extensions/plan-mode/index.ts +209 -0
- package/extensions/plan-mode/reminder.ts +33 -0
- package/extensions/plan-mode/slug.ts +28 -0
- package/extensions/plan-mode/viewer.ts +124 -0
- package/extensions/plugins/index.ts +115 -0
- package/extensions/plugins/template.ts +42 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +24 -0
- package/extensions/skill/index.ts +170 -0
- package/extensions/subagents/agents.ts +126 -0
- package/extensions/subagents/child.ts +414 -0
- package/extensions/subagents/default-model.ts +122 -0
- package/extensions/subagents/index.ts +1119 -0
- package/extensions/subagents/model-select.ts +346 -0
- package/extensions/subagents/rpc-turns.ts +123 -0
- package/extensions/subagents/runs.ts +93 -0
- package/extensions/subagents/usage.ts +53 -0
- package/extensions/subagents/worktree.ts +69 -0
- package/extensions/system-prompt/environment.ts +35 -0
- package/extensions/system-prompt/index.ts +54 -0
- package/extensions/system-prompt/template.ts +103 -0
- package/extensions/system-prompt/tiers/common.ts +63 -0
- package/extensions/system-prompt/tiers/frontier.ts +24 -0
- package/extensions/system-prompt/tiers/low.ts +71 -0
- package/extensions/system-prompt/tiers/mid.ts +56 -0
- package/extensions/system-reminder/index.ts +37 -0
- package/extensions/tasks/index.ts +140 -0
- package/extensions/tasks/store.ts +174 -0
- package/extensions/todo/index.ts +133 -0
- package/extensions/tool-search/index.ts +168 -0
- package/extensions/tool-style/index.ts +84 -0
- package/extensions/web/index.ts +43 -0
- package/extensions/web-fetch/extract.ts +139 -0
- package/extensions/web-fetch/index.ts +291 -0
- package/extensions/web-fetch/summarize.ts +78 -0
- package/extensions/workflow/agent-session.ts +368 -0
- package/extensions/workflow/globals.ts +251 -0
- package/extensions/workflow/index.ts +364 -0
- package/extensions/workflow/journal.ts +75 -0
- package/extensions/workflow/permission-gate.ts +71 -0
- package/extensions/workflow/run-manager.ts +306 -0
- package/extensions/workflow/saved-workflows.ts +45 -0
- package/extensions/workflow/script-source.ts +201 -0
- package/extensions/workflow/types.ts +99 -0
- package/extensions/workflow/vm-runtime.ts +31 -0
- package/extensions/workflow/widget.ts +56 -0
- package/extensions/worktree/index.ts +266 -0
- package/extensions/worktree/rewrite.ts +59 -0
- package/package.json +101 -0
- package/themes/one-code-light.json +76 -0
- package/themes/one-code.json +76 -0
- package/types/app.d.ts +5 -0
- package/types/community-packages.d.ts +12 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bash extension — Claude Code's Bash `run_in_background` on top of pi's own
|
|
3
|
+
* bash tool.
|
|
4
|
+
*
|
|
5
|
+
* Registering a tool named `bash` overrides the built-in (findings §2), which
|
|
6
|
+
* is how pi's official sandbox example does it too. The foreground path
|
|
7
|
+
* delegates to pi's real executor (`createBashToolDefinition`) so upstream
|
|
8
|
+
* bash behavior — timeout handling, truncation, PI_* env, spawn hooks — stays
|
|
9
|
+
* exactly pi's; this file only adds the background branch.
|
|
10
|
+
*
|
|
11
|
+
* A background run spawns detached, returns a task id immediately, spools
|
|
12
|
+
* output to `<sessionDir>/bash/<taskId>/output.log`, registers on the shared
|
|
13
|
+
* background registry (task_output/task_stop just work — they are
|
|
14
|
+
* kind-agnostic), and announces completion as a follow-up system
|
|
15
|
+
* notification. The permission gate and auto-mode classifier run before
|
|
16
|
+
* execute like any bash call — a background command is NOT auto-allowed, and
|
|
17
|
+
* the gate fires before anything detaches.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { mkdirSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { createBashToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { Type } from "typebox";
|
|
25
|
+
import { generateTaskId, TASK_REGISTER_CHANNEL } from "../background/registry.ts";
|
|
26
|
+
import { type BashFinishSummary, startBackgroundBash, tailCap } from "./background.ts";
|
|
27
|
+
import { systemNotification } from "../lib/notifications.ts";
|
|
28
|
+
import { perCwd } from "../lib/per-cwd.ts";
|
|
29
|
+
import { ccWrapBuiltinRenderers, linesComponent, resultLines } from "../lib/tui-render.ts";
|
|
30
|
+
|
|
31
|
+
const NOTIFY_OUTPUT_CAP = 30_000;
|
|
32
|
+
|
|
33
|
+
const BashParams = Type.Object({
|
|
34
|
+
command: Type.String({ description: "Bash command to execute" }),
|
|
35
|
+
timeout: Type.Optional(
|
|
36
|
+
Type.Number({ description: "Timeout in seconds (optional; also applies to background runs)" }),
|
|
37
|
+
),
|
|
38
|
+
run_in_background: Type.Optional(
|
|
39
|
+
Type.Boolean({
|
|
40
|
+
description:
|
|
41
|
+
"Run detached and return a task id immediately instead of waiting. Completion arrives as a system notification; inspect with task_output, stop with task_stop. Use this instead of nohup/'&' — those leave an unmanaged orphan process",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
description: Type.Optional(
|
|
45
|
+
Type.String({ description: "5-10 word description of what the command does (shown in notifications)" }),
|
|
46
|
+
),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export default function bashExtension(pi: ExtensionAPI) {
|
|
50
|
+
// pi's definition supplies the description and TUI renderers; the executor
|
|
51
|
+
// is re-created per working directory because it closes over cwd (worktree
|
|
52
|
+
// switches change ctx.cwd mid-session).
|
|
53
|
+
const base = createBashToolDefinition(process.cwd());
|
|
54
|
+
const foreground = perCwd(createBashToolDefinition);
|
|
55
|
+
|
|
56
|
+
const notify = (text: string, details: Record<string, unknown>) => {
|
|
57
|
+
pi.sendMessage(
|
|
58
|
+
{ customType: "task-notification", content: [{ type: "text", text }], display: true, details },
|
|
59
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const taskLogPath = (ctx: ExtensionContext, taskId: string): string | undefined => {
|
|
64
|
+
try {
|
|
65
|
+
const dir = join(ctx.sessionManager.getSessionDir(), "bash", taskId);
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
return join(dir, "output.log");
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const finishLine = (summary: BashFinishSummary, timeoutSeconds?: number): string => {
|
|
74
|
+
if (summary.stopped) return "stopped";
|
|
75
|
+
if (summary.timedOut) return `failed (timed out after ${timeoutSeconds}s)`;
|
|
76
|
+
if (summary.exitCode === 0) return "completed";
|
|
77
|
+
return `failed (${summary.exitCode !== null ? `exit code ${summary.exitCode}` : `terminated by ${summary.signal ?? "unknown signal"}`})`;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
pi.registerTool({
|
|
81
|
+
name: "bash",
|
|
82
|
+
label: base.label,
|
|
83
|
+
description: `${base.description} Pass run_in_background: true for long-running commands (builds, servers, watches): it returns a task id immediately so you can keep working, completion arrives as a system notification, and the output is retrievable with task_output / stoppable with task_stop.`,
|
|
84
|
+
promptSnippet: base.promptSnippet,
|
|
85
|
+
promptGuidelines: base.promptGuidelines,
|
|
86
|
+
executionMode: base.executionMode,
|
|
87
|
+
...(() => {
|
|
88
|
+
// `● Bash(cmd)` / elbow-indented output, like every other One Code tool.
|
|
89
|
+
// Not passing base.renderCall also drops its timer state, so the
|
|
90
|
+
// misleading "Took Ns" that counted permission-prompt wait disappears.
|
|
91
|
+
const wrapped = ccWrapBuiltinRenderers<{ command?: string }>("Bash", base, { title: (a) => a?.command });
|
|
92
|
+
return {
|
|
93
|
+
renderShell: wrapped.renderShell,
|
|
94
|
+
renderCall: wrapped.renderCall as ToolDefinition<typeof BashParams>["renderCall"],
|
|
95
|
+
renderResult: ((result, options, theme, context) => {
|
|
96
|
+
// A background start returns a model-facing instruction paragraph;
|
|
97
|
+
// the transcript needs one line (Claude Code: "Running in the background").
|
|
98
|
+
const details = result.details as { taskId?: string; logPath?: string } | undefined;
|
|
99
|
+
if (details?.taskId && !context.isError) {
|
|
100
|
+
const line = `Running in background (task ${details.taskId}${details.logPath ? ` · log: ${details.logPath}` : ""})`;
|
|
101
|
+
return linesComponent(() => resultLines(theme as any, line, options.expanded, false));
|
|
102
|
+
}
|
|
103
|
+
return wrapped.renderResult(result, options, theme, context);
|
|
104
|
+
}) as ToolDefinition<typeof BashParams>["renderResult"],
|
|
105
|
+
};
|
|
106
|
+
})(),
|
|
107
|
+
parameters: BashParams,
|
|
108
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
109
|
+
if (!params.run_in_background) {
|
|
110
|
+
return foreground(ctx.cwd).execute(
|
|
111
|
+
toolCallId,
|
|
112
|
+
{ command: params.command, timeout: params.timeout },
|
|
113
|
+
signal,
|
|
114
|
+
onUpdate,
|
|
115
|
+
ctx,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const id = generateTaskId();
|
|
120
|
+
const logPath = taskLogPath(ctx, id);
|
|
121
|
+
const description = params.description || params.command.slice(0, 80);
|
|
122
|
+
const task = startBackgroundBash({
|
|
123
|
+
id,
|
|
124
|
+
command: params.command,
|
|
125
|
+
description,
|
|
126
|
+
cwd: ctx.cwd,
|
|
127
|
+
timeoutSeconds: params.timeout,
|
|
128
|
+
logPath,
|
|
129
|
+
onFinished: (finishedTask, summary) => {
|
|
130
|
+
notify(
|
|
131
|
+
systemNotification(`Background bash ${id} (${description}) ${finishLine(summary, params.timeout)}.\n\n${tailCap(summary.output, NOTIFY_OUTPUT_CAP)}`),
|
|
132
|
+
{ taskId: id, status: finishedTask.status, exitCode: summary.exitCode, logPath },
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
pi.events.emit(TASK_REGISTER_CHANNEL, task);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: `⏳ Bash task ${id} running in background (${description}).\n\nCompletion (with output) will arrive as a system notification on its own — you do not need to wait for it or poll; keep working.${logPath ? ` To check interim output, read ${logPath}.` : ""} If your next step cannot proceed without the result, task_output with block=true waits for it. Stop with task_stop.`,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
details: { taskId: id, logPath },
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assistant message marker (pure) — Claude Code prefixes each block of assistant
|
|
3
|
+
* prose with a "●" bullet in the gutter. pi renders assistant text through its
|
|
4
|
+
* own markdown component, and the only hook it exposes for that text is a
|
|
5
|
+
* *text-level* markdown transformer (`registerMarkdownTransformer`) — there is no
|
|
6
|
+
* lines-level access to paint a true gutter, the way `prompt-marker.ts` can for
|
|
7
|
+
* the input editor. So the marker rides as a `● ` prefix on the message's first
|
|
8
|
+
* line instead.
|
|
9
|
+
*
|
|
10
|
+
* A raw prefix would corrupt any message whose first line is a markdown block
|
|
11
|
+
* that must start at column 0 — a heading, a list item, a block quote, a fenced
|
|
12
|
+
* code block, a table row, a horizontal rule, or raw HTML. Prepending `● ` to
|
|
13
|
+
* those turns them into plain paragraphs (`● ## Title`, `● - item`), losing the
|
|
14
|
+
* formatting. Those first lines are therefore left untouched: a missing marker
|
|
15
|
+
* is a far lesser evil than mangled output, and assistant turns here almost
|
|
16
|
+
* always open with a sentence (the system prompt asks for a preamble line before
|
|
17
|
+
* tools), so the guard rarely fires.
|
|
18
|
+
*
|
|
19
|
+
* Kept here as a pure string→string function so it is unit-testable without a
|
|
20
|
+
* terminal; `index.ts` only wires it to `pi.registerMarkdownTransformer`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** The bullet that marks the start of a block of assistant prose. */
|
|
24
|
+
export const ASSISTANT_MARKER = "●";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* First-line shapes a `● ` prefix would corrupt — markdown blocks that must begin
|
|
28
|
+
* at column 0. Matched against the first line only; the string is assumed already
|
|
29
|
+
* trimmed of leading whitespace (pi trims assistant text before rendering).
|
|
30
|
+
*/
|
|
31
|
+
const BLOCK_START = [
|
|
32
|
+
/^#{1,6}\s/, // ATX heading
|
|
33
|
+
/^[-*+]\s/, // unordered list item
|
|
34
|
+
/^\d+[.)]\s/, // ordered list item
|
|
35
|
+
/^>/, // block quote
|
|
36
|
+
/^(?:```|~~~)/, // fenced code
|
|
37
|
+
/^\|/, // table row
|
|
38
|
+
/^(?:-{3,}|\*{3,}|_{3,})\s*$/, // horizontal rule
|
|
39
|
+
/^</, // raw HTML / tag
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Prefix assistant `markdown` with the `● ` marker, unless its first line is a
|
|
44
|
+
* markdown block the prefix would corrupt (see `BLOCK_START`). Returns the input
|
|
45
|
+
* unchanged for empty text or a corrupting first line, and never throws.
|
|
46
|
+
*/
|
|
47
|
+
export function markAssistantMarkdown(markdown: string, marker: string = ASSISTANT_MARKER): string {
|
|
48
|
+
if (!markdown) return markdown;
|
|
49
|
+
const newline = markdown.indexOf("\n");
|
|
50
|
+
const firstLine = newline === -1 ? markdown : markdown.slice(0, newline);
|
|
51
|
+
if (BLOCK_START.some((pattern) => pattern.test(firstLine))) return markdown;
|
|
52
|
+
return `${marker} ${markdown}`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* branding extension — replaces pi's startup header.
|
|
3
|
+
*
|
|
4
|
+
* pi's own banner ("pi v0.83.0 … Ask it how to use or extend Pi") comes from its
|
|
5
|
+
* `piConfig`, which resolves from pi's *own* installed package.json and so cannot
|
|
6
|
+
* be changed by a dependent package. But `ctx.ui.setHeader()` replaces the header
|
|
7
|
+
* component outright, which gets us the same result without a fork.
|
|
8
|
+
*
|
|
9
|
+
* A pi-tui `Component` is small enough to implement inline — `render(width)` plus
|
|
10
|
+
* a no-op `invalidate()` (we have no cached layout to drop) — so this needs no
|
|
11
|
+
* new dependency on pi-tui.
|
|
12
|
+
*
|
|
13
|
+
* Set `CC_NO_BANNER=1` to keep pi's original header.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { getAgentDir, SettingsManager, VERSION as PI_VERSION } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { piVersionWarning } from "../lib/pi-version.ts";
|
|
23
|
+
import {
|
|
24
|
+
PERMISSION_STATUS_CHANNEL,
|
|
25
|
+
permissionModeDisplay,
|
|
26
|
+
type PermissionStatus,
|
|
27
|
+
shortModelName,
|
|
28
|
+
} from "../permissions/modes.ts";
|
|
29
|
+
import { SUBAGENT_STATUS_CHANNEL, type SubagentStatus } from "../subagents/model-select.ts";
|
|
30
|
+
import {
|
|
31
|
+
collectStartupSections,
|
|
32
|
+
quietStartupEnabled,
|
|
33
|
+
shouldDefaultFlushOutputPad,
|
|
34
|
+
shouldDefaultHideThinking,
|
|
35
|
+
type StartupSection,
|
|
36
|
+
} from "./startup.ts";
|
|
37
|
+
import { safeThemePaint, truncateLine } from "../lib/tui-render.ts";
|
|
38
|
+
import { markAssistantMarkdown } from "./assistant-marker.ts";
|
|
39
|
+
import { PROMPT_PADDING, PromptEditor } from "./prompt-editor.ts";
|
|
40
|
+
|
|
41
|
+
export { truncateLine };
|
|
42
|
+
|
|
43
|
+
const NAME = "One Code";
|
|
44
|
+
/** Tagline under the wordmark — the "any model" positioning, matching the README. */
|
|
45
|
+
const SUBTITLE = "the Claude Code experience, on any model";
|
|
46
|
+
|
|
47
|
+
export interface BannerInput {
|
|
48
|
+
version: string;
|
|
49
|
+
model?: string;
|
|
50
|
+
cwd: string;
|
|
51
|
+
mode: string;
|
|
52
|
+
/** Effective subagent/workflow default with its selection source. */
|
|
53
|
+
subagents?: string;
|
|
54
|
+
/** Compact resource sections, shown when pi's own listing is silenced. */
|
|
55
|
+
sections?: StartupSection[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* "ONE CODE" wordmark (figlet "ANSI Shadow"), ONE and CODE on a single line.
|
|
60
|
+
* Rendered as its own block above the text column. Rows are normalised to a
|
|
61
|
+
* single width at load, so a trailing-space trim in the source can never
|
|
62
|
+
* misalign the block. Below WORDMARK_WIDTH a compact "One Code" title replaces
|
|
63
|
+
* it, so a narrow pane never shows sheared glyphs.
|
|
64
|
+
*/
|
|
65
|
+
const WORDMARK_RAW = [
|
|
66
|
+
" ██████╗ ███╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
67
|
+
"██╔═══██╗████╗ ██║██╔════╝ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
68
|
+
"██║ ██║██╔██╗ ██║█████╗ ██║ ██║ ██║██║ ██║█████╗",
|
|
69
|
+
"██║ ██║██║╚██╗██║██╔══╝ ██║ ██║ ██║██║ ██║██╔══╝",
|
|
70
|
+
"╚██████╔╝██║ ╚████║███████╗ ╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
71
|
+
" ╚═════╝ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
72
|
+
];
|
|
73
|
+
/** Display width every wordmark row is padded to (the widest raw row). */
|
|
74
|
+
export const WORDMARK_WIDTH = Math.max(...WORDMARK_RAW.map((r) => [...r].length));
|
|
75
|
+
export const WORDMARK = WORDMARK_RAW.map((r) => r + " ".repeat(WORDMARK_WIDTH - [...r].length));
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* One curated hint line, not a keymap dump: only the controls someone cannot
|
|
79
|
+
* discover on their own — the dials this package renames or hides state behind
|
|
80
|
+
* (effort, permission mode, collapsed thinking/output) plus the two input
|
|
81
|
+
* prefixes and the opt-in keyword nobody could guess. Everything else lives in
|
|
82
|
+
* pi's own /hotkeys listing, which the line points at.
|
|
83
|
+
*/
|
|
84
|
+
const HINTS: [key: string, what: string][] = [
|
|
85
|
+
// First so it survives narrow terminals: the pointer to everything else.
|
|
86
|
+
["/hotkeys", "all keys"],
|
|
87
|
+
// pi calls this dial "thinking" and reserves shift+tab for it; we present
|
|
88
|
+
// it as Claude Code's "effort" so the key and /effort agree on the name.
|
|
89
|
+
["shift+tab", "effort"],
|
|
90
|
+
// Claude Code cycles permission modes on shift+tab; pi owns that key, and
|
|
91
|
+
// ctrl+q is the one ctrl+letter both pi and terminals leave free.
|
|
92
|
+
["ctrl+q", "permissions"],
|
|
93
|
+
["ctrl+t", "thinking"],
|
|
94
|
+
["ctrl+o", "expand output"],
|
|
95
|
+
["/", "commands"],
|
|
96
|
+
["!", "bash"],
|
|
97
|
+
// A keyword rather than a binding — an opt-in feature nobody knows the
|
|
98
|
+
// word for is invisible.
|
|
99
|
+
["ultracode", "max effort"],
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Keep only whole leading items that fit the budget (measured unpainted, so
|
|
104
|
+
* ANSI codes don't count), rather than letting truncateLine cut mid-word.
|
|
105
|
+
* Always keeps at least one item; truncateLine remains the backstop.
|
|
106
|
+
*/
|
|
107
|
+
export function fitItems<T>(items: T[], plainLength: (item: T) => number, budget: number | undefined, sepLength = 3): T[] {
|
|
108
|
+
if (budget === undefined) return items;
|
|
109
|
+
const kept: T[] = [];
|
|
110
|
+
let used = 0;
|
|
111
|
+
for (const item of items) {
|
|
112
|
+
const extra = (kept.length ? sepLength : 0) + plainLength(item);
|
|
113
|
+
if (kept.length > 0 && used + extra > budget) break;
|
|
114
|
+
kept.push(item);
|
|
115
|
+
used += extra;
|
|
116
|
+
}
|
|
117
|
+
return kept;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Sections compressed to one line: context files and themes are few and short,
|
|
122
|
+
* so their names carry information; skills/workflows lists are long and get
|
|
123
|
+
* truncated into noise, so past three items they collapse to a count.
|
|
124
|
+
*/
|
|
125
|
+
export function sectionSummary(sections: StartupSection[], paint: (color: string, text: string) => string): string {
|
|
126
|
+
return sections
|
|
127
|
+
.filter((s) => s.items.length > 0)
|
|
128
|
+
.map((s) =>
|
|
129
|
+
s.items.length <= 3
|
|
130
|
+
? `${paint("muted", s.label)} ${paint("dim", s.items.join(", "))}`
|
|
131
|
+
: `${paint("muted", s.label)} ${paint("dim", String(s.items.length))}`,
|
|
132
|
+
)
|
|
133
|
+
.join(paint("muted", " · "));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Banner layout: the wordmark block, then a one-space-indented text column
|
|
138
|
+
* (tagline + version, context, hints, sections). Kept pure so the layout is
|
|
139
|
+
* unit-testable without a terminal. Below WORDMARK_WIDTH the wordmark is
|
|
140
|
+
* replaced by a compact "One Code" title, so a narrow pane never shows sheared
|
|
141
|
+
* fragments of the art.
|
|
142
|
+
*/
|
|
143
|
+
export function bannerLines(
|
|
144
|
+
input: BannerInput,
|
|
145
|
+
paint: (color: string, text: string) => string,
|
|
146
|
+
width?: number,
|
|
147
|
+
): string[] {
|
|
148
|
+
const indent = " ";
|
|
149
|
+
const budget = width === undefined ? undefined : width - indent.length;
|
|
150
|
+
|
|
151
|
+
const subtitle = `${paint("dim", SUBTITLE)}${paint("muted", " · ")}${paint("dim", `v${input.version}`)}`;
|
|
152
|
+
const context = [
|
|
153
|
+
input.model ? `${paint("muted", "model")} ${input.model}` : undefined,
|
|
154
|
+
`${paint("muted", "mode")} ${input.mode}`,
|
|
155
|
+
input.subagents ? `${paint("muted", "subagents")} ${input.subagents}` : undefined,
|
|
156
|
+
]
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.join(paint("muted", " · "));
|
|
159
|
+
// Narrow terminals drop whole trailing hints (the /hotkeys pointer is
|
|
160
|
+
// first, so it always survives) instead of cutting one mid-word.
|
|
161
|
+
const hints = fitItems(HINTS, ([key, what]) => key.length + 1 + what.length, budget)
|
|
162
|
+
.map(([key, what]) => `${paint("accent", key)} ${paint("muted", what)}`)
|
|
163
|
+
.join(paint("muted", " · "));
|
|
164
|
+
const sections = sectionSummary(input.sections ?? [], paint);
|
|
165
|
+
|
|
166
|
+
const textColumn = [subtitle, context, hints, ...(sections ? [sections] : [])].map((line) => `${indent}${line}`);
|
|
167
|
+
|
|
168
|
+
// The wordmark is a fixed-width block; if it cannot fit, show a text title
|
|
169
|
+
// rather than let truncateLine shear the glyphs mid-row.
|
|
170
|
+
const head =
|
|
171
|
+
width !== undefined && width < WORDMARK_WIDTH ? [paint("accent", NAME)] : WORDMARK.map((row) => paint("accent", row));
|
|
172
|
+
|
|
173
|
+
const assembled = [...head, ...textColumn];
|
|
174
|
+
return width === undefined ? assembled : assembled.map((line) => truncateLine(line, width));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Collapsed-thinking placeholder — pi paints it in thinkingText + italic. */
|
|
178
|
+
const THINKING_LABEL = "✻ Thinking… (ctrl+t to expand)";
|
|
179
|
+
|
|
180
|
+
/** Claude Code-style input marker, painted in the editor's left gutter. A heavy
|
|
181
|
+
* chevron (U+276F), one column wide so the two-column gutter still aligns. */
|
|
182
|
+
const PROMPT_GLYPH = "❯";
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* `pi.registerMarkdownTransformer` typed locally: the runtime (pi ≥ 0.84) exposes
|
|
186
|
+
* it, but the pinned 0.83 type declarations do not, so we cast and feature-detect
|
|
187
|
+
* — the same "older pi may lack the hook" handling as the prompt marker.
|
|
188
|
+
*/
|
|
189
|
+
interface MarkdownTransformerHost {
|
|
190
|
+
registerMarkdownTransformer?: (
|
|
191
|
+
transformer: (
|
|
192
|
+
markdown: string,
|
|
193
|
+
context: { messageType: "user" | "assistant" | "assistant-thinking"; isStreaming: boolean; availableWidth: number },
|
|
194
|
+
) => string,
|
|
195
|
+
) => void;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The slice of the UI context the prompt marker needs (older pi may lack the hook). */
|
|
199
|
+
interface BrandingEditorUI {
|
|
200
|
+
setEditorComponent?: (factory: (tui: unknown, theme: unknown, keybindings: unknown) => unknown) => void;
|
|
201
|
+
theme?: unknown;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Replace the core input editor with one that paints a "›" prompt marker, the
|
|
206
|
+
* way Claude Code does. TUI-only (print/rpc have no editor), skipped when pi is
|
|
207
|
+
* too old to expose the hook, and disableable with CC_NO_INPUT_MARKER=1 in case
|
|
208
|
+
* a pi-tui render change ever makes the marker misplace (cosmetic only — typing
|
|
209
|
+
* is never affected; see prompt-marker.ts).
|
|
210
|
+
*/
|
|
211
|
+
function installPromptMarker(ctx: { hasUI: boolean; mode: string; ui: BrandingEditorUI }): void {
|
|
212
|
+
if (process.env.CC_NO_INPUT_MARKER === "1") return;
|
|
213
|
+
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
214
|
+
const ui = ctx.ui;
|
|
215
|
+
if (typeof ui.setEditorComponent !== "function") return;
|
|
216
|
+
// The marker follows live theme changes because it is re-read per render.
|
|
217
|
+
const renderMarker = () => {
|
|
218
|
+
// Bold makes the chevron read heavier than the input text beside it.
|
|
219
|
+
return `\x1b[1m${safeThemePaint(ui.theme)("accent", PROMPT_GLYPH)}\x1b[0m${" ".repeat(PROMPT_PADDING - 1)}`;
|
|
220
|
+
};
|
|
221
|
+
ui.setEditorComponent((tui: unknown, theme: unknown, keybindings: unknown) => new PromptEditor(tui as never, theme as never, keybindings as never, renderMarker));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Default thinking blocks to collapsed, once, respecting any user choice.
|
|
226
|
+
* pi caches settings in memory at startup, so a write here lands from the
|
|
227
|
+
* *next* session; the current one keeps whatever the file said at launch
|
|
228
|
+
* (ctrl+t still works immediately and persists the user's own preference).
|
|
229
|
+
*/
|
|
230
|
+
function applyThinkingDefault(): void {
|
|
231
|
+
try {
|
|
232
|
+
const agentDir = getAgentDir();
|
|
233
|
+
const settingsPath = join(agentDir, "settings.json");
|
|
234
|
+
const raw = existsSync(settingsPath) ? readFileSync(settingsPath, "utf8") : undefined;
|
|
235
|
+
if (shouldDefaultHideThinking(raw)) {
|
|
236
|
+
SettingsManager.create(process.cwd(), agentDir).setHideThinkingBlock(true);
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// Presentation-only default — never let a settings hiccup break startup.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Default pi's output padding to 0 so the assistant "●" marker sits flush at
|
|
245
|
+
* column 0, aligned with the tool bullets (which render there). Only when the
|
|
246
|
+
* user hasn't set `outputPad` themselves; like the thinking default, pi caches
|
|
247
|
+
* settings at startup, so this lands from the *next* session (the current one
|
|
248
|
+
* keeps whatever pad it launched with). Skipped when the marker is disabled, so
|
|
249
|
+
* the two travel together.
|
|
250
|
+
*/
|
|
251
|
+
function applyOutputPadDefault(): void {
|
|
252
|
+
if (process.env.CC_NO_ASSISTANT_MARKER === "1") return;
|
|
253
|
+
try {
|
|
254
|
+
const agentDir = getAgentDir();
|
|
255
|
+
const settingsPath = join(agentDir, "settings.json");
|
|
256
|
+
const raw = existsSync(settingsPath) ? readFileSync(settingsPath, "utf8") : undefined;
|
|
257
|
+
if (shouldDefaultFlushOutputPad(raw)) {
|
|
258
|
+
SettingsManager.create(process.cwd(), agentDir).setOutputPad(0);
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
// Presentation-only default — never let a settings hiccup break startup.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export default function brandingExtension(pi: ExtensionAPI) {
|
|
266
|
+
applyThinkingDefault();
|
|
267
|
+
applyOutputPadDefault();
|
|
268
|
+
// Mark each block of assistant prose with Claude Code's "●" bullet. Only
|
|
269
|
+
// assistant text is transformed (not thinking or user turns); the transformer
|
|
270
|
+
// runs in markdown rendering, so print/rpc output is untouched.
|
|
271
|
+
// CC_NO_ASSISTANT_MARKER=1 opts out, mirroring CC_NO_INPUT_MARKER.
|
|
272
|
+
const transformerHost = pi as unknown as MarkdownTransformerHost;
|
|
273
|
+
if (process.env.CC_NO_ASSISTANT_MARKER !== "1" && typeof transformerHost.registerMarkdownTransformer === "function") {
|
|
274
|
+
transformerHost.registerMarkdownTransformer((markdown, context) =>
|
|
275
|
+
context.messageType === "assistant" ? markAssistantMarkdown(markdown) : markdown,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
// The label and prompt marker apply in every session; CC_NO_BANNER only
|
|
279
|
+
// disables the header.
|
|
280
|
+
pi.on("session_start", (_event, ctx) => {
|
|
281
|
+
ctx.ui.setHiddenThinkingLabel(THINKING_LABEL);
|
|
282
|
+
installPromptMarker(ctx as unknown as { hasUI: boolean; mode: string; ui: BrandingEditorUI });
|
|
283
|
+
// Soft drift guard: warn once at startup when the hosting pi is outside
|
|
284
|
+
// the range this release was tested against (see lib/pi-version.ts).
|
|
285
|
+
const versionWarning = piVersionWarning(PI_VERSION);
|
|
286
|
+
if (versionWarning) ctx.ui.notify(versionWarning, "warning");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (process.env.CC_NO_BANNER === "1") return;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Mode and classifier arrive over the bus from the permissions extension
|
|
293
|
+
* (jiti isolates module state, so this cannot be a shared variable). The
|
|
294
|
+
* banner re-renders on every update, so cycling modes or the classifier
|
|
295
|
+
* pinning mid-session keeps the header truthful instead of frozen at
|
|
296
|
+
* whatever was true at startup.
|
|
297
|
+
*/
|
|
298
|
+
let permissionStatus: PermissionStatus | undefined;
|
|
299
|
+
let subagentStatus: SubagentStatus | undefined;
|
|
300
|
+
let currentModelId: string | undefined;
|
|
301
|
+
let requestHeaderRender: (() => void) | undefined;
|
|
302
|
+
// The model line goes stale the same way the mode line did once the header
|
|
303
|
+
// re-renders live, so it follows ctrl+p / /model changes too.
|
|
304
|
+
pi.on("model_select", (event) => {
|
|
305
|
+
currentModelId = event.model?.id ?? currentModelId;
|
|
306
|
+
requestHeaderRender?.();
|
|
307
|
+
});
|
|
308
|
+
pi.events.on(PERMISSION_STATUS_CHANNEL, (data) => {
|
|
309
|
+
permissionStatus = data as PermissionStatus;
|
|
310
|
+
requestHeaderRender?.();
|
|
311
|
+
});
|
|
312
|
+
pi.events.on(SUBAGENT_STATUS_CHANNEL, (data) => {
|
|
313
|
+
subagentStatus = data as SubagentStatus;
|
|
314
|
+
requestHeaderRender?.();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
pi.on("session_start", (_event, ctx) => {
|
|
318
|
+
// Only the TUI has a header to replace; rpc/print modes have no chrome.
|
|
319
|
+
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
320
|
+
|
|
321
|
+
const version = process.env.CC_VERSION ?? "0.1.0";
|
|
322
|
+
currentModelId = ctx.model ? `${ctx.model.id}` : currentModelId;
|
|
323
|
+
|
|
324
|
+
// With pi's own listing silenced, the banner carries compact sections
|
|
325
|
+
// instead (minus the internal [Extensions] noise).
|
|
326
|
+
const home = os.homedir();
|
|
327
|
+
// Resolve pi's live agent dir (honours PI_CODING_AGENT_DIR) so an
|
|
328
|
+
// isolated one-code app reads its own settings/skills, never ~/.pi.
|
|
329
|
+
const agentDir = getAgentDir();
|
|
330
|
+
const sections = quietStartupEnabled(join(agentDir, "settings.json"))
|
|
331
|
+
? collectStartupSections(ctx.cwd, home, join(dirname(fileURLToPath(import.meta.url)), "..", "..", "themes"), agentDir)
|
|
332
|
+
: undefined;
|
|
333
|
+
|
|
334
|
+
ctx.ui.setTitle(NAME);
|
|
335
|
+
ctx.ui.setHeader((tui: unknown, theme: unknown) => {
|
|
336
|
+
const paint = safeThemePaint(theme);
|
|
337
|
+
requestHeaderRender = () => {
|
|
338
|
+
(tui as { requestRender?: () => void } | undefined)?.requestRender?.();
|
|
339
|
+
};
|
|
340
|
+
return {
|
|
341
|
+
// Rendered per paint rather than precomputed, so the mode line
|
|
342
|
+
// follows ctrl+q cycles and the classifier pinning.
|
|
343
|
+
render: (width: number) => [
|
|
344
|
+
"",
|
|
345
|
+
...bannerLines(
|
|
346
|
+
{
|
|
347
|
+
version,
|
|
348
|
+
model: currentModelId,
|
|
349
|
+
cwd: ctx.cwd,
|
|
350
|
+
mode: permissionModeDisplay(permissionStatus ?? { mode: "default", paused: false }),
|
|
351
|
+
subagents: subagentStatus?.model
|
|
352
|
+
? `${shortModelName(subagentStatus.model)}${subagentStatus.via ? ` (${subagentStatus.via})` : ""}`
|
|
353
|
+
: undefined,
|
|
354
|
+
sections,
|
|
355
|
+
},
|
|
356
|
+
paint,
|
|
357
|
+
width,
|
|
358
|
+
),
|
|
359
|
+
"",
|
|
360
|
+
],
|
|
361
|
+
// Nothing is cached, so there is nothing to invalidate.
|
|
362
|
+
invalidate: () => {},
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptEditor — the core input editor with a Claude Code-style "❯" marker.
|
|
3
|
+
*
|
|
4
|
+
* pi's editor has no prompt marker and no option for one, but `CustomEditor` is
|
|
5
|
+
* the sanctioned base for a replacement (it already wires every app keybinding
|
|
6
|
+
* through handleInput, which we leave untouched). We only reserve a two-column
|
|
7
|
+
* left gutter and paint the marker into it — see prompt-marker.ts for why that
|
|
8
|
+
* is cursor-safe. Registered via `ctx.ui.setEditorComponent`; this file is thin
|
|
9
|
+
* wiring over the pure `applyPromptMarker`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { applyPromptMarker } from "./prompt-marker.ts";
|
|
14
|
+
|
|
15
|
+
/** Width of the left gutter, and therefore the visible width the marker must fill. */
|
|
16
|
+
export const PROMPT_PADDING = 2;
|
|
17
|
+
|
|
18
|
+
type EditorArgs = ConstructorParameters<typeof CustomEditor>;
|
|
19
|
+
|
|
20
|
+
export class PromptEditor extends CustomEditor {
|
|
21
|
+
/** Re-read per render so the marker follows live theme changes. */
|
|
22
|
+
#renderMarker: () => string;
|
|
23
|
+
|
|
24
|
+
constructor(tui: EditorArgs[0], theme: EditorArgs[1], keybindings: EditorArgs[2], renderMarker: () => string) {
|
|
25
|
+
super(tui, theme, keybindings, { paddingX: PROMPT_PADDING });
|
|
26
|
+
this.#renderMarker = renderMarker;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pin the gutter. On install pi copies the *default* editor's paddingX (0)
|
|
31
|
+
* onto us, which would leave no room to paint into; keep it at our width so
|
|
32
|
+
* the marker always has its gutter. A caller asking for a wider gutter still
|
|
33
|
+
* gets at least ours.
|
|
34
|
+
*/
|
|
35
|
+
setPaddingX(padding: number): void {
|
|
36
|
+
super.setPaddingX(Math.max(PROMPT_PADDING, padding));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render(width: number): string[] {
|
|
40
|
+
return applyPromptMarker(super.render(width), PROMPT_PADDING, this.#renderMarker());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input prompt marker (pure) — Claude Code shows a "❯" at the start of the
|
|
3
|
+
* input line; pi's editor draws none. pi's editor renders as
|
|
4
|
+
* `[topBorder, ...contentLines, bottomBorder]`, and each content line begins
|
|
5
|
+
* with `paddingX` literal spaces (the left gutter). We overwrite exactly those
|
|
6
|
+
* gutter spaces on the first content line with a marker the caller has sized to
|
|
7
|
+
* `paddingX` visible columns. The visible cursor is drawn inside the text (and
|
|
8
|
+
* reported separately via getCursor), so painting into the always-blank gutter
|
|
9
|
+
* never shifts text or the cursor — the coupling can only ever go wrong
|
|
10
|
+
* cosmetically, never break typing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Return a copy of the editor's rendered lines with the first content line's
|
|
15
|
+
* left gutter replaced by `marker`. No-op (returns the input) when there is no
|
|
16
|
+
* content line yet or the first gutter is not the blank padding we expect — so
|
|
17
|
+
* an unexpected render shape degrades to "no marker" rather than corruption.
|
|
18
|
+
*
|
|
19
|
+
* `marker` must occupy exactly `paddingX` visible columns (e.g. "› " for 2).
|
|
20
|
+
*/
|
|
21
|
+
export function applyPromptMarker(lines: string[], paddingX: number, marker: string): string[] {
|
|
22
|
+
// index 0 is always the top border; the first content line is index 1.
|
|
23
|
+
if (paddingX <= 0 || lines.length < 2) return lines;
|
|
24
|
+
const first = lines[1];
|
|
25
|
+
if (first.slice(0, paddingX) !== " ".repeat(paddingX)) return lines;
|
|
26
|
+
const out = lines.slice();
|
|
27
|
+
out[1] = marker + first.slice(paddingX);
|
|
28
|
+
return out;
|
|
29
|
+
}
|