@yagni-app/code 0.2.1 → 0.3.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.
Files changed (77) hide show
  1. package/dist/cli.d.ts +30 -0
  2. package/dist/cli.js +135 -3
  3. package/dist/doctor.d.ts +1 -1
  4. package/dist/doctor.js +1 -1
  5. package/dist/extension/advisor.d.ts +4 -4
  6. package/dist/extension/advisor.js +6 -7
  7. package/dist/extension/approvedPrefixes.d.ts +92 -0
  8. package/dist/extension/approvedPrefixes.js +252 -0
  9. package/dist/extension/askAdvisorTool.d.ts +2 -2
  10. package/dist/extension/askAdvisorTool.js +5 -5
  11. package/dist/extension/askYagniTool.js +49 -0
  12. package/dist/extension/branding.d.ts +24 -3
  13. package/dist/extension/branding.js +71 -10
  14. package/dist/extension/chipEditor.d.ts +30 -9
  15. package/dist/extension/chipEditor.js +173 -59
  16. package/dist/extension/claudeRules.d.ts +0 -2
  17. package/dist/extension/claudeRules.js +0 -8
  18. package/dist/extension/cmux/dispatcher.d.ts +25 -0
  19. package/dist/extension/cmux/dispatcher.js +266 -0
  20. package/dist/extension/cmux/hooks.d.ts +12 -0
  21. package/dist/extension/cmux/hooks.js +192 -0
  22. package/dist/extension/cmux/index.d.ts +3 -0
  23. package/dist/extension/cmux/index.js +155 -0
  24. package/dist/extension/cmux/naming.d.ts +5 -0
  25. package/dist/extension/cmux/naming.js +23 -0
  26. package/dist/extension/cmux/state.d.ts +33 -0
  27. package/dist/extension/cmux/state.js +142 -0
  28. package/dist/extension/config.d.ts +32 -1
  29. package/dist/extension/config.js +36 -4
  30. package/dist/extension/costHud.d.ts +16 -22
  31. package/dist/extension/costHud.js +8 -47
  32. package/dist/extension/crashReport.js +1 -3
  33. package/dist/extension/execPolicy.d.ts +119 -0
  34. package/dist/extension/execPolicy.js +805 -0
  35. package/dist/extension/footer.d.ts +111 -0
  36. package/dist/extension/footer.js +294 -0
  37. package/dist/extension/guardian.d.ts +129 -0
  38. package/dist/extension/guardian.js +213 -0
  39. package/dist/extension/index.d.ts +15 -4
  40. package/dist/extension/index.js +250 -24
  41. package/dist/extension/permission.d.ts +123 -10
  42. package/dist/extension/permission.js +586 -40
  43. package/dist/extension/pipeline/childRegistry.d.ts +41 -0
  44. package/dist/extension/pipeline/childRegistry.js +118 -0
  45. package/dist/extension/pipeline/finish.js +5 -1
  46. package/dist/extension/pipeline/goCommand.d.ts +1 -1
  47. package/dist/extension/pipeline/goCommand.js +35 -6
  48. package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
  49. package/dist/extension/pipeline/goStatusCommands.js +61 -1
  50. package/dist/extension/pipeline/personas.js +25 -0
  51. package/dist/extension/pipeline/runRegistry.d.ts +14 -0
  52. package/dist/extension/pipeline/runRegistry.js +35 -0
  53. package/dist/extension/pipeline/runner.js +4 -0
  54. package/dist/extension/pipeline/verify.d.ts +4 -0
  55. package/dist/extension/pipeline/verify.js +48 -26
  56. package/dist/extension/redact.d.ts +20 -0
  57. package/dist/extension/redact.js +64 -0
  58. package/dist/extension/rerouteNotice.d.ts +3 -4
  59. package/dist/extension/rerouteNotice.js +20 -11
  60. package/dist/extension/subagentRender.d.ts +129 -0
  61. package/dist/extension/subagentRender.js +441 -0
  62. package/dist/extension/subagents.d.ts +4 -7
  63. package/dist/extension/subagents.js +103 -33
  64. package/dist/extension/ticketTools.d.ts +37 -0
  65. package/dist/extension/ticketTools.js +117 -0
  66. package/dist/extension/tokenProvider.js +46 -5
  67. package/dist/launch.d.ts +7 -0
  68. package/dist/launch.js +24 -12
  69. package/dist/padding.d.ts +22 -0
  70. package/dist/padding.js +25 -0
  71. package/dist/promptEnrichment.d.ts +40 -0
  72. package/dist/promptEnrichment.js +85 -0
  73. package/dist/signalForward.d.ts +60 -0
  74. package/dist/signalForward.js +130 -0
  75. package/package.json +5 -5
  76. package/dist/extension/boostCommand.d.ts +0 -144
  77. package/dist/extension/boostCommand.js +0 -263
