@yagni-app/code-staging 1.1.4-staging.1424.1 → 1.1.4-staging.1426.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/dist/extension/bgJobsPanel.js +3 -13
- package/dist/extension/decisionRubric.d.ts +7 -2
- package/dist/extension/decisionRubric.js +8 -2
- package/dist/extension/footer.d.ts +23 -2
- package/dist/extension/footer.js +23 -2
- package/dist/extension/index.js +31 -10
- package/dist/extension/modelPicker.d.ts +132 -0
- package/dist/extension/modelPicker.js +244 -0
- package/dist/extension/panelBorder.d.ts +13 -0
- package/dist/extension/panelBorder.js +17 -0
- package/dist/extension/recordDecisionTool.js +5 -1
- package/dist/extension/sandbox/panel.js +3 -14
- package/dist/extension/tierCommand.d.ts +52 -17
- package/dist/extension/tierCommand.js +103 -67
- package/package.json +2 -2
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { Box, Container, Key, Text, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
17
17
|
import { logEvent } from "./errorSink.js";
|
|
18
18
|
import { listBgJobs } from "./bgJobs.js";
|
|
19
|
+
import { PanelBorder } from "./panelBorder.js";
|
|
19
20
|
/** The content frame's horizontal padding (SandboxPanel's CONTENT_PADDING_X). */
|
|
20
21
|
export const CONTENT_PADDING_X = 2;
|
|
21
22
|
/** Max rendered output lines in the detail view. */
|
|
@@ -100,17 +101,6 @@ export function formatPanelSubtitle(jobs) {
|
|
|
100
101
|
}
|
|
101
102
|
// ---------------------------------------------------------------------------
|
|
102
103
|
// The panel: SandboxPanel's structure (Container + Border + Box + Border).
|
|
103
|
-
/** Full-width border line (the house pattern, color fn passed explicitly). */
|
|
104
|
-
class Border {
|
|
105
|
-
color;
|
|
106
|
-
constructor(color) {
|
|
107
|
-
this.color = color;
|
|
108
|
-
}
|
|
109
|
-
render(width) {
|
|
110
|
-
return [this.color("─".repeat(Math.max(1, width)))];
|
|
111
|
-
}
|
|
112
|
-
invalidate() { }
|
|
113
|
-
}
|
|
114
104
|
export class BgJobsPanel extends Container {
|
|
115
105
|
deps;
|
|
116
106
|
theme;
|
|
@@ -138,9 +128,9 @@ export class BgJobsPanel extends Container {
|
|
|
138
128
|
this.content.addChild(new Text("", 0, 0));
|
|
139
129
|
this.content.addChild(new Text("", 0, 0));
|
|
140
130
|
this.content.addChild(new Text("", 0, 0));
|
|
141
|
-
this.addChild(new
|
|
131
|
+
this.addChild(new PanelBorder((s) => theme.fg("borderMuted", s)));
|
|
142
132
|
this.addChild(this.content);
|
|
143
|
-
this.addChild(new
|
|
133
|
+
this.addChild(new PanelBorder((s) => theme.fg("borderMuted", s)));
|
|
144
134
|
this.rebuild();
|
|
145
135
|
}
|
|
146
136
|
dispose() {
|
|
@@ -19,7 +19,12 @@ export declare const RECORD_NEVER: string;
|
|
|
19
19
|
* How the decision text itself must read. The ledger shows the decision as
|
|
20
20
|
* the headline and the question beneath it, so an answer-shaped decision
|
|
21
21
|
* ("No. Surface the decline.") reads as a fragment on every surface that
|
|
22
|
-
* renders it
|
|
22
|
+
* renders it, and a decision that carries its own reasoning reads as a
|
|
23
|
+
* paragraph in headline type. The ruling is the decision; the constraints,
|
|
24
|
+
* specifics, and reasoning belong in the rationale, where retrieval indexes
|
|
25
|
+
* them just the same (the embedding document is question + decision +
|
|
26
|
+
* rationale) and contested detection, which compares decision against
|
|
27
|
+
* decision, is not muddied by shared context words.
|
|
23
28
|
*/
|
|
24
29
|
export declare const DECISION_STATEMENT: string;
|
|
25
30
|
/**
|
|
@@ -27,5 +32,5 @@ export declare const DECISION_STATEMENT: string;
|
|
|
27
32
|
* never, and how the decision text must read, so every surface that records
|
|
28
33
|
* a decision produces one the headline-first ledger can render whole.
|
|
29
34
|
*/
|
|
30
|
-
export declare const RECORD_DECISION_RUBRIC = "Record a decision only when it is consequential: a product-intent, architecture, or team-convention call that another engineer would need to know and that is not already written down, especially one a human just settled by answering a question or correcting you. Never record taste, styling, naming of a local variable, formatting, a workaround, a restatement of the code change, or anything the user is still experimenting with; when in doubt, do not record it. Write the decision as a standalone statement that reads without the question: name the subject and what was chosen (\"Checkout surfaces a declined card to the customer instead of retrying it\"), never a bare yes or no or an answer that only makes sense under the question.";
|
|
35
|
+
export declare const RECORD_DECISION_RUBRIC = "Record a decision only when it is consequential: a product-intent, architecture, or team-convention call that another engineer would need to know and that is not already written down, especially one a human just settled by answering a question or correcting you. Never record taste, styling, naming of a local variable, formatting, a workaround, a restatement of the code change, or anything the user is still experimenting with; when in doubt, do not record it. Write the decision as a standalone statement that reads without the question: name the subject and what was chosen (\"Checkout surfaces a declined card to the customer instead of retrying it\"), never a bare yes or no or an answer that only makes sense under the question. Keep the decision to the ruling itself, one or two sentences; put the constraints, specifics, and reasoning behind it in the rationale, not the decision.";
|
|
31
36
|
//# sourceMappingURL=decisionRubric.d.ts.map
|
|
@@ -25,12 +25,18 @@ export const RECORD_NEVER = "Never record taste, styling, naming of a local vari
|
|
|
25
25
|
* How the decision text itself must read. The ledger shows the decision as
|
|
26
26
|
* the headline and the question beneath it, so an answer-shaped decision
|
|
27
27
|
* ("No. Surface the decline.") reads as a fragment on every surface that
|
|
28
|
-
* renders it
|
|
28
|
+
* renders it, and a decision that carries its own reasoning reads as a
|
|
29
|
+
* paragraph in headline type. The ruling is the decision; the constraints,
|
|
30
|
+
* specifics, and reasoning belong in the rationale, where retrieval indexes
|
|
31
|
+
* them just the same (the embedding document is question + decision +
|
|
32
|
+
* rationale) and contested detection, which compares decision against
|
|
33
|
+
* decision, is not muddied by shared context words.
|
|
29
34
|
*/
|
|
30
35
|
export const DECISION_STATEMENT = "Write the decision as a standalone statement that reads without the question: name " +
|
|
31
36
|
"the subject and what was chosen (\"Checkout surfaces a declined card to the customer " +
|
|
32
37
|
"instead of retrying it\"), never a bare yes or no or an answer that only makes sense " +
|
|
33
|
-
"under the question."
|
|
38
|
+
"under the question. Keep the decision to the ruling itself, one or two sentences; put " +
|
|
39
|
+
"the constraints, specifics, and reasoning behind it in the rationale, not the decision.";
|
|
34
40
|
/**
|
|
35
41
|
* The rubric as one paragraph for persona bodies and task templates: when,
|
|
36
42
|
* never, and how the decision text must read, so every surface that records
|
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Renders three lines:
|
|
5
5
|
* 1. folder · [worktree] · branch (git context; ~-path fallback off-repo)
|
|
6
|
-
* 2. ↑in ↓out $cost · ctx%
|
|
6
|
+
* 2. ◆ tier · mode · ↑in ↓out $cost · ctx% (the rung in play, permission mode,
|
|
7
|
+
* session stats; context % is an integer)
|
|
7
8
|
* 3. extension statuses (branding, todo counter, mode) joined by " · "
|
|
8
9
|
*
|
|
10
|
+
* The tier segment is the footer's answer to "what am I driving on" now that
|
|
11
|
+
* the rung is a dial (/model, Ctrl+L, /tier): it reads `ctx.model` live on
|
|
12
|
+
* every render, so a switch shows the moment the footer repaints.
|
|
13
|
+
*
|
|
9
14
|
* --- How to customize the status bar (for future tickets) ---
|
|
10
15
|
*
|
|
11
16
|
* LIFECYCLE: `ctx.ui.setFooter()` is NOT available at extension factory time.
|
|
@@ -42,6 +47,17 @@ import type { ExtensionContext, ReadonlyFooterDataProvider, Theme } from "@earen
|
|
|
42
47
|
import type { ChildUsageHandle } from "./childUsage.js";
|
|
43
48
|
import type { ModeHolder, PermissionMode } from "./permission/gate.js";
|
|
44
49
|
export declare const BRANCH_MAX_WIDTH = 60;
|
|
50
|
+
/** The glyph in front of the tier segment (the desktop start chips' rung mark). */
|
|
51
|
+
export declare const TIER_GLYPH = "\u25C6";
|
|
52
|
+
/**
|
|
53
|
+
* The footer's tier label for the model a session is on: the rate-card name
|
|
54
|
+
* for a selectable rung, the catalog name (or id) for anything else, null
|
|
55
|
+
* when there is no model yet. PURE.
|
|
56
|
+
*/
|
|
57
|
+
export declare function footerTierLabel(model: {
|
|
58
|
+
id: string;
|
|
59
|
+
name?: string;
|
|
60
|
+
} | undefined): string | null;
|
|
45
61
|
export declare function cyclePermissionMode(current: PermissionMode): PermissionMode;
|
|
46
62
|
export declare function isShiftTab(data: string): boolean;
|
|
47
63
|
export declare const GIT_MUTATING_PATTERN: RegExp;
|
|
@@ -94,7 +110,12 @@ export declare function detectGitInfo(cwd: string, home: string | undefined): Gi
|
|
|
94
110
|
/** Pure line-builder, exported for tests. All data injected; colors via theme. */
|
|
95
111
|
export declare function renderFooterLines(input: {
|
|
96
112
|
git: GitInfo;
|
|
97
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* The rung in play, already resolved to its label ("Advanced"); leads
|
|
115
|
+
* line 2 as "◆ Advanced". Null hides the segment (no model yet).
|
|
116
|
+
*/
|
|
117
|
+
tier?: string | null;
|
|
118
|
+
/** Current permission mode; shown on line 2 after the tier as "<mode> mode". */
|
|
98
119
|
mode?: PermissionMode | null;
|
|
99
120
|
/** Formatted `+N -M` / `±`, or null when clean — appended to the branch as `[ … ]`. */
|
|
100
121
|
diff?: string | null;
|
package/dist/extension/footer.js
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Renders three lines:
|
|
5
5
|
* 1. folder · [worktree] · branch (git context; ~-path fallback off-repo)
|
|
6
|
-
* 2. ↑in ↓out $cost · ctx%
|
|
6
|
+
* 2. ◆ tier · mode · ↑in ↓out $cost · ctx% (the rung in play, permission mode,
|
|
7
|
+
* session stats; context % is an integer)
|
|
7
8
|
* 3. extension statuses (branding, todo counter, mode) joined by " · "
|
|
8
9
|
*
|
|
10
|
+
* The tier segment is the footer's answer to "what am I driving on" now that
|
|
11
|
+
* the rung is a dial (/model, Ctrl+L, /tier): it reads `ctx.model` live on
|
|
12
|
+
* every render, so a switch shows the moment the footer repaints.
|
|
13
|
+
*
|
|
9
14
|
* --- How to customize the status bar (for future tickets) ---
|
|
10
15
|
*
|
|
11
16
|
* LIFECYCLE: `ctx.ui.setFooter()` is NOT available at extension factory time.
|
|
@@ -43,10 +48,23 @@ import { statSync } from "node:fs";
|
|
|
43
48
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
44
49
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
45
50
|
import { createDiffStatCache, formatDiffStat } from "./diffStat.js";
|
|
51
|
+
import { isSelectableTier, tierLabel } from "./tierCommand.js";
|
|
46
52
|
export const BRANCH_MAX_WIDTH = 60;
|
|
47
53
|
const WORKTREE_MAX_WIDTH = 30;
|
|
48
54
|
/** Section separator: single space + middle dot + single space. */
|
|
49
55
|
const SEP = " · ";
|
|
56
|
+
/** The glyph in front of the tier segment (the desktop start chips' rung mark). */
|
|
57
|
+
export const TIER_GLYPH = "◆";
|
|
58
|
+
/**
|
|
59
|
+
* The footer's tier label for the model a session is on: the rate-card name
|
|
60
|
+
* for a selectable rung, the catalog name (or id) for anything else, null
|
|
61
|
+
* when there is no model yet. PURE.
|
|
62
|
+
*/
|
|
63
|
+
export function footerTierLabel(model) {
|
|
64
|
+
if (!model)
|
|
65
|
+
return null;
|
|
66
|
+
return isSelectableTier(model.id) ? tierLabel(model.id) : model.name || model.id;
|
|
67
|
+
}
|
|
50
68
|
const MODE_CYCLE = ["auto", "review", "plan"];
|
|
51
69
|
export function cyclePermissionMode(current) {
|
|
52
70
|
const idx = MODE_CYCLE.indexOf(current);
|
|
@@ -244,7 +262,7 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
244
262
|
}
|
|
245
263
|
}
|
|
246
264
|
const line1 = pad + truncateToWidth(line1Parts.join(sep), contentWidth, dim("…"));
|
|
247
|
-
// Line 2: [mode ·] ↑in ↓out $cost · ctx%
|
|
265
|
+
// Line 2: [◆ tier ·] [mode ·] ↑in ↓out $cost · ctx%
|
|
248
266
|
// Child-process spend (subagents, advisor, /go) joins the driver totals so
|
|
249
267
|
// the footer matches the per-child receipts and /cost's session scope.
|
|
250
268
|
const child = input.childUsage;
|
|
@@ -261,6 +279,8 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
261
279
|
const stats = statParts.join(" ");
|
|
262
280
|
const percentText = input.contextPercent === null ? "?" : `${Math.round(input.contextPercent)}%`;
|
|
263
281
|
const line2Parts = [];
|
|
282
|
+
if (input.tier)
|
|
283
|
+
line2Parts.push(theme.fg("accent", `${TIER_GLYPH} ${input.tier}`));
|
|
264
284
|
if (input.mode) {
|
|
265
285
|
const modeLabel = modeDisplay(input.mode);
|
|
266
286
|
line2Parts.push(theme.fg(modeLabel.color, modeLabel.text) + dim(" (shift+tab to change)"));
|
|
@@ -306,6 +326,7 @@ export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle, chil
|
|
|
306
326
|
.map(([, text]) => text);
|
|
307
327
|
return renderFooterLines({
|
|
308
328
|
git: gitInfo(),
|
|
329
|
+
tier: footerTierLabel(ctx.model),
|
|
309
330
|
mode: modeHolder?.get() ?? null,
|
|
310
331
|
diff: formatDiffStat(diffStatCache?.get() ?? null),
|
|
311
332
|
usage: collectUsage(ctx.sessionManager),
|
package/dist/extension/index.js
CHANGED
|
@@ -61,7 +61,8 @@ import { flushSpool as defaultFlushSpool } from "./spool.js";
|
|
|
61
61
|
import { makeAuthedFetch, makeTokenProvider } from "./tokenProvider.js";
|
|
62
62
|
import { attributionHeaders, fetchCatalog as defaultFetchCatalog, fetchContextBrief as defaultFetchContextBrief, getToken, getTokenExpiresAt as defaultGetTokenExpiresAt, getWorkspaceId as defaultGetWorkspaceId, isDriverCaller, resolveBaseUrl, tokenExpiryNotice, } from "./config.js";
|
|
63
63
|
import { buildYagniProvider } from "./provider.js";
|
|
64
|
-
import { orderDriverCatalog, registerStartTierGuard, registerTierCommand } from "./tierCommand.js";
|
|
64
|
+
import { orderDriverCatalog, registerStartTierGuard, registerTierCommand, trailReason } from "./tierCommand.js";
|
|
65
|
+
import { makeModelPickerListener } from "./modelPicker.js";
|
|
65
66
|
import { registerChipEditor } from "./chipEditor.js";
|
|
66
67
|
import { registerSlashCommandFilter } from "./slashCommandFilter.js";
|
|
67
68
|
import { defaultMineBeatGit, fileMineBeatMarkers, maybeOfferMiningBeat as defaultMaybeOfferMiningBeat, } from "./mineBeat.js";
|
|
@@ -1186,11 +1187,12 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
1186
1187
|
// count as costHud's "Excludes N earlier /go runs." note.
|
|
1187
1188
|
droppedSessionRuns,
|
|
1188
1189
|
});
|
|
1189
|
-
// The manual tier dial. `/model`
|
|
1190
|
-
//
|
|
1191
|
-
// sibling that reports and switches inline.
|
|
1192
|
-
// "movable" from becoming "parked on peak" —
|
|
1193
|
-
// child or eval lane names its tier on the
|
|
1190
|
+
// The manual tier dial. `/model` and Ctrl+L open YAGNI's own picker (the
|
|
1191
|
+
// terminal-input listener wired on session_start below, modelPicker.ts);
|
|
1192
|
+
// `/tier` is the tier-language sibling that reports and switches inline.
|
|
1193
|
+
// The start guard is what keeps "movable" from becoming "parked on peak" —
|
|
1194
|
+
// driver sessions only, since a child or eval lane names its tier on the
|
|
1195
|
+
// command line.
|
|
1194
1196
|
if (!evalMode && driver) {
|
|
1195
1197
|
registerTierCommand(pi);
|
|
1196
1198
|
registerStartTierGuard(pi);
|
|
@@ -1677,10 +1679,11 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
1677
1679
|
const mastheadCwd = formatCwd(process.cwd(), process.env.HOME);
|
|
1678
1680
|
ctx.ui?.setHeader?.((_tui, theme) => new Text(buildMastheadString(theme, { version: mastheadVersion, cwd: mastheadCwd })));
|
|
1679
1681
|
// Replace the built-in footer with the YAGNI status bar: folder +
|
|
1680
|
-
// [worktree] + branch on line 1
|
|
1681
|
-
// context % on line 2
|
|
1682
|
-
// line 3. The factory captures ctx so
|
|
1683
|
-
//
|
|
1682
|
+
// [worktree] + branch on line 1; the tier in play, the permission mode,
|
|
1683
|
+
// token/cost stats and integer context % on line 2; and extension
|
|
1684
|
+
// statuses (brand, todos, mode) on line 3. The factory captures ctx so
|
|
1685
|
+
// the footer can read session data (the model, token stats, context
|
|
1686
|
+
// usage) that isn't on the footerData provider.
|
|
1684
1687
|
ctx.ui?.setFooter?.((tui, theme, footerData) => createYagniFooterFactory(ctx, modeHolder, footerInvalidateHandle, childUsage)(tui, theme, footerData));
|
|
1685
1688
|
ctx.ui?.onTerminalInput?.((data) => {
|
|
1686
1689
|
if (isShiftTab(data)) {
|
|
@@ -1690,6 +1693,24 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
1690
1693
|
}
|
|
1691
1694
|
return undefined;
|
|
1692
1695
|
});
|
|
1696
|
+
// YAGNI's own model picker in place of pi's: Ctrl+L and Enter on a
|
|
1697
|
+
// `/model` line are consumed here, before the editor sees them, and
|
|
1698
|
+
// mount modelPicker.ts's panel. Driver sessions only, like /tier.
|
|
1699
|
+
if (!evalMode && driver && ctx.hasUI) {
|
|
1700
|
+
try {
|
|
1701
|
+
ctx.ui?.onTerminalInput?.(makeModelPickerListener(pi, ctx, { footer: footerInvalidateHandle }));
|
|
1702
|
+
}
|
|
1703
|
+
catch (err) {
|
|
1704
|
+
// The picker is a convenience over /tier; it must never break
|
|
1705
|
+
// session start. But a "Ctrl+L does nothing" report needs a trail.
|
|
1706
|
+
logEvent({
|
|
1707
|
+
source: "tier",
|
|
1708
|
+
level: "warn",
|
|
1709
|
+
event: "model_picker_register_failed",
|
|
1710
|
+
fields: { reason: trailReason(err) },
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1693
1714
|
// Label the collapsed chain-of-thought line so users know it is reasoning
|
|
1694
1715
|
// and how to reveal the full trace. Harmless when reasoning is expanded
|
|
1695
1716
|
// (the label only shows on hidden thinking blocks). Fails closed to pi's
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model picker: YAGNI's own `/model` and Ctrl+L.
|
|
3
|
+
*
|
|
4
|
+
* pi ships a model selector for `/model` and Ctrl+L, and with the four rungs
|
|
5
|
+
* registered it did list them. It also rendered them the way pi renders any
|
|
6
|
+
* provider's catalog: raw ids (`advanced`) under a lowercase provider badge
|
|
7
|
+
* (`[yagni]`), the rung in play hoisted to the top so the order changed with
|
|
8
|
+
* every switch, and a "Use /login to add providers" hint that is pi's
|
|
9
|
+
* multi-provider story, not this product's (YAGNI maintains the models a
|
|
10
|
+
* workspace can drive on; there is nothing to log in to). None of that is
|
|
11
|
+
* adjustable in place: the strings and the sort live inside pi's compiled
|
|
12
|
+
* component, `/model` is dispatched from pi's built-in command ladder before
|
|
13
|
+
* any extension command is consulted, and `app.model.select` is a reserved
|
|
14
|
+
* keybinding `registerShortcut` cannot take.
|
|
15
|
+
*
|
|
16
|
+
* What CAN be owned is the keystroke. Extension `onTerminalInput` listeners
|
|
17
|
+
* run before the focused editor sees anything (the same mechanism index.ts
|
|
18
|
+
* uses for shift+tab), so this module consumes Ctrl+L and an Enter on a
|
|
19
|
+
* `/model` line, and mounts its own panel through `ctx.ui.custom` in their
|
|
20
|
+
* place. The panel is built on the sandbox panel reference
|
|
21
|
+
* (`sandbox/panel.ts`): Border · padded Box · Border, a SelectList, pi-tui's
|
|
22
|
+
* `matchesKey` for every key. It lists the rate card's ladder in ladder order
|
|
23
|
+
* with the capitalized lane names, marks the rung in play and preselects it,
|
|
24
|
+
* and says "YAGNI" the way the product does.
|
|
25
|
+
*
|
|
26
|
+
* Switching goes through `switchTier`, the same path as `/tier`, so the
|
|
27
|
+
* trail entries and notices are one set. Headless (`--mode rpc`, print) is
|
|
28
|
+
* untouched: the desktop composer has its own pill, and there is no terminal
|
|
29
|
+
* input to listen to.
|
|
30
|
+
*/
|
|
31
|
+
import type { ExtensionAPI, ExtensionContext, TerminalInputHandler } from "@earendil-works/pi-coding-agent";
|
|
32
|
+
import { Container } from "@earendil-works/pi-tui";
|
|
33
|
+
import type { FooterInvalidateHandle } from "./footer.js";
|
|
34
|
+
import type { RenderTheme } from "./subagentRender.js";
|
|
35
|
+
import { type SelectableTier } from "./tierCommand.js";
|
|
36
|
+
/** The panel's title line. The provider word is the product's, capitalized. */
|
|
37
|
+
export declare const MODEL_PICKER_TITLE = "YAGNI model tier";
|
|
38
|
+
/** The panel's footer line: the keys that work, nothing about providers. */
|
|
39
|
+
export declare const MODEL_PICKER_FOOTER = "up/down to navigate \u00B7 Enter to select \u00B7 Esc to close";
|
|
40
|
+
/** One row of the picker. PURE data; the panel renders it. */
|
|
41
|
+
export interface PickerRow {
|
|
42
|
+
tier: SelectableTier;
|
|
43
|
+
/** The capitalized lane name, with {@link CURRENT_MARK} when in play. */
|
|
44
|
+
label: string;
|
|
45
|
+
description: string;
|
|
46
|
+
current: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The rows to offer: the ladder in {@link SELECTABLE_TIERS} order, filtered
|
|
50
|
+
* to the rungs the registry actually holds (a workspace whose catalog omits
|
|
51
|
+
* one must not get a row that fails on select), with the rung in play
|
|
52
|
+
* marked. PURE.
|
|
53
|
+
*/
|
|
54
|
+
export declare function pickerRows(available: readonly string[], currentId: string | undefined): PickerRow[];
|
|
55
|
+
/** What an Enter on the editor's current text is asking for. */
|
|
56
|
+
export type ModelCommand = {
|
|
57
|
+
kind: "open";
|
|
58
|
+
} | {
|
|
59
|
+
kind: "set";
|
|
60
|
+
tier: SelectableTier;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Read a `/model` line the way pi's own ladder would have: a bare `/model`
|
|
64
|
+
* opens the picker; `/model <rung>` switches directly when the argument
|
|
65
|
+
* names a rung; any other argument opens the picker (pi would have opened
|
|
66
|
+
* its selector pre-filtered; there are four rows here, so a filter buys
|
|
67
|
+
* nothing). Anything that is not a `/model` line is `null` and passes
|
|
68
|
+
* through to the editor untouched. PURE.
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseModelCommand(editorText: string): ModelCommand | null;
|
|
71
|
+
/** True for the keystroke pi binds to its own selector (`app.model.select`). */
|
|
72
|
+
export declare function isModelPickerKey(data: string): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The picker panel. Structure: top border · title · blank · rows · blank ·
|
|
75
|
+
* footer · bottom border. pi focuses this root and calls its handleInput;
|
|
76
|
+
* Esc closes with no choice, everything else goes to the SelectList, whose
|
|
77
|
+
* Enter resolves with the chosen rung.
|
|
78
|
+
*/
|
|
79
|
+
export declare class ModelPickerPanel extends Container {
|
|
80
|
+
private readonly list;
|
|
81
|
+
private readonly done;
|
|
82
|
+
constructor(rows: readonly PickerRow[], theme: RenderTheme, done: (tier?: SelectableTier) => void);
|
|
83
|
+
handleInput(data: string): void;
|
|
84
|
+
}
|
|
85
|
+
/** What the picker needs from the session beyond the pi API. */
|
|
86
|
+
export interface ModelPickerDeps {
|
|
87
|
+
/** Repaints the footer after a switch so its tier segment follows. */
|
|
88
|
+
footer?: Pick<FooterInvalidateHandle, "requestRender">;
|
|
89
|
+
}
|
|
90
|
+
/** The slice of the session context the picker reads. */
|
|
91
|
+
type PickerContext = Pick<ExtensionContext, "model" | "modelRegistry" | "hasUI" | "ui">;
|
|
92
|
+
/**
|
|
93
|
+
* Mount the panel in the editor slot and apply the choice. Resolves when the
|
|
94
|
+
* panel has closed and any switch has settled.
|
|
95
|
+
*/
|
|
96
|
+
export declare function openModelPicker(pi: ExtensionAPI, ctx: PickerContext, deps?: ModelPickerDeps): Promise<void>;
|
|
97
|
+
/** Mount the panel and resolve with the chosen rung, or undefined on Esc. */
|
|
98
|
+
export declare function pickTier(ctx: PickerContext): Promise<SelectableTier | undefined>;
|
|
99
|
+
/**
|
|
100
|
+
* The notice when either path the listener runs (opening the picker, or the
|
|
101
|
+
* direct switch behind `/model <rung>`) throws. Worded for both: what failed
|
|
102
|
+
* is the change of tier, whichever way it was asked for. The trail entry
|
|
103
|
+
* carries the reason.
|
|
104
|
+
*/
|
|
105
|
+
export declare const MODEL_PICKER_FAILED_NOTICE = "Could not change the model tier. Use /tier <rung> instead.";
|
|
106
|
+
/**
|
|
107
|
+
* The terminal-input listener, installed by index.ts on `session_start` (TUI
|
|
108
|
+
* driver sessions only, right after the shift+tab listener): Ctrl+L opens
|
|
109
|
+
* the picker; Enter on a `/model` line clears the editor and either switches
|
|
110
|
+
* or opens; everything else passes through.
|
|
111
|
+
*
|
|
112
|
+
* The listener tracks which of two things is in flight, because they want
|
|
113
|
+
* opposite treatment of Enter:
|
|
114
|
+
*
|
|
115
|
+
* - **A panel is up.** A further Ctrl+L is swallowed (pi's selector must
|
|
116
|
+
* not open on top, and the panel has no use for it), but Enter passes
|
|
117
|
+
* through UNTOUCHED. The panel is the focused component then, and it
|
|
118
|
+
* needs that Enter to select; the editor may still hold the `/model`
|
|
119
|
+
* line that opened it, and reading it here would eat the keystroke the
|
|
120
|
+
* panel is waiting for.
|
|
121
|
+
* - **A switch is in flight, no panel.** The editor is focused, so an
|
|
122
|
+
* Enter on a `/model` line that passed through would reach pi's own
|
|
123
|
+
* command ladder and open pi's selector, the very thing this module
|
|
124
|
+
* exists to prevent. That Enter is consumed but the line is NOT cleared:
|
|
125
|
+
* the text survives, and Enter acts on it once the switch has settled.
|
|
126
|
+
* Enter on any other text passes through as always.
|
|
127
|
+
*
|
|
128
|
+
* Nothing is cleared unless the listener actually acts on it.
|
|
129
|
+
*/
|
|
130
|
+
export declare function makeModelPickerListener(pi: ExtensionAPI, ctx: PickerContext, deps?: ModelPickerDeps): TerminalInputHandler;
|
|
131
|
+
export {};
|
|
132
|
+
//# sourceMappingURL=modelPicker.d.ts.map
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model picker: YAGNI's own `/model` and Ctrl+L.
|
|
3
|
+
*
|
|
4
|
+
* pi ships a model selector for `/model` and Ctrl+L, and with the four rungs
|
|
5
|
+
* registered it did list them. It also rendered them the way pi renders any
|
|
6
|
+
* provider's catalog: raw ids (`advanced`) under a lowercase provider badge
|
|
7
|
+
* (`[yagni]`), the rung in play hoisted to the top so the order changed with
|
|
8
|
+
* every switch, and a "Use /login to add providers" hint that is pi's
|
|
9
|
+
* multi-provider story, not this product's (YAGNI maintains the models a
|
|
10
|
+
* workspace can drive on; there is nothing to log in to). None of that is
|
|
11
|
+
* adjustable in place: the strings and the sort live inside pi's compiled
|
|
12
|
+
* component, `/model` is dispatched from pi's built-in command ladder before
|
|
13
|
+
* any extension command is consulted, and `app.model.select` is a reserved
|
|
14
|
+
* keybinding `registerShortcut` cannot take.
|
|
15
|
+
*
|
|
16
|
+
* What CAN be owned is the keystroke. Extension `onTerminalInput` listeners
|
|
17
|
+
* run before the focused editor sees anything (the same mechanism index.ts
|
|
18
|
+
* uses for shift+tab), so this module consumes Ctrl+L and an Enter on a
|
|
19
|
+
* `/model` line, and mounts its own panel through `ctx.ui.custom` in their
|
|
20
|
+
* place. The panel is built on the sandbox panel reference
|
|
21
|
+
* (`sandbox/panel.ts`): Border · padded Box · Border, a SelectList, pi-tui's
|
|
22
|
+
* `matchesKey` for every key. It lists the rate card's ladder in ladder order
|
|
23
|
+
* with the capitalized lane names, marks the rung in play and preselects it,
|
|
24
|
+
* and says "YAGNI" the way the product does.
|
|
25
|
+
*
|
|
26
|
+
* Switching goes through `switchTier`, the same path as `/tier`, so the
|
|
27
|
+
* trail entries and notices are one set. Headless (`--mode rpc`, print) is
|
|
28
|
+
* untouched: the desktop composer has its own pill, and there is no terminal
|
|
29
|
+
* input to listen to.
|
|
30
|
+
*/
|
|
31
|
+
import { Box, Container, Key, SelectList, Text, matchesKey, } from "@earendil-works/pi-tui";
|
|
32
|
+
import { logEvent } from "./errorSink.js";
|
|
33
|
+
import { PanelBorder } from "./panelBorder.js";
|
|
34
|
+
import { SELECTABLE_TIERS, TIER_BLURB, TIER_LABEL, YAGNI_PROVIDER, isSelectableTier, switchTier, trailReason, } from "./tierCommand.js";
|
|
35
|
+
/** The panel's title line. The provider word is the product's, capitalized. */
|
|
36
|
+
export const MODEL_PICKER_TITLE = "YAGNI model tier";
|
|
37
|
+
/** The panel's footer line: the keys that work, nothing about providers. */
|
|
38
|
+
export const MODEL_PICKER_FOOTER = "up/down to navigate · Enter to select · Esc to close";
|
|
39
|
+
/** The mark on the rung in play. */
|
|
40
|
+
const CURRENT_MARK = " ✓";
|
|
41
|
+
/**
|
|
42
|
+
* The rows to offer: the ladder in {@link SELECTABLE_TIERS} order, filtered
|
|
43
|
+
* to the rungs the registry actually holds (a workspace whose catalog omits
|
|
44
|
+
* one must not get a row that fails on select), with the rung in play
|
|
45
|
+
* marked. PURE.
|
|
46
|
+
*/
|
|
47
|
+
export function pickerRows(available, currentId) {
|
|
48
|
+
const have = new Set(available);
|
|
49
|
+
return SELECTABLE_TIERS.filter((tier) => have.has(tier)).map((tier) => {
|
|
50
|
+
const current = tier === currentId;
|
|
51
|
+
return {
|
|
52
|
+
tier,
|
|
53
|
+
label: current ? `${TIER_LABEL[tier]}${CURRENT_MARK}` : TIER_LABEL[tier],
|
|
54
|
+
description: TIER_BLURB[tier],
|
|
55
|
+
current,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read a `/model` line the way pi's own ladder would have: a bare `/model`
|
|
61
|
+
* opens the picker; `/model <rung>` switches directly when the argument
|
|
62
|
+
* names a rung; any other argument opens the picker (pi would have opened
|
|
63
|
+
* its selector pre-filtered; there are four rows here, so a filter buys
|
|
64
|
+
* nothing). Anything that is not a `/model` line is `null` and passes
|
|
65
|
+
* through to the editor untouched. PURE.
|
|
66
|
+
*/
|
|
67
|
+
export function parseModelCommand(editorText) {
|
|
68
|
+
const text = editorText.trim();
|
|
69
|
+
if (text === "/model")
|
|
70
|
+
return { kind: "open" };
|
|
71
|
+
if (!text.startsWith("/model "))
|
|
72
|
+
return null;
|
|
73
|
+
const arg = text.slice("/model ".length).trim().toLowerCase();
|
|
74
|
+
return isSelectableTier(arg) ? { kind: "set", tier: arg } : { kind: "open" };
|
|
75
|
+
}
|
|
76
|
+
/** True for the keystroke pi binds to its own selector (`app.model.select`). */
|
|
77
|
+
export function isModelPickerKey(data) {
|
|
78
|
+
return matchesKey(data, Key.ctrl("l"));
|
|
79
|
+
}
|
|
80
|
+
/** Horizontal breathing room for panel content (the sandbox panel's). */
|
|
81
|
+
const CONTENT_PADDING_X = 2;
|
|
82
|
+
function selectListTheme(theme) {
|
|
83
|
+
return {
|
|
84
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
85
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
86
|
+
description: (t) => theme.fg("muted", t),
|
|
87
|
+
scrollInfo: (t) => theme.fg("muted", t),
|
|
88
|
+
noMatch: (t) => theme.fg("muted", t),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The picker panel. Structure: top border · title · blank · rows · blank ·
|
|
93
|
+
* footer · bottom border. pi focuses this root and calls its handleInput;
|
|
94
|
+
* Esc closes with no choice, everything else goes to the SelectList, whose
|
|
95
|
+
* Enter resolves with the chosen rung.
|
|
96
|
+
*/
|
|
97
|
+
export class ModelPickerPanel extends Container {
|
|
98
|
+
list;
|
|
99
|
+
done;
|
|
100
|
+
constructor(rows, theme, done) {
|
|
101
|
+
super();
|
|
102
|
+
this.done = done;
|
|
103
|
+
const items = rows.map((row) => ({
|
|
104
|
+
value: row.tier,
|
|
105
|
+
label: row.label,
|
|
106
|
+
description: row.description,
|
|
107
|
+
}));
|
|
108
|
+
this.list = new SelectList(items, SELECTABLE_TIERS.length, selectListTheme(theme));
|
|
109
|
+
// The rung in play is where the cursor starts, never the first row: a
|
|
110
|
+
// stray Enter re-selects what the session already drives on.
|
|
111
|
+
const currentIndex = rows.findIndex((row) => row.current);
|
|
112
|
+
if (currentIndex >= 0)
|
|
113
|
+
this.list.setSelectedIndex(currentIndex);
|
|
114
|
+
this.list.onSelect = (item) => {
|
|
115
|
+
this.done(isSelectableTier(item.value) ? item.value : undefined);
|
|
116
|
+
};
|
|
117
|
+
this.list.onCancel = () => this.done();
|
|
118
|
+
const content = new Box(CONTENT_PADDING_X, 0);
|
|
119
|
+
content.addChild(new Text(theme.bold(MODEL_PICKER_TITLE), 0, 0));
|
|
120
|
+
content.addChild(new Text("", 0, 0));
|
|
121
|
+
content.addChild(this.list);
|
|
122
|
+
content.addChild(new Text(theme.fg("muted", MODEL_PICKER_FOOTER), 0, 1));
|
|
123
|
+
this.addChild(new PanelBorder((s) => theme.fg("borderMuted", s)));
|
|
124
|
+
this.addChild(content);
|
|
125
|
+
this.addChild(new PanelBorder((s) => theme.fg("borderMuted", s)));
|
|
126
|
+
}
|
|
127
|
+
handleInput(data) {
|
|
128
|
+
if (matchesKey(data, Key.escape)) {
|
|
129
|
+
this.done();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.list.handleInput(data);
|
|
133
|
+
this.invalidate();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Mount the panel in the editor slot and apply the choice. Resolves when the
|
|
138
|
+
* panel has closed and any switch has settled.
|
|
139
|
+
*/
|
|
140
|
+
export async function openModelPicker(pi, ctx, deps = {}) {
|
|
141
|
+
const tier = await pickTier(ctx);
|
|
142
|
+
if (!tier)
|
|
143
|
+
return;
|
|
144
|
+
await applyTier(pi, ctx, tier, deps);
|
|
145
|
+
}
|
|
146
|
+
/** Mount the panel and resolve with the chosen rung, or undefined on Esc. */
|
|
147
|
+
export async function pickTier(ctx) {
|
|
148
|
+
const available = SELECTABLE_TIERS.filter((tier) => ctx.modelRegistry.find(YAGNI_PROVIDER, tier) !== undefined);
|
|
149
|
+
const rows = pickerRows(available, ctx.model?.id);
|
|
150
|
+
if (rows.length === 0) {
|
|
151
|
+
if (ctx.hasUI)
|
|
152
|
+
ctx.ui.notify("No YAGNI model tiers are available on this workspace.", "error");
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
return ctx.ui.custom((_tui, theme, _keybindings, done) => new ModelPickerPanel(rows, theme, done));
|
|
156
|
+
}
|
|
157
|
+
/** Switch through the shared path and repaint the footer's tier segment. */
|
|
158
|
+
async function applyTier(pi, ctx, tier, deps) {
|
|
159
|
+
await switchTier(pi, ctx, tier);
|
|
160
|
+
deps.footer?.requestRender();
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The notice when either path the listener runs (opening the picker, or the
|
|
164
|
+
* direct switch behind `/model <rung>`) throws. Worded for both: what failed
|
|
165
|
+
* is the change of tier, whichever way it was asked for. The trail entry
|
|
166
|
+
* carries the reason.
|
|
167
|
+
*/
|
|
168
|
+
export const MODEL_PICKER_FAILED_NOTICE = "Could not change the model tier. Use /tier <rung> instead.";
|
|
169
|
+
/**
|
|
170
|
+
* The terminal-input listener, installed by index.ts on `session_start` (TUI
|
|
171
|
+
* driver sessions only, right after the shift+tab listener): Ctrl+L opens
|
|
172
|
+
* the picker; Enter on a `/model` line clears the editor and either switches
|
|
173
|
+
* or opens; everything else passes through.
|
|
174
|
+
*
|
|
175
|
+
* The listener tracks which of two things is in flight, because they want
|
|
176
|
+
* opposite treatment of Enter:
|
|
177
|
+
*
|
|
178
|
+
* - **A panel is up.** A further Ctrl+L is swallowed (pi's selector must
|
|
179
|
+
* not open on top, and the panel has no use for it), but Enter passes
|
|
180
|
+
* through UNTOUCHED. The panel is the focused component then, and it
|
|
181
|
+
* needs that Enter to select; the editor may still hold the `/model`
|
|
182
|
+
* line that opened it, and reading it here would eat the keystroke the
|
|
183
|
+
* panel is waiting for.
|
|
184
|
+
* - **A switch is in flight, no panel.** The editor is focused, so an
|
|
185
|
+
* Enter on a `/model` line that passed through would reach pi's own
|
|
186
|
+
* command ladder and open pi's selector, the very thing this module
|
|
187
|
+
* exists to prevent. That Enter is consumed but the line is NOT cleared:
|
|
188
|
+
* the text survives, and Enter acts on it once the switch has settled.
|
|
189
|
+
* Enter on any other text passes through as always.
|
|
190
|
+
*
|
|
191
|
+
* Nothing is cleared unless the listener actually acts on it.
|
|
192
|
+
*/
|
|
193
|
+
export function makeModelPickerListener(pi, ctx, deps = {}) {
|
|
194
|
+
let busy = null;
|
|
195
|
+
const run = (phase, work) => {
|
|
196
|
+
busy = phase;
|
|
197
|
+
void work()
|
|
198
|
+
.catch((err) => {
|
|
199
|
+
logEvent({ source: "tier", level: "warn", event: "model_picker_failed", fields: { reason: trailReason(err) } });
|
|
200
|
+
try {
|
|
201
|
+
if (ctx.hasUI)
|
|
202
|
+
ctx.ui.notify(MODEL_PICKER_FAILED_NOTICE, "error");
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* the trail entry is the durable half; a dead UI cannot undo it */
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
.finally(() => {
|
|
209
|
+
busy = null;
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
// The panel phase ends when the panel closes; the switch that follows a
|
|
213
|
+
// choice runs with the editor focused again, so it is its own phase.
|
|
214
|
+
const openThenSwitch = () => run("panel", async () => {
|
|
215
|
+
const tier = await pickTier(ctx);
|
|
216
|
+
if (!tier)
|
|
217
|
+
return;
|
|
218
|
+
busy = "switch";
|
|
219
|
+
await applyTier(pi, ctx, tier, deps);
|
|
220
|
+
});
|
|
221
|
+
return (data) => {
|
|
222
|
+
if (isModelPickerKey(data)) {
|
|
223
|
+
if (!busy)
|
|
224
|
+
openThenSwitch();
|
|
225
|
+
return { consume: true };
|
|
226
|
+
}
|
|
227
|
+
if (busy === "panel" || !matchesKey(data, Key.enter))
|
|
228
|
+
return undefined;
|
|
229
|
+
const command = parseModelCommand(ctx.ui.getEditorText());
|
|
230
|
+
if (!command)
|
|
231
|
+
return undefined;
|
|
232
|
+
if (busy === "switch")
|
|
233
|
+
return { consume: true };
|
|
234
|
+
ctx.ui.setEditorText("");
|
|
235
|
+
if (command.kind === "set") {
|
|
236
|
+
run("switch", () => applyTier(pi, ctx, command.tier, deps));
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
openThenSwitch();
|
|
240
|
+
}
|
|
241
|
+
return { consume: true };
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=modelPicker.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The full-width border line every YAGNI panel frames itself with (the house
|
|
3
|
+
* DynamicBorder pattern: Border · padded Box · Border). The color function
|
|
4
|
+
* is passed in explicitly because extensions never read pi's global theme.
|
|
5
|
+
* One copy, shared by the sandbox, background-jobs, and model picker panels.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PanelBorder {
|
|
8
|
+
private readonly color;
|
|
9
|
+
constructor(color: (s: string) => string);
|
|
10
|
+
render(width: number): string[];
|
|
11
|
+
invalidate(): void;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=panelBorder.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The full-width border line every YAGNI panel frames itself with (the house
|
|
3
|
+
* DynamicBorder pattern: Border · padded Box · Border). The color function
|
|
4
|
+
* is passed in explicitly because extensions never read pi's global theme.
|
|
5
|
+
* One copy, shared by the sandbox, background-jobs, and model picker panels.
|
|
6
|
+
*/
|
|
7
|
+
export class PanelBorder {
|
|
8
|
+
color;
|
|
9
|
+
constructor(color) {
|
|
10
|
+
this.color = color;
|
|
11
|
+
}
|
|
12
|
+
render(width) {
|
|
13
|
+
return [this.color("─".repeat(Math.max(1, width)))];
|
|
14
|
+
}
|
|
15
|
+
invalidate() { }
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=panelBorder.js.map
|
|
@@ -33,7 +33,11 @@ const parameters = Type.Object({
|
|
|
33
33
|
question: Type.String({ description: "The question that was decided, as another engineer would ask it." }),
|
|
34
34
|
decision: Type.String({ description: `The direction taken. ${DECISION_STATEMENT}` }),
|
|
35
35
|
kind: Type.Union([Type.Literal("product"), Type.Literal("architecture"), Type.Literal("convention")], { description: `What sort of call this is: ${DECISION_KIND_DESCRIPTION}.` }),
|
|
36
|
-
rationale: Type.String({
|
|
36
|
+
rationale: Type.String({
|
|
37
|
+
description: "Why: the durable reasoning a teammate would want six months later, plus the constraints and " +
|
|
38
|
+
"specifics that do not fit the one- or two-sentence decision. This is indexed for retrieval " +
|
|
39
|
+
"alongside the decision, so nothing is lost by putting detail here.",
|
|
40
|
+
}),
|
|
37
41
|
repo: Type.Optional(Type.String()),
|
|
38
42
|
workItemId: Type.Optional(Type.String()),
|
|
39
43
|
});
|
|
@@ -22,6 +22,7 @@ import { Box, Container, HStack, Key, SelectList, Text, matchesKey, } from "@ear
|
|
|
22
22
|
import { mergeRulesIntoSandbox } from "./config.js";
|
|
23
23
|
import { resolveWorktreeGitAccess } from "./worktreeGit.js";
|
|
24
24
|
import { logEvent } from "../errorSink.js";
|
|
25
|
+
import { PanelBorder } from "../panelBorder.js";
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
// Pure derivation helpers (unit-tested; no TUI dependency)
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
@@ -247,18 +248,6 @@ function selectListTheme(theme) {
|
|
|
247
248
|
noMatch: (t) => theme.fg("muted", t),
|
|
248
249
|
};
|
|
249
250
|
}
|
|
250
|
-
/** Full-width border line (the house DynamicBorder pattern, color fn passed
|
|
251
|
-
* explicitly — extensions never read pi's global theme). */
|
|
252
|
-
class Border {
|
|
253
|
-
color;
|
|
254
|
-
constructor(color) {
|
|
255
|
-
this.color = color;
|
|
256
|
-
}
|
|
257
|
-
render(width) {
|
|
258
|
-
return [this.color("─".repeat(Math.max(1, width)))];
|
|
259
|
-
}
|
|
260
|
-
invalidate() { }
|
|
261
|
-
}
|
|
262
251
|
/**
|
|
263
252
|
* The /sandbox panel. Structure: top border · tab strip · blank · tab
|
|
264
253
|
* content · blank · footer · bottom border. pi focuses this root and calls
|
|
@@ -288,9 +277,9 @@ export class SandboxPanel extends Container {
|
|
|
288
277
|
this.content = new Box(CONTENT_PADDING_X, 0);
|
|
289
278
|
this.content.addChild(new Text("", 0, 0)); // tab strip placeholder
|
|
290
279
|
this.content.addChild(new Text("", 0, 0)); // body placeholder
|
|
291
|
-
this.addChild(new
|
|
280
|
+
this.addChild(new PanelBorder((s) => theme.fg("borderMuted", s)));
|
|
292
281
|
this.addChild(this.content);
|
|
293
|
-
this.addChild(new
|
|
282
|
+
this.addChild(new PanelBorder((s) => theme.fg("borderMuted", s)));
|
|
294
283
|
this.setTab(this.activeTab);
|
|
295
284
|
}
|
|
296
285
|
setTab(tab) {
|
|
@@ -45,23 +45,40 @@
|
|
|
45
45
|
* session on peak" after the fact. A rung that failed to move is invisible in
|
|
46
46
|
* every other record, which is why the failures log at all.
|
|
47
47
|
*/
|
|
48
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
48
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
49
49
|
/** The provider the tier models are registered under (see provider.ts). */
|
|
50
50
|
export declare const YAGNI_PROVIDER = "yagni";
|
|
51
51
|
/**
|
|
52
|
-
* The rungs a driver session may sit on, in PICKER ORDER
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
52
|
+
* The rungs a driver session may sit on, in PICKER ORDER: the rate card's
|
|
53
|
+
* ladder, strongest first (`peak`, `advanced`, `standard`, `efficient`).
|
|
54
|
+
*
|
|
55
|
+
* The list reads the same way everywhere the tiers are named (the pricing
|
|
56
|
+
* page, the desktop start screen, the catalog), so the picker does not invent
|
|
57
|
+
* a second ordering. Peak leading the list is safe because list position is
|
|
58
|
+
* not what keeps a session off it: {@link registerStartTierGuard} refuses to
|
|
59
|
+
* OPEN on peak, and the picker preselects the rung in play rather than the
|
|
60
|
+
* first row, so a stray Enter never lands on peak by accident.
|
|
59
61
|
*/
|
|
60
|
-
export declare const SELECTABLE_TIERS: readonly ["
|
|
62
|
+
export declare const SELECTABLE_TIERS: readonly ["peak", "advanced", "standard", "efficient"];
|
|
61
63
|
export type SelectableTier = (typeof SELECTABLE_TIERS)[number];
|
|
62
64
|
/** The rung every interactive driver session opens on. */
|
|
63
65
|
export declare const SESSION_START_TIER: SelectableTier;
|
|
66
|
+
/**
|
|
67
|
+
* The customer-facing name of each rung: the rate card's capitalized lane
|
|
68
|
+
* names, the same words the backend catalog serves as `name`. Ids stay
|
|
69
|
+
* lowercase on the wire and in `/tier <arg>`; only what a person reads
|
|
70
|
+
* changes.
|
|
71
|
+
*/
|
|
72
|
+
export declare const TIER_LABEL: Record<SelectableTier, string>;
|
|
73
|
+
/** One-line description per rung, for `/tier` with no argument. */
|
|
74
|
+
export declare const TIER_BLURB: Record<SelectableTier, string>;
|
|
64
75
|
export declare function isSelectableTier(value: string | undefined): value is SelectableTier;
|
|
76
|
+
/**
|
|
77
|
+
* The label a person reads for a model id: the rate-card name for a
|
|
78
|
+
* selectable rung, the id itself for anything else the session may be on
|
|
79
|
+
* (`balanced`, a child's exact model), and "unknown" when there is none.
|
|
80
|
+
*/
|
|
81
|
+
export declare function tierLabel(id: string | undefined): string;
|
|
65
82
|
/**
|
|
66
83
|
* The driver's picker catalog: the four rungs, in {@link SELECTABLE_TIERS}
|
|
67
84
|
* order, filtered against what the backend actually served.
|
|
@@ -138,17 +155,35 @@ export declare function formatTierSwitchFailed(requested: SelectableTier, curren
|
|
|
138
155
|
/** The notice shown when a session is corrected off a never-start rung. PURE. */
|
|
139
156
|
export declare function formatStartCorrection(from: string, to: SelectableTier): string;
|
|
140
157
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
* The slice of a command or session context a switch needs: the rung in
|
|
159
|
+
* play, the registry to resolve the target in, and a way to tell the
|
|
160
|
+
* developer what happened. Both `ExtensionCommandContext` and the
|
|
161
|
+
* `session_start` `ExtensionContext` satisfy it, which is what lets `/tier`
|
|
162
|
+
* and the model picker share one switch path.
|
|
163
|
+
*/
|
|
164
|
+
export type TierSwitchContext = Pick<ExtensionCommandContext, "model" | "modelRegistry" | "hasUI" | "ui">;
|
|
165
|
+
/**
|
|
166
|
+
* Move the session to `tier`, the one switch path behind `/tier <rung>` and
|
|
167
|
+
* the model picker. Returns true when the rung moved.
|
|
148
168
|
*
|
|
149
169
|
* `setModel` resolves through `ctx.modelRegistry.find`, so a rung the
|
|
150
170
|
* workspace's catalog never registered fails with a real message instead of
|
|
151
|
-
* silently selecting nothing.
|
|
171
|
+
* silently selecting nothing. Mirrors the start guard's posture: a switch
|
|
172
|
+
* that does not land is logged AND named. `setModel` reaches a provider, so
|
|
173
|
+
* it can reject or throw; an uncaught throw here would escape the caller, and
|
|
174
|
+
* a silent one would leave a developer believing they moved rungs when they
|
|
175
|
+
* did not. Both end on the same durable trail entry.
|
|
176
|
+
*/
|
|
177
|
+
export declare function switchTier(pi: ExtensionAPI, ctx: TierSwitchContext, tier: SelectableTier): Promise<boolean>;
|
|
178
|
+
/**
|
|
179
|
+
* Register `/tier`.
|
|
180
|
+
*
|
|
181
|
+
* The picker itself (`/model`, Ctrl+L) is YAGNI's own since modelPicker.ts:
|
|
182
|
+
* pi's built-in selector renders raw ids under a lowercase provider badge and
|
|
183
|
+
* a "/login to add providers" hint that has no meaning here, and it cannot be
|
|
184
|
+
* adjusted in place. `/tier` is the tier-language sibling: it reports the
|
|
185
|
+
* current rung inline without opening a picker, and switches in one line when
|
|
186
|
+
* the rung is already known. Both go through {@link switchTier}.
|
|
152
187
|
*/
|
|
153
188
|
export declare function registerTierCommand(pi: ExtensionAPI): void;
|
|
154
189
|
/**
|
|
@@ -50,29 +50,53 @@ import { scrubSecrets } from "./pipeline/scrubSecrets.js";
|
|
|
50
50
|
/** The provider the tier models are registered under (see provider.ts). */
|
|
51
51
|
export const YAGNI_PROVIDER = "yagni";
|
|
52
52
|
/**
|
|
53
|
-
* The rungs a driver session may sit on, in PICKER ORDER
|
|
53
|
+
* The rungs a driver session may sit on, in PICKER ORDER: the rate card's
|
|
54
|
+
* ladder, strongest first (`peak`, `advanced`, `standard`, `efficient`).
|
|
54
55
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* the
|
|
56
|
+
* The list reads the same way everywhere the tiers are named (the pricing
|
|
57
|
+
* page, the desktop start screen, the catalog), so the picker does not invent
|
|
58
|
+
* a second ordering. Peak leading the list is safe because list position is
|
|
59
|
+
* not what keeps a session off it: {@link registerStartTierGuard} refuses to
|
|
60
|
+
* OPEN on peak, and the picker preselects the rung in play rather than the
|
|
61
|
+
* first row, so a stray Enter never lands on peak by accident.
|
|
60
62
|
*/
|
|
61
|
-
export const SELECTABLE_TIERS = ["
|
|
63
|
+
export const SELECTABLE_TIERS = ["peak", "advanced", "standard", "efficient"];
|
|
62
64
|
/** The rung every interactive driver session opens on. */
|
|
63
65
|
export const SESSION_START_TIER = "advanced";
|
|
64
66
|
/** Rungs a session must never be left parked on at start. Peak only, today. */
|
|
65
67
|
const NEVER_START_TIERS = new Set(["peak"]);
|
|
68
|
+
/**
|
|
69
|
+
* The customer-facing name of each rung: the rate card's capitalized lane
|
|
70
|
+
* names, the same words the backend catalog serves as `name`. Ids stay
|
|
71
|
+
* lowercase on the wire and in `/tier <arg>`; only what a person reads
|
|
72
|
+
* changes.
|
|
73
|
+
*/
|
|
74
|
+
export const TIER_LABEL = {
|
|
75
|
+
peak: "Peak",
|
|
76
|
+
advanced: "Advanced",
|
|
77
|
+
standard: "Standard",
|
|
78
|
+
efficient: "Efficient",
|
|
79
|
+
};
|
|
66
80
|
/** One-line description per rung, for `/tier` with no argument. */
|
|
67
|
-
const TIER_BLURB = {
|
|
81
|
+
export const TIER_BLURB = {
|
|
82
|
+
peak: "strongest judgment, several times the spend",
|
|
68
83
|
advanced: "the default driver rung",
|
|
69
84
|
standard: "cheaper, for mechanical work",
|
|
70
85
|
efficient: "cheapest, for search and triage",
|
|
71
|
-
peak: "strongest judgment, several times the spend",
|
|
72
86
|
};
|
|
73
87
|
export function isSelectableTier(value) {
|
|
74
88
|
return !!value && SELECTABLE_TIERS.includes(value);
|
|
75
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The label a person reads for a model id: the rate-card name for a
|
|
92
|
+
* selectable rung, the id itself for anything else the session may be on
|
|
93
|
+
* (`balanced`, a child's exact model), and "unknown" when there is none.
|
|
94
|
+
*/
|
|
95
|
+
export function tierLabel(id) {
|
|
96
|
+
if (isSelectableTier(id))
|
|
97
|
+
return TIER_LABEL[id];
|
|
98
|
+
return id ?? "unknown";
|
|
99
|
+
}
|
|
76
100
|
/**
|
|
77
101
|
* The driver's picker catalog: the four rungs, in {@link SELECTABLE_TIERS}
|
|
78
102
|
* order, filtered against what the backend actually served.
|
|
@@ -121,17 +145,17 @@ export function parseTierArg(args) {
|
|
|
121
145
|
export function formatTierStatus(currentId) {
|
|
122
146
|
const current = isSelectableTier(currentId) ? currentId : undefined;
|
|
123
147
|
const head = current
|
|
124
|
-
? `This session is on ${current} (${TIER_BLURB[current]}).`
|
|
148
|
+
? `This session is on ${TIER_LABEL[current]} (${TIER_BLURB[current]}).`
|
|
125
149
|
: `This session is on "${currentId ?? "unknown"}", which is not one of the selectable rungs.`;
|
|
126
|
-
const options = SELECTABLE_TIERS.map((t) => ` /tier ${t} - ${TIER_BLURB[t]}`).join("\n");
|
|
127
|
-
return `${head}\nSwitch with /tier <rung>, or pick from /model:\n${options}\nEvery session starts on ${SESSION_START_TIER}.`;
|
|
150
|
+
const options = SELECTABLE_TIERS.map((t) => ` /tier ${t} - ${TIER_LABEL[t]}: ${TIER_BLURB[t]}`).join("\n");
|
|
151
|
+
return `${head}\nSwitch with /tier <rung>, or pick from /model (Ctrl+L):\n${options}\nEvery session starts on ${TIER_LABEL[SESSION_START_TIER]}.`;
|
|
128
152
|
}
|
|
129
153
|
/** The notice shown when a switch lands. PURE. */
|
|
130
154
|
export function formatTierSwitched(tier) {
|
|
131
155
|
const parked = tier === "peak"
|
|
132
|
-
?
|
|
156
|
+
? ` Peak is priced for judgment, not for driving: the next session starts back on ${TIER_LABEL[SESSION_START_TIER]}.`
|
|
133
157
|
: "";
|
|
134
|
-
return `Session tier is now ${tier} (${TIER_BLURB[tier]}).${parked}`;
|
|
158
|
+
return `Session tier is now ${TIER_LABEL[tier]} (${TIER_BLURB[tier]}).${parked}`;
|
|
135
159
|
}
|
|
136
160
|
/** Upper bound on a trail `reason`, in characters. */
|
|
137
161
|
export const TRAIL_REASON_MAX = 180;
|
|
@@ -207,29 +231,80 @@ export function trailReason(err) {
|
|
|
207
231
|
}
|
|
208
232
|
/** The notice shown when a requested switch did not land. PURE. */
|
|
209
233
|
export function formatTierSwitchFailed(requested, currentId) {
|
|
210
|
-
const still = currentId ? ` Still on ${currentId}.` : "";
|
|
211
|
-
return `Could not switch to ${requested}.${still}`;
|
|
234
|
+
const still = currentId ? ` Still on ${tierLabel(currentId)}.` : "";
|
|
235
|
+
return `Could not switch to ${TIER_LABEL[requested]}.${still}`;
|
|
212
236
|
}
|
|
213
237
|
/** The notice shown when a session is corrected off a never-start rung. PURE. */
|
|
214
238
|
export function formatStartCorrection(from, to) {
|
|
215
|
-
return `Opened on ${to} rather than ${from}: sessions always start on ${to}. Use /tier ${from} to go back.`;
|
|
239
|
+
return `Opened on ${TIER_LABEL[to]} rather than ${tierLabel(from)}: sessions always start on ${TIER_LABEL[to]}. Use /tier ${from} to go back.`;
|
|
216
240
|
}
|
|
217
241
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* pi already ships `/model` (and Ctrl+P), which now lists the four rungs
|
|
221
|
-
* because the driver catalog carries them — that surface is left entirely to
|
|
222
|
-
* pi rather than shadowed with a same-named command. `/tier` is the
|
|
223
|
-
* tier-language sibling: it reports the current rung inline without opening a
|
|
224
|
-
* picker, and switches in one line when the rung is already known.
|
|
242
|
+
* Move the session to `tier`, the one switch path behind `/tier <rung>` and
|
|
243
|
+
* the model picker. Returns true when the rung moved.
|
|
225
244
|
*
|
|
226
245
|
* `setModel` resolves through `ctx.modelRegistry.find`, so a rung the
|
|
227
246
|
* workspace's catalog never registered fails with a real message instead of
|
|
228
|
-
* silently selecting nothing.
|
|
247
|
+
* silently selecting nothing. Mirrors the start guard's posture: a switch
|
|
248
|
+
* that does not land is logged AND named. `setModel` reaches a provider, so
|
|
249
|
+
* it can reject or throw; an uncaught throw here would escape the caller, and
|
|
250
|
+
* a silent one would leave a developer believing they moved rungs when they
|
|
251
|
+
* did not. Both end on the same durable trail entry.
|
|
252
|
+
*/
|
|
253
|
+
export async function switchTier(pi, ctx, tier) {
|
|
254
|
+
const notify = (message, type) => {
|
|
255
|
+
if (ctx.hasUI)
|
|
256
|
+
ctx.ui.notify(message, type);
|
|
257
|
+
};
|
|
258
|
+
if (ctx.model?.id === tier) {
|
|
259
|
+
notify(`Already on ${TIER_LABEL[tier]}.`, "info");
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const model = ctx.modelRegistry.find(YAGNI_PROVIDER, tier);
|
|
263
|
+
if (!model) {
|
|
264
|
+
logEvent({
|
|
265
|
+
source: "tier",
|
|
266
|
+
level: "warn",
|
|
267
|
+
event: "tier_switch_failed",
|
|
268
|
+
fields: { from: ctx.model?.id ?? "unknown", to: tier, reason: "registry_miss" },
|
|
269
|
+
});
|
|
270
|
+
notify(`The ${TIER_LABEL[tier]} tier is not available on this workspace.`, "error");
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
let ok = false;
|
|
274
|
+
let reason = "set_model_rejected";
|
|
275
|
+
try {
|
|
276
|
+
ok = await pi.setModel(model);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
reason = trailReason(err);
|
|
280
|
+
}
|
|
281
|
+
if (!ok) {
|
|
282
|
+
logEvent({
|
|
283
|
+
source: "tier",
|
|
284
|
+
level: "warn",
|
|
285
|
+
event: "tier_switch_failed",
|
|
286
|
+
fields: { from: ctx.model?.id ?? "unknown", to: tier, reason },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
// A failed switch still has to leave the rung in play legible: that is
|
|
290
|
+
// the whole feature, and "could not switch" alone tells you nothing
|
|
291
|
+
// about what you are now driving on.
|
|
292
|
+
notify(ok ? formatTierSwitched(tier) : formatTierSwitchFailed(tier, ctx.model?.id), ok ? "info" : "error");
|
|
293
|
+
return ok;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Register `/tier`.
|
|
297
|
+
*
|
|
298
|
+
* The picker itself (`/model`, Ctrl+L) is YAGNI's own since modelPicker.ts:
|
|
299
|
+
* pi's built-in selector renders raw ids under a lowercase provider badge and
|
|
300
|
+
* a "/login to add providers" hint that has no meaning here, and it cannot be
|
|
301
|
+
* adjusted in place. `/tier` is the tier-language sibling: it reports the
|
|
302
|
+
* current rung inline without opening a picker, and switches in one line when
|
|
303
|
+
* the rung is already known. Both go through {@link switchTier}.
|
|
229
304
|
*/
|
|
230
305
|
export function registerTierCommand(pi) {
|
|
231
306
|
pi.registerCommand("tier", {
|
|
232
|
-
description: "Show or switch this session's model tier (advanced / standard / efficient
|
|
307
|
+
description: "Show or switch this session's model tier (peak / advanced / standard / efficient).",
|
|
233
308
|
handler: async (args, ctx) => {
|
|
234
309
|
const notify = (message, type) => {
|
|
235
310
|
if (ctx.hasUI)
|
|
@@ -244,46 +319,7 @@ export function registerTierCommand(pi) {
|
|
|
244
319
|
notify(`"${request.input}" is not a tier. Choose one of: ${SELECTABLE_TIERS.join(", ")}.`, "warning");
|
|
245
320
|
return;
|
|
246
321
|
}
|
|
247
|
-
|
|
248
|
-
notify(`Already on ${request.tier}.`, "info");
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
const model = ctx.modelRegistry.find(YAGNI_PROVIDER, request.tier);
|
|
252
|
-
if (!model) {
|
|
253
|
-
logEvent({
|
|
254
|
-
source: "tier",
|
|
255
|
-
level: "warn",
|
|
256
|
-
event: "tier_switch_failed",
|
|
257
|
-
fields: { from: ctx.model?.id ?? "unknown", to: request.tier, reason: "registry_miss" },
|
|
258
|
-
});
|
|
259
|
-
notify(`The ${request.tier} tier is not available on this workspace.`, "error");
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
// Mirrors the start guard's posture: a switch that does not land is
|
|
263
|
-
// logged AND named. `setModel` reaches a provider, so it can reject or
|
|
264
|
-
// throw; an uncaught throw here would escape the command handler, and a
|
|
265
|
-
// silent one would leave a developer believing they moved rungs when
|
|
266
|
-
// they did not. Both end on the same durable trail entry.
|
|
267
|
-
let ok = false;
|
|
268
|
-
let reason = "set_model_rejected";
|
|
269
|
-
try {
|
|
270
|
-
ok = await pi.setModel(model);
|
|
271
|
-
}
|
|
272
|
-
catch (err) {
|
|
273
|
-
reason = trailReason(err);
|
|
274
|
-
}
|
|
275
|
-
if (!ok) {
|
|
276
|
-
logEvent({
|
|
277
|
-
source: "tier",
|
|
278
|
-
level: "warn",
|
|
279
|
-
event: "tier_switch_failed",
|
|
280
|
-
fields: { from: ctx.model?.id ?? "unknown", to: request.tier, reason },
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
// A failed switch still has to leave the rung in play legible: that is
|
|
284
|
-
// the whole feature, and "could not switch" alone tells you nothing
|
|
285
|
-
// about what you are now driving on.
|
|
286
|
-
notify(ok ? formatTierSwitched(request.tier) : formatTierSwitchFailed(request.tier, ctx.model?.id), ok ? "info" : "error");
|
|
322
|
+
await switchTier(pi, ctx, request.tier);
|
|
287
323
|
},
|
|
288
324
|
});
|
|
289
325
|
}
|
|
@@ -298,7 +334,7 @@ export function registerTierCommand(pi) {
|
|
|
298
334
|
* fused the two and read as though pricing had caused the failure.
|
|
299
335
|
*/
|
|
300
336
|
export function formatStartCorrectionFailed(from) {
|
|
301
|
-
return `Could not move this session off ${from}. That rung is priced for judgment rather than driving, so use /tier ${SESSION_START_TIER} unless you need it.`;
|
|
337
|
+
return `Could not move this session off ${tierLabel(from)}. That rung is priced for judgment rather than driving, so use /tier ${SESSION_START_TIER} unless you need it.`;
|
|
302
338
|
}
|
|
303
339
|
/**
|
|
304
340
|
* Hold an interactive driver to {@link SESSION_START_TIER} at session start.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.1.4-staging.
|
|
3
|
+
"version": "1.1.4-staging.1426.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "98dad92531848916fdbe681059451b8d03646409"
|
|
62
62
|
}
|