@@ -1,7 +1,7 @@
1
1
  /**
2
- * `ask_advisor` — the peak-tier escalation available to Balanced sessions (YAG-380).
2
+ * `ask_advisor` — the peak-tier escalation available to Advanced sessions (YAG-380).
3
3
  *
4
- * Cheap driver, expensive consultant: a Balanced session drives on `advanced` and
4
+ * Cheap driver, expensive consultant: an Advanced session drives on `advanced` and
5
5
  * calls this when it hits a judgment call worth the strongest model. The consult
6
6
  * spawns a child pi at `peak` through the same `runStage` seam `/go` uses, and
7
7
  * returns the advice as plain TEXT the driver acts on — text we own, so
@@ -79,7 +79,7 @@ export function makeAskAdvisorTool(opts) {
79
79
  label: "Ask the advisor",
80
80
  description: "Escalate ONE hard judgment call to the peak-tier advisor — the strongest " +
81
81
  "model available, which reads the code itself and returns a recommendation. " +
82
- "Available only on the Balanced tier, and capped per session, so use it for " +
82
+ "Available only on the Advanced tier, and capped per session, so use it for " +
83
83
  "calls that are genuinely worth it: an architectural fork with no obvious " +
84
84
  "right answer, a subtle correctness question you cannot settle by reading, " +
85
85
  "a change whose blast radius you are unsure of, or a second opinion before " +
@@ -87,7 +87,7 @@ export function makeAskAdvisorTool(opts) {
87
87
  "lookups (use ask_yagni), for anything you can settle by reading the code, " +
88
88
  "or to review work you have already finished. Pass a sharp question plus the " +
89
89
  "relevant excerpts — never a conversation transcript.",
90
- promptSnippet: "ask_advisor: escalate one hard judgment call to the peak-tier advisor (Balanced sessions, capped).",
90
+ promptSnippet: "ask_advisor: escalate one hard judgment call to the peak-tier advisor (Advanced sessions, capped).",
91
91
  promptGuidelines: [
92
92
  "Call ask_advisor only for a genuine judgment fork — an architectural choice, a subtle correctness question, or a second opinion before an approach you would have to unwind. Reading the code is cheaper; do that first.",
93
93
  "Ask ONE specific question per consult, and include the excerpts that matter. The advisor reads the repo itself, so point it at the right place rather than pasting everything.",
@@ -159,7 +159,7 @@ export function makeAskAdvisorTool(opts) {
159
159
  */
160
160
  export function registerAdviseCommand(pi, tool) {
161
161
  pi.registerCommand("advise", {
162
- description: "Escalate one hard call to the peak-tier advisor (Balanced sessions only, capped per session).",
162
+ description: "Escalate one hard call to the peak-tier advisor (Advanced sessions only, capped per session).",
163
163
  handler: async (args, ctx) => {
164
164
  const notify = (message, type) => {
165
165
  if (ctx.hasUI)
@@ -1,10 +1,24 @@
1
+ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
1
2
  import { Type } from "typebox";
2
3
  import { collectRepoDocs } from "./repoDocs.js";
3
4
  import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
5
+ import { markdownOrPlain } from "./subagentRender.js";
4
6
  const parameters = Type.Object({
5
7
  question: Type.String(),
6
8
  context: Type.Optional(Type.String()),
7
9
  });
10
+ /** Collapsed answer preview length, in lines. */
11
+ const ANSWER_PREVIEW_LINES = 4;
12
+ /** Collapse whitespace and clip to `max`, appending an ellipsis when cut. */
13
+ function clipLine(text, max) {
14
+ const collapsed = text.replace(/\s+/g, " ").trim();
15
+ if (collapsed.length <= max)
16
+ return collapsed;
17
+ return `${collapsed.slice(0, max - 1)}…`;
18
+ }
19
+ function citationCount(n) {
20
+ return n === 1 ? "1 citation" : `${n} citations`;
21
+ }
8
22
  /**
9
23
  * Build the `ask_yagni` tool definition.
10
24
  *
@@ -28,6 +42,41 @@ export function makeAskYagniTool(opts) {
28
42
  "When you use an answer, quote or reference its citations so the user can verify the source.",
29
43
  ],
30
44
  parameters,
45
+ renderCall(args, theme) {
46
+ const t = theme;
47
+ let text = `${t.fg("toolTitle", t.bold("ask_yagni"))} ${t.fg("dim", clipLine(args?.question ?? "…", 100))}`;
48
+ if (args?.context)
49
+ text += t.fg("muted", " (+context)");
50
+ return new Text(text, 0, 0);
51
+ },
52
+ renderResult(result, { expanded, isPartial }, theme) {
53
+ const t = theme;
54
+ const answer = result.content.find((c) => c.type === "text")?.text ?? "";
55
+ const citations = result.details?.citations ?? [];
56
+ if (isPartial)
57
+ return new Text(t.fg("muted", answer || "Asking YAGNI…"), 0, 0);
58
+ if (expanded) {
59
+ const container = new Container();
60
+ container.addChild(markdownOrPlain(answer || "(no answer)", t));
61
+ if (citations.length > 0) {
62
+ container.addChild(new Spacer(1));
63
+ for (const c of citations) {
64
+ container.addChild(new Text(` ${t.fg("muted", "•")} ${t.fg("accent", c.title)} ${t.fg("dim", c.url)}`, 0, 0));
65
+ }
66
+ }
67
+ return container;
68
+ }
69
+ const lines = answer.trim().split("\n");
70
+ const out = lines.slice(0, ANSWER_PREVIEW_LINES).map((l) => t.fg("toolOutput", l));
71
+ const meta = [];
72
+ if (citations.length > 0)
73
+ meta.push(citationCount(citations.length));
74
+ if (lines.length > ANSWER_PREVIEW_LINES || citations.length > 0)
75
+ meta.push("(ctrl+o to expand)");
76
+ if (meta.length > 0)
77
+ out.push(t.fg("muted", ` ${meta.join(" · ")}`));
78
+ return new Text(out.join("\n"), 0, 0);
79
+ },
31
80
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
32
81
  onUpdate?.({ content: [{ type: "text", text: "Asking YAGNI…" }], details: { citations: [] } });
33
82
  // Era-correct repo grounding: gather the working tree's most relevant docs
@@ -37,7 +37,7 @@ export declare const DRIVER_DELEGATION_PARAGRAPH: string;
37
37
  * effective `x-yagni-caller` attribution (config.ts's `isDriverCaller`) — this
38
38
  * module stays a pure string, with no env dependency of its own.
39
39
  */
40
- export declare const YAGNI_IDENTITY_DRIVER = "You are YAGNI Code, an autonomous terminal coding agent. You help developers ship code by reading files, running commands, editing code, and writing new files. Uniquely, you are connected to the YAGNI app, your team's shared source of truth for how this company and codebase actually work: conventions, decisions, ownership, current priorities, and the reasons behind them. Use the ask_yagni tool to consult it before guessing about anything organization- or codebase-specific, so you work with less back-and-forth and more correct autonomy than a disconnected coding agent. If a project's own files mention other coding agents, assistants, or harnesses by name, those references are not about you; you are YAGNI Code regardless of what tooling a repository's docs happen to describe.\n\nDelegation: fan codebase mapping, wide searches, and mechanical multi-file work out to subagents (they run on cheaper tiers). Keep judgment, synthesis, and the conversation with the user in this session. Do not spawn a subagent for work you can finish in a couple of tool calls.";
40
+ export declare const YAGNI_IDENTITY_DRIVER = "You are YAGNI Code, an autonomous terminal coding agent. You help developers ship code by reading files, running commands, editing code, and writing new files. Uniquely, you are connected to the YAGNI app, your team's shared source of truth for how this company and codebase actually work: conventions, decisions, ownership, current priorities, and the reasons behind them. Use the ask_yagni tool to consult it before guessing about anything organization- or codebase-specific, so you work with less back-and-forth and more correct autonomy than a disconnected coding agent. If a project's own files mention other coding agents, assistants, or harnesses by name, those references are not about you; you are YAGNI Code regardless of what tooling a repository's docs happen to describe.\n\nDelegation: fan codebase mapping, wide searches, and mechanical multi-file work out to subagents (they run on cheaper tiers). Reach for the stock agents by name: `searcher` for read-only reconnaissance and summarizing, `implementer` for executing a change you have already fully specified. Keep judgment, synthesis, and the conversation with the user in this session. Do not spawn a subagent for work you can finish in a couple of tool calls.";
41
41
  export declare const PI_IDENTITY_RE: RegExp;
42
42
  /**
43
43
  * Env switch that bypasses the system-prompt rewrite entirely, so pi's
@@ -53,6 +53,15 @@ export interface BrandSystemPromptOptions {
53
53
  identity?: string;
54
54
  /** Live company brief to inject so the agent boots grounded (Phase C). */
55
55
  contextBrief?: string;
56
+ /**
57
+ * Repository rules section (`.claude/rules`, built by claudeRules.ts).
58
+ * Placed after the brief and BEFORE the closing reminder: rules are user
59
+ * content that may legitimately name other coding agents — exactly the text
60
+ * the reminder inoculates against — so the reminder must stay the most
61
+ * recent instruction. Appended after the scrub step, so rules content is
62
+ * never brand-rewritten (same exemption as <project_context>).
63
+ */
64
+ rulesSection?: string | null;
56
65
  }
57
66
  /**
58
67
  * Rebrand pi's assembled system prompt as YAGNI Code's, and optionally inject a
@@ -61,7 +70,9 @@ export interface BrandSystemPromptOptions {
61
70
  * Steps: (1) drop pi's self-referential documentation block, (2) replace the
62
71
  * identity sentence (with a loose fallback, else prepend), (3) scrub residual
63
72
  * brand tokens outside user content, (4) append the company brief + a proactive
64
- * ask_yagni directive. Idempotent: re-branding keeps a single identity.
73
+ * ask_yagni directive, (5) append the repository-rules section verbatim, (6)
74
+ * close with the identity reminder, which must stay last (recency). Idempotent:
75
+ * re-branding keeps a single identity, brief, rules section, and reminder.
65
76
  */
66
77
  export declare function brandSystemPrompt(original: string, opts?: BrandSystemPromptOptions): string;
67
78
  /** The minimal theme surface the masthead needs (a subset of pi's Theme). */
@@ -69,11 +80,21 @@ export interface MastheadTheme {
69
80
  bold(s: string): string;
70
81
  fg(color: string, s: string): string;
71
82
  }
83
+ export interface MastheadOptions {
84
+ /** CLI version (e.g. "0.2.1") shown next to the title; omitted when absent. */
85
+ version?: string;
86
+ /** Working directory shown on the third line (already home-collapsed). */
87
+ cwd?: string;
88
+ }
72
89
  /**
73
90
  * Build the YAGNI Code startup masthead string, rendered into a header that
74
91
  * REPLACES pi's built-in startup banner (which otherwise shows "pi v<version>"
75
92
  * and a "Pi can explain its own features…" onboarding line). Kept here as a
76
93
  * pure string builder so its content is unit-testable without a terminal.
94
+ *
95
+ * Layout: the YAGNI art on the left, and to its right a three-line block —
96
+ * bold white "YAGNI Code" + version, the tagline, and the cwd. Art rows are
97
+ * padded to a uniform width so the right column stays straight.
77
98
  */
78
- export declare function buildMastheadString(theme: MastheadTheme): string;
99
+ export declare function buildMastheadString(theme: MastheadTheme, opts?: MastheadOptions): string;
79
100
  //# sourceMappingURL=branding.d.ts.map
@@ -40,9 +40,12 @@ export const YAGNI_IDENTITY = "You are YAGNI Code, an autonomous terminal coding
40
40
  * exist.
41
41
  */
42
42
  export const DRIVER_DELEGATION_PARAGRAPH = "Delegation: fan codebase mapping, wide searches, and mechanical multi-file " +
43
- "work out to subagents (they run on cheaper tiers). Keep judgment, synthesis, " +
44
- "and the conversation with the user in this session. Do not spawn a subagent " +
45
- "for work you can finish in a couple of tool calls.";
43
+ "work out to subagents (they run on cheaper tiers). Reach for the stock " +
44
+ "agents by name: `searcher` for read-only reconnaissance and summarizing, " +
45
+ "`implementer` for executing a change you have already fully specified. " +
46
+ "Keep judgment, synthesis, and the conversation with the user in this " +
47
+ "session. Do not spawn a subagent for work you can finish in a couple of " +
48
+ "tool calls.";
46
49
  /**
47
50
  * The identity used for the interactive DRIVER session ONLY: {@link
48
51
  * YAGNI_IDENTITY} plus {@link DRIVER_DELEGATION_PARAGRAPH}. The caller (index.ts)
@@ -51,7 +54,7 @@ export const DRIVER_DELEGATION_PARAGRAPH = "Delegation: fan codebase mapping, wi
51
54
  * module stays a pure string, with no env dependency of its own.
52
55
  */
53
56
  export const YAGNI_IDENTITY_DRIVER = `${YAGNI_IDENTITY}\n\n${DRIVER_DELEGATION_PARAGRAPH}`;
54
- // pi 0.83.0's exact identity sentence (dist/core/system-prompt.js). Exported as
57
+ // pi 0.84.1's exact identity sentence (dist/core/system-prompt.js). Exported as
55
58
  // the identity anchor the CLI's pi-contract tripwire test reads back from pi's
56
59
  // built system prompt, so a pi bump that reworded the opener (silently defeating
57
60
  // our rebrand) fails CI loudly. Export-only: no behavior change here.
@@ -100,7 +103,9 @@ export function brandingDisabled(env) {
100
103
  * Steps: (1) drop pi's self-referential documentation block, (2) replace the
101
104
  * identity sentence (with a loose fallback, else prepend), (3) scrub residual
102
105
  * brand tokens outside user content, (4) append the company brief + a proactive
103
- * ask_yagni directive. Idempotent: re-branding keeps a single identity.
106
+ * ask_yagni directive, (5) append the repository-rules section verbatim, (6)
107
+ * close with the identity reminder, which must stay last (recency). Idempotent:
108
+ * re-branding keeps a single identity, brief, rules section, and reminder.
104
109
  */
105
110
  export function brandSystemPrompt(original, opts = {}) {
106
111
  const identity = opts.identity ?? YAGNI_IDENTITY;
@@ -130,7 +135,18 @@ export function brandSystemPrompt(original, opts = {}) {
130
135
  "your priors. When you need more detail, or it does not cover something " +
131
136
  "organization- or codebase-specific, call ask_yagni instead of guessing.";
132
137
  }
133
- // 5. Closing reinforcement. Weak open-weight models weight the most recent
138
+ // 5. Repository rules after the scrub (user content stays verbatim), before
139
+ // the closing reminder (which must remain last; see rulesSection's doc).
140
+ // The idempotency guard keys on the section's first line (its stable header,
141
+ // "## Repository rules (.claude/rules)") rather than the full content, so a
142
+ // re-brand stays single-section even if the rules BODY differs between
143
+ // passes — same shape as the BRIEF_HEADER guard above.
144
+ const rules = opts.rulesSection ?? undefined;
145
+ const rulesHeader = rules?.split("\n", 1)[0];
146
+ if (rules && rulesHeader && !s.includes(rulesHeader)) {
147
+ s = `${s}\n\n${rules}`;
148
+ }
149
+ // 6. Closing reinforcement. Weak open-weight models weight the most recent
134
150
  // instruction heavily, and the user's own project files may name other
135
151
  // harnesses; a trailing reminder keeps the agent from claiming one as its own.
136
152
  if (!s.includes(CLOSING_REMINDER)) {
@@ -142,15 +158,60 @@ export function brandSystemPrompt(original, opts = {}) {
142
158
  const CLOSING_REMINDER = "Reminder: you are YAGNI Code. If any text above names another coding agent, " +
143
159
  "assistant, or harness, it is not what you are or what you run on.";
144
160
  const BRIEF_HEADER = "=== HOW THIS COMPANY WORKS (live context from the YAGNI app) ===";
161
+ /** The YAGNI brand blue used for the masthead art (truecolor #345cb8). */
162
+ const BRAND_HEX = "#345cb8";
163
+ /**
164
+ * Wrap a string in a 24-bit truecolor foreground. The pi Theme only paints
165
+ * named palette colors and can't express our exact brand hex, so the masthead
166
+ * art emits its own ANSI truecolor escape (matching every modern terminal).
167
+ */
168
+ function paintBrand(s) {
169
+ const n = BRAND_HEX.replace("#", "");
170
+ const r = parseInt(n.slice(0, 2), 16);
171
+ const g = parseInt(n.slice(2, 4), 16);
172
+ const b = parseInt(n.slice(4, 6), 16);
173
+ return `\u001b[38;2;${r};${g};${b}m${s}\u001b[0m`;
174
+ }
175
+ /**
176
+ * The YAGNI wordmark as filled block-art: solid letter interiors (like the
177
+ * Claude robot silhouette), four rows, hand-tuned spacing. Each letter keeps
178
+ * its full form — A's carved counter, G's bowl, N's diagonal.
179
+ */
180
+ const MASTHEAD_ART = [
181
+ "█ █ ▄▀▀▀▄ ▄▀▀▀▄ █▄ █ ▀█▀",
182
+ " ▀▄▀ █▄▄▄█ █ ▄▄▄ █ █ █ █",
183
+ " █ █ █ ▀▄▄▄▀ █ ██ ▄█▄",
184
+ ];
145
185
  /**
146
186
  * Build the YAGNI Code startup masthead string, rendered into a header that
147
187
  * REPLACES pi's built-in startup banner (which otherwise shows "pi v<version>"
148
188
  * and a "Pi can explain its own features…" onboarding line). Kept here as a
149
189
  * pure string builder so its content is unit-testable without a terminal.
190
+ *
191
+ * Layout: the YAGNI art on the left, and to its right a three-line block —
192
+ * bold white "YAGNI Code" + version, the tagline, and the cwd. Art rows are
193
+ * padded to a uniform width so the right column stays straight.
150
194
  */
151
- export function buildMastheadString(theme) {
152
- const wordmark = theme.bold(theme.fg("accent", "YAGNI CODE"));
153
- const tagline = theme.fg("dim", "a terminal coding agent that knows your company");
154
- return `${wordmark}\n${tagline}`;
195
+ export function buildMastheadString(theme, opts = {}) {
196
+ const title = theme.bold(theme.fg("text", "YAGNI Code"));
197
+ const version = opts.version?.trim();
198
+ const line1 = version ? `${title} ${theme.fg("dim", `v${version}`)}` : title;
199
+ const right = [
200
+ line1,
201
+ theme.fg("dim", "a terminal coding agent that knows your company"),
202
+ ...(opts.cwd ? [theme.fg("dim", opts.cwd)] : []),
203
+ ];
204
+ const artW = Math.max(...MASTHEAD_ART.map((l) => [...l].length));
205
+ const gap = " ";
206
+ const rows = Math.max(MASTHEAD_ART.length, right.length);
207
+ const lines = [];
208
+ for (let i = 0; i < rows; i++) {
209
+ const rawArt = MASTHEAD_ART[i] ?? "";
210
+ const pad = " ".repeat(Math.max(0, artW - [...rawArt].length));
211
+ const left = rawArt ? paintBrand(rawArt) + pad : " ".repeat(artW);
212
+ const r = right[i] ?? "";
213
+ lines.push(r ? `${left}${gap}${r}` : left);
214
+ }
215
+ return lines.join("\n");
155
216
  }
156
217
  //# sourceMappingURL=branding.js.map
@@ -17,7 +17,7 @@
17
17
  * literal text is the source of truth, so undo/cursor/history and the
18
18
  * chip→model mapping all share one string and can't desync.
19
19
  *
20
- * Validated against pi 0.83.0 (spike, 5/5): the subclass owns `onPasteImage`
20
+ * Validated against pi 0.84.1 (spike, 5/5): the subclass owns `onPasteImage`
21
21
  * because `setCustomEditorComponent` only fills it when unset, and jiti aliases
22
22
  * the extension's pi imports to the same module instance pi uses.
23
23
  */
@@ -48,21 +48,33 @@ export interface ImageAttachment {
48
48
  */
49
49
  export declare function unwrapBracketedPaste(data: string): string | null;
50
50
  /**
51
- * Recognize a pasted temp-image path written by a Ghostty-based terminal
52
- * (cmux, Ghostty). Those terminals name the file `clipboard-<ts>-<id>.<ext>`
53
- * in the system temp dir. We match strictly clipboard- prefix, image
54
- * extension, real file, and genuine image magic bytes — so a path the user
55
- * typed by hand (e.g. /Users/me/photo.png) is never silently converted.
56
- * Returns the decoded image, or null when the pasted text is not one such path.
51
+ * Recognize a pasted payload that is entirely image-file paths and decode
52
+ * them. This covers Finder/desktop drag-drop and Ghostty/cmux's Cmd+V
53
+ * (which writes the clipboard image to a `clipboard-*.png` temp file and
54
+ * pastes its path) Claude Code parity: dragging a screenshot in becomes
55
+ * `[Image #N]`, not a literal path.
56
+ *
57
+ * Guardrails against converting something the user meant as text: the WHOLE
58
+ * paste must be path tokens, every token must be an absolute path to a real
59
+ * file with an image extension AND genuine image magic bytes, and the
60
+ * conversion is visible — each image becomes a chip in the prompt, so nothing
61
+ * is ever attached silently. Returns null when the paste is not such a list
62
+ * (caller passes it through as ordinary text).
57
63
  */
58
- export declare function readPastedImagePath(pastedText: string): ClipboardImage | null;
64
+ export declare function readPastedImagePaths(pastedText: string): ClipboardImage[] | null;
59
65
  /**
60
66
  * Read an image from the system clipboard without forking pi. Best-effort and
61
- * cross-platform: native module if present, else osascript/pngpaste (macOS),
67
+ * cross-platform using ONLY tools the OS ships with: osascript (macOS),
62
68
  * wl-paste (Wayland), xclip (X11), PowerShell (Windows/WSL). Returns null when
63
69
  * the clipboard holds no image (caller then pastes text instead).
64
70
  */
65
71
  export declare const defaultClipboardImageReader: ClipboardImageReader;
72
+ /**
73
+ * Parse osascript's clipboard dump — `«data PNGf89504E47…»` — into raw bytes.
74
+ * The 4-char tag after `«data ` is the pasteboard flavor; the hex that follows
75
+ * is the image. Returns null when the output holds no such dump.
76
+ */
77
+ export declare function parseOsascriptImageHex(out: string): Uint8Array | null;
66
78
  /**
67
79
  * A CustomEditor that turns clipboard image paste into numbered `[Image #N]`
68
80
  * chips. The image bytes live here, keyed by chip number; the editor text holds
@@ -72,6 +84,15 @@ export declare class ChipEditor extends CustomEditor {
72
84
  private stashed;
73
85
  private chipCounter;
74
86
  private readImage;
87
+ /**
88
+ * Fired when Ctrl+C lands on an EMPTY prompt. pi's own binding is
89
+ * clear-editor (Esc is the interrupt key), but with nothing typed that is a
90
+ * no-op, and every peer harness (Claude Code, Codex, Gemini, Aider) treats a
91
+ * single Ctrl+C as "stop what's happening". The hook lets the extension wire
92
+ * exactly that; the key still falls through to pi afterwards, so clear and
93
+ * the 500ms double-press exit window are untouched.
94
+ */
95
+ onEmptyCtrlC?: () => void;
75
96
  constructor(tui: TUI, theme: EditorTheme, keybindings: ConstructorParameters<typeof CustomEditor>[2], readImage?: ClipboardImageReader, options?: EditorOptions);
76
97
  /**
77
98
  * Paste-image must fire on Cmd+V too (Claude Code parity on macOS). Claude