amness 0.0.0-stage → 0.1.2
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 +21 -0
- package/README.md +237 -2
- package/dist/cli/acpcli.d.ts +7 -0
- package/dist/cli/acpcli.js +14 -0
- package/dist/cli/amness.d.ts +2 -0
- package/dist/cli/amness.js +694 -0
- package/dist/cli/benchcli.d.ts +5 -0
- package/dist/cli/benchcli.js +160 -0
- package/dist/cli/config.d.ts +79 -0
- package/dist/cli/config.js +111 -0
- package/dist/cli/demo.d.ts +13 -0
- package/dist/cli/demo.js +44 -0
- package/dist/cli/detect.d.ts +2 -0
- package/dist/cli/detect.js +20 -0
- package/dist/cli/evalcli.d.ts +15 -0
- package/dist/cli/evalcli.js +198 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/format.js +55 -0
- package/dist/cli/headless.d.ts +2 -0
- package/dist/cli/headless.js +169 -0
- package/dist/cli/keystore.d.ts +9 -0
- package/dist/cli/keystore.js +108 -0
- package/dist/cli/model.d.ts +11 -0
- package/dist/cli/model.js +19 -0
- package/dist/cli/plugincli.d.ts +2 -0
- package/dist/cli/plugincli.js +176 -0
- package/dist/cli/schedulecli.d.ts +2 -0
- package/dist/cli/schedulecli.js +66 -0
- package/dist/cli/state.d.ts +11 -0
- package/dist/cli/state.js +19 -0
- package/dist/cli/tui/ansi.d.ts +38 -0
- package/dist/cli/tui/ansi.js +142 -0
- package/dist/cli/tui/app.d.ts +328 -0
- package/dist/cli/tui/app.js +1358 -0
- package/dist/cli/tui/blocks.d.ts +151 -0
- package/dist/cli/tui/blocks.js +128 -0
- package/dist/cli/tui/brand.d.ts +9 -0
- package/dist/cli/tui/brand.js +16 -0
- package/dist/cli/tui/canvas.d.ts +92 -0
- package/dist/cli/tui/canvas.js +199 -0
- package/dist/cli/tui/cards.d.ts +44 -0
- package/dist/cli/tui/cards.js +215 -0
- package/dist/cli/tui/clipboard.d.ts +5 -0
- package/dist/cli/tui/clipboard.js +29 -0
- package/dist/cli/tui/commands.d.ts +10 -0
- package/dist/cli/tui/commands.js +650 -0
- package/dist/cli/tui/complete.d.ts +39 -0
- package/dist/cli/tui/complete.js +113 -0
- package/dist/cli/tui/custom.d.ts +9 -0
- package/dist/cli/tui/custom.js +34 -0
- package/dist/cli/tui/diffview.d.ts +25 -0
- package/dist/cli/tui/diffview.js +130 -0
- package/dist/cli/tui/editor.d.ts +60 -0
- package/dist/cli/tui/editor.js +230 -0
- package/dist/cli/tui/full.d.ts +86 -0
- package/dist/cli/tui/full.js +630 -0
- package/dist/cli/tui/keys.d.ts +31 -0
- package/dist/cli/tui/keys.js +134 -0
- package/dist/cli/tui/markdown.d.ts +37 -0
- package/dist/cli/tui/markdown.js +155 -0
- package/dist/cli/tui/mentions.d.ts +15 -0
- package/dist/cli/tui/mentions.js +48 -0
- package/dist/cli/tui/modal.d.ts +26 -0
- package/dist/cli/tui/modal.js +107 -0
- package/dist/cli/tui/overlays.d.ts +98 -0
- package/dist/cli/tui/overlays.js +237 -0
- package/dist/cli/tui/pager.d.ts +15 -0
- package/dist/cli/tui/pager.js +66 -0
- package/dist/cli/tui/pricing.d.ts +9 -0
- package/dist/cli/tui/pricing.js +23 -0
- package/dist/cli/tui/rich.d.ts +16 -0
- package/dist/cli/tui/rich.js +72 -0
- package/dist/cli/tui/screen.d.ts +30 -0
- package/dist/cli/tui/screen.js +62 -0
- package/dist/cli/tui/session.d.ts +77 -0
- package/dist/cli/tui/session.js +103 -0
- package/dist/cli/tui/sidebar.d.ts +84 -0
- package/dist/cli/tui/sidebar.js +114 -0
- package/dist/cli/tui/text.d.ts +15 -0
- package/dist/cli/tui/text.js +94 -0
- package/dist/cli/tui/theme.d.ts +59 -0
- package/dist/cli/tui/theme.js +148 -0
- package/dist/cli/tui/themes.d.ts +56 -0
- package/dist/cli/tui/themes.js +80 -0
- package/dist/cli/tui/tokens.d.ts +39 -0
- package/dist/cli/tui/tokens.js +71 -0
- package/dist/cli/tui/view.d.ts +60 -0
- package/dist/cli/tui/view.js +178 -0
- package/dist/cli/ui.d.ts +34 -0
- package/dist/cli/ui.js +224 -0
- package/dist/src/acp.d.ts +28 -0
- package/dist/src/acp.js +111 -0
- package/dist/src/agents.d.ts +23 -0
- package/dist/src/agents.js +71 -0
- package/dist/src/anthropic.d.ts +30 -0
- package/dist/src/anthropic.js +91 -0
- package/dist/src/approver.d.ts +15 -0
- package/dist/src/approver.js +40 -0
- package/dist/src/bench.d.ts +67 -0
- package/dist/src/bench.js +201 -0
- package/dist/src/bundle.d.ts +36 -0
- package/dist/src/bundle.js +55 -0
- package/dist/src/chaos.d.ts +15 -0
- package/dist/src/chaos.js +26 -0
- package/dist/src/code.d.ts +187 -0
- package/dist/src/code.js +590 -0
- package/dist/src/codeindex.d.ts +65 -0
- package/dist/src/codeindex.js +300 -0
- package/dist/src/deep.d.ts +62 -0
- package/dist/src/deep.js +177 -0
- package/dist/src/diff.d.ts +8 -0
- package/dist/src/diff.js +74 -0
- package/dist/src/dynamic.d.ts +42 -0
- package/dist/src/dynamic.js +133 -0
- package/dist/src/embed.d.ts +8 -0
- package/dist/src/embed.js +25 -0
- package/dist/src/evalcases.d.ts +11 -0
- package/dist/src/evalcases.js +196 -0
- package/dist/src/evals.d.ts +197 -0
- package/dist/src/evals.js +340 -0
- package/dist/src/gitflow.d.ts +27 -0
- package/dist/src/gitflow.js +56 -0
- package/dist/src/grade.d.ts +42 -0
- package/dist/src/grade.js +96 -0
- package/dist/src/guidance.d.ts +15 -0
- package/dist/src/guidance.js +68 -0
- package/dist/src/hardcases.d.ts +2 -0
- package/dist/src/hardcases.js +94 -0
- package/dist/src/harness.d.ts +411 -0
- package/dist/src/harness.js +725 -0
- package/dist/src/hooks.d.ts +44 -0
- package/dist/src/hooks.js +116 -0
- package/dist/src/images.d.ts +13 -0
- package/dist/src/images.js +27 -0
- package/dist/src/index.d.ts +57 -0
- package/dist/src/index.js +57 -0
- package/dist/src/ledger.d.ts +58 -0
- package/dist/src/ledger.js +78 -0
- package/dist/src/live.d.ts +16 -0
- package/dist/src/live.js +80 -0
- package/dist/src/lsp.d.ts +31 -0
- package/dist/src/lsp.js +147 -0
- package/dist/src/memory.d.ts +128 -0
- package/dist/src/memory.js +390 -0
- package/dist/src/metrics.d.ts +3 -0
- package/dist/src/metrics.js +42 -0
- package/dist/src/mining.d.ts +49 -0
- package/dist/src/mining.js +126 -0
- package/dist/src/openai.d.ts +50 -0
- package/dist/src/openai.js +307 -0
- package/dist/src/parallel.d.ts +66 -0
- package/dist/src/parallel.js +135 -0
- package/dist/src/patch.d.ts +45 -0
- package/dist/src/patch.js +219 -0
- package/dist/src/permissions.d.ts +46 -0
- package/dist/src/permissions.js +245 -0
- package/dist/src/plugins.d.ts +148 -0
- package/dist/src/plugins.js +225 -0
- package/dist/src/pluginscan.d.ts +8 -0
- package/dist/src/pluginscan.js +20 -0
- package/dist/src/procs.d.ts +41 -0
- package/dist/src/procs.js +94 -0
- package/dist/src/profiles.d.ts +36 -0
- package/dist/src/profiles.js +61 -0
- package/dist/src/progress.d.ts +29 -0
- package/dist/src/progress.js +75 -0
- package/dist/src/prompts.d.ts +10 -0
- package/dist/src/prompts.js +28 -0
- package/dist/src/ratelimit.d.ts +34 -0
- package/dist/src/ratelimit.js +116 -0
- package/dist/src/replay.d.ts +25 -0
- package/dist/src/replay.js +99 -0
- package/dist/src/review.d.ts +20 -0
- package/dist/src/review.js +49 -0
- package/dist/src/route.d.ts +9 -0
- package/dist/src/route.js +30 -0
- package/dist/src/router.d.ts +61 -0
- package/dist/src/router.js +126 -0
- package/dist/src/sandbox.d.ts +13 -0
- package/dist/src/sandbox.js +68 -0
- package/dist/src/schedule.d.ts +20 -0
- package/dist/src/schedule.js +59 -0
- package/dist/src/schema.d.ts +13 -0
- package/dist/src/schema.js +36 -0
- package/dist/src/secrets.d.ts +10 -0
- package/dist/src/secrets.js +41 -0
- package/dist/src/shell.d.ts +34 -0
- package/dist/src/shell.js +95 -0
- package/dist/src/skills.d.ts +20 -0
- package/dist/src/skills.js +112 -0
- package/dist/src/snapshot.d.ts +20 -0
- package/dist/src/snapshot.js +93 -0
- package/dist/src/stats.d.ts +24 -0
- package/dist/src/stats.js +62 -0
- package/dist/src/strategies.d.ts +16 -0
- package/dist/src/strategies.js +35 -0
- package/dist/src/suites.d.ts +30 -0
- package/dist/src/suites.js +112 -0
- package/dist/src/trace.d.ts +63 -0
- package/dist/src/trace.js +153 -0
- package/dist/src/tune.d.ts +38 -0
- package/dist/src/tune.js +49 -0
- package/dist/src/web.d.ts +19 -0
- package/dist/src/web.js +56 -0
- package/dist/src/worktree.d.ts +24 -0
- package/dist/src/worktree.js +85 -0
- package/docs/agent-loop.md +222 -0
- package/docs/architecture.md +228 -0
- package/docs/bench.md +292 -0
- package/docs/budgets.md +185 -0
- package/docs/build-your-own-harness.md +424 -0
- package/docs/cookbook.md +375 -0
- package/docs/development.md +150 -0
- package/docs/evaluation.md +450 -0
- package/docs/features.md +1320 -0
- package/docs/getting-started.md +205 -0
- package/docs/headless.md +188 -0
- package/docs/images/approval.png +0 -0
- package/docs/images/chat.png +0 -0
- package/docs/images/daylight.png +0 -0
- package/docs/images/loop.png +0 -0
- package/docs/images/palette.png +0 -0
- package/docs/images/running.png +0 -0
- package/docs/images/subagent-done.png +0 -0
- package/docs/images/subagent-running.png +0 -0
- package/docs/images/welcome.png +0 -0
- package/docs/index.md +25 -0
- package/docs/library.md +267 -0
- package/docs/plugins.md +691 -0
- package/docs/providers.md +175 -0
- package/docs/safety.md +250 -0
- package/docs/usage.md +348 -0
- package/docs/why.md +155 -0
- package/docs/writing-guide.md +39 -0
- package/package.json +66 -4
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
// The full-screen UI: header, scrollable transcript, sidebar, input panel, modals, toasts and mouse handling, drawn on a cell canvas.
|
|
2
|
+
import { copyToClipboard } from "./clipboard.js";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
import { A, mix } from "./ansi.js";
|
|
5
|
+
import { brandOfModel } from "./brand.js";
|
|
6
|
+
import { Canvas } from "./canvas.js";
|
|
7
|
+
import { isQuiet, renderBlock, renderGroup } from "./cards.js";
|
|
8
|
+
import { MOUSE_OFF, MOUSE_ON } from "./keys.js";
|
|
9
|
+
import { drawModal } from "./modal.js";
|
|
10
|
+
import { ChoiceOverlay, InputOverlay, PickerOverlay, TextOverlay } from "./overlays.js";
|
|
11
|
+
import { contextWindow, estimateCost, fmtDuration, fmtTokens } from "./pricing.js";
|
|
12
|
+
import { BOLD, F, ITAL, chip, fit, split } from "./rich.js";
|
|
13
|
+
import { renderSidebar } from "./sidebar.js";
|
|
14
|
+
import { cellWidth, clipWidth } from "./text.js";
|
|
15
|
+
import { getTheme } from "./themes.js";
|
|
16
|
+
import { level as colorLevel, TIPS } from "./theme.js";
|
|
17
|
+
import { contextMap } from "./tokens.js";
|
|
18
|
+
import { LOGO_ART } from "./brand.js";
|
|
19
|
+
import { argSummary, toolLabel } from "./view.js";
|
|
20
|
+
const SPIN = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏", BARS = "▁▂▃▄▅▆▇█";
|
|
21
|
+
export const MODE_LABEL = { default: "ask", acceptEdits: "auto-edit", plan: "plan", yolo: "yolo" };
|
|
22
|
+
export class FullView {
|
|
23
|
+
app;
|
|
24
|
+
canvas;
|
|
25
|
+
scroll = 0; // rows scrolled up from the bottom (0 = following)
|
|
26
|
+
unseen = 0; // new rows that arrived while scrolled up
|
|
27
|
+
showSidebar = true;
|
|
28
|
+
expandAll = false;
|
|
29
|
+
diffMode = "auto";
|
|
30
|
+
mouseOn = process.env.AMNESS_NO_MOUSE !== "1";
|
|
31
|
+
toasts = [];
|
|
32
|
+
cache = new Map();
|
|
33
|
+
regions = [];
|
|
34
|
+
prevTotal = 0;
|
|
35
|
+
viewTop = 0;
|
|
36
|
+
viewH = 0;
|
|
37
|
+
viewX = 0;
|
|
38
|
+
viewW = 0;
|
|
39
|
+
total = 0;
|
|
40
|
+
items = [];
|
|
41
|
+
/** Groups of quiet tool calls (reads, searches) the user has opened, by the id of the first call. */
|
|
42
|
+
groupOpen = new Set();
|
|
43
|
+
sel;
|
|
44
|
+
down;
|
|
45
|
+
lastClick = { t: 0, x: -1, y: -1, n: 0 };
|
|
46
|
+
sideX = 1e9;
|
|
47
|
+
mainW = 80;
|
|
48
|
+
toastBase = 20;
|
|
49
|
+
lastWheel = 0;
|
|
50
|
+
ctxCache;
|
|
51
|
+
forceFull = true;
|
|
52
|
+
tipIdx = Math.floor(Math.random() * TIPS.length);
|
|
53
|
+
startedAt = Date.now();
|
|
54
|
+
constructor(app) {
|
|
55
|
+
this.app = app;
|
|
56
|
+
this.canvas = new Canvas(app.io.output.columns || 80, app.io.output.rows || 24);
|
|
57
|
+
}
|
|
58
|
+
get theme() { return getTheme(this.app.cfg.theme); }
|
|
59
|
+
// ------------------------------------------------------------------ terminal modes
|
|
60
|
+
enter() {
|
|
61
|
+
const o = this.app.io.output;
|
|
62
|
+
o.write("\x1b[?1049h\x1b[2J\x1b[H" + (this.mouseOn ? MOUSE_ON : ""));
|
|
63
|
+
this.forceFull = true;
|
|
64
|
+
}
|
|
65
|
+
leave() { this.app.io.output.write(MOUSE_OFF + "\x1b[?25h\x1b[?1049l"); }
|
|
66
|
+
setMouse(on) { this.mouseOn = on; this.app.io.output.write(on ? MOUSE_ON : MOUSE_OFF); this.toast(on ? "mouse on (drag to select, wheel to scroll)" : "mouse off: native selection restored", "info"); }
|
|
67
|
+
toast(text, kind = "info", ms = 2600) { this.toasts.push({ text, kind, until: Date.now() + ms }); this.toasts = this.toasts.slice(-3); }
|
|
68
|
+
invalidate() { this.forceFull = true; this.cache.clear(); }
|
|
69
|
+
// ------------------------------------------------------------------ frame
|
|
70
|
+
draw() {
|
|
71
|
+
const app = this.app, o = app.io.output, W = o.columns || 80, H = o.rows || 24, t = this.theme;
|
|
72
|
+
if (this.canvas.w !== W || this.canvas.h !== H) {
|
|
73
|
+
this.canvas = new Canvas(W, H, t.bg);
|
|
74
|
+
this.forceFull = true;
|
|
75
|
+
}
|
|
76
|
+
const c = this.canvas;
|
|
77
|
+
c.defaultBg = t.bg;
|
|
78
|
+
c.level = colorLevel;
|
|
79
|
+
c.clear(t.bg);
|
|
80
|
+
c.cursor = null;
|
|
81
|
+
this.regions = [];
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
this.toasts = this.toasts.filter((x) => x.until > now);
|
|
84
|
+
const sbW = this.showSidebar && W >= 104 ? Math.max(30, Math.min(40, Math.floor(W * 0.27))) : 0, mainW = W - sbW;
|
|
85
|
+
const edRows = this.editorRows(mainW - 8), inputH = edRows + 2, footerY = H - 1, inputY = footerY - inputH;
|
|
86
|
+
const status = app.running || app.queued.length ? 1 + Math.min(2, app.queued.length) : 0, statusY = inputY - status;
|
|
87
|
+
const trY = 1, trH = Math.max(1, statusY - trY);
|
|
88
|
+
this.drawHeader(c, t, W);
|
|
89
|
+
this.drawTranscript(c, t, 0, trY, mainW, trH, now);
|
|
90
|
+
this.sideX = sbW ? mainW : 1e9;
|
|
91
|
+
this.mainW = mainW;
|
|
92
|
+
this.toastBase = statusY;
|
|
93
|
+
if (sbW)
|
|
94
|
+
this.drawSidebar(c, t, mainW, trY, sbW, H - 1 - trY);
|
|
95
|
+
if (status)
|
|
96
|
+
this.drawStatus(c, t, 1, statusY, mainW - 2, now);
|
|
97
|
+
this.drawInput(c, t, 1, inputY, mainW - 2, inputH);
|
|
98
|
+
this.drawSuggestions(c, t, 1, inputY, mainW - 2);
|
|
99
|
+
this.drawFooter(c, t, W, footerY, now);
|
|
100
|
+
let modal;
|
|
101
|
+
if (app.overlay)
|
|
102
|
+
modal = drawModal(c, t, app.overlay, app.frame);
|
|
103
|
+
if (modal) {
|
|
104
|
+
for (const h of modal.hits)
|
|
105
|
+
this.regions.push({ x: h.x, y: h.y, w: h.w, h: h.h, kind: "modal", click: () => this.modalClick(h) });
|
|
106
|
+
if (modal.cursor)
|
|
107
|
+
c.cursor = modal.cursor;
|
|
108
|
+
}
|
|
109
|
+
this.drawToasts(c, t, W);
|
|
110
|
+
this.drawSelection(c, t);
|
|
111
|
+
const seq = c.flush(this.forceFull);
|
|
112
|
+
this.forceFull = false;
|
|
113
|
+
o.write("\x1b[?2026h\x1b[?25l" + seq + (c.cursor ? `\x1b[${c.cursor.y + 1};${c.cursor.x + 1}H\x1b[?25h` : "") + "\x1b[?2026l");
|
|
114
|
+
}
|
|
115
|
+
// ------------------------------------------------------------------ header
|
|
116
|
+
drawHeader(c, t, W) {
|
|
117
|
+
const app = this.app, brand = brandOfModel(app.cfg.model, app.cfg.provider, t), bg = t.surface;
|
|
118
|
+
const mode = app.mode, modeCol = mode === "yolo" ? t.err : mode === "plan" ? t.info : mode === "acceptEdits" ? t.ok : t.accent;
|
|
119
|
+
c.fill(0, 0, W, 1, bg);
|
|
120
|
+
const logo = gradientText("✻ Amness", t.gradient), proj = BOLD(F(t.text, basename(app.cwd))), br = app.branch ? F(t.dim, " ⎇ " + app.branch) : "";
|
|
121
|
+
const left = " " + logo + F(t.border, " │ ") + proj + br;
|
|
122
|
+
const modelChip = chip(`${F(brand.color, brand.icon)} ${app.cfg.model}`, t.text, t.surface2), modeChip = chip(MODE_LABEL[mode] ?? mode, t.onAccent, modeCol, true), sideChip = chip(this.showSidebar ? "▥" : "▤", t.dim, t.surface2);
|
|
123
|
+
const right = modelChip + " " + modeChip + " " + sideChip + " ";
|
|
124
|
+
const title = app.session.title ? F(t.faint, ITAL(clipWidth(app.session.title, Math.max(0, W - cellWidth(left) - cellWidth(right) - 6)))) : "";
|
|
125
|
+
const mid = W - cellWidth(left) - cellWidth(right);
|
|
126
|
+
c.ansi(0, 0, fit(left + (mid > 12 && title ? " " + F(t.border, "·") + " " + title : ""), W - cellWidth(right), bg));
|
|
127
|
+
c.ansi(W - cellWidth(right), 0, right);
|
|
128
|
+
// clickable chips
|
|
129
|
+
const rw = cellWidth(right), mx = W - rw, mlen = cellWidth(modelChip), dlen = cellWidth(modeChip), slen = cellWidth(sideChip);
|
|
130
|
+
this.regions.push({ x: mx, y: 0, w: mlen, h: 1, kind: "model", click: () => void app.runCommand("/model") }, { x: mx + mlen + 1, y: 0, w: dlen, h: 1, kind: "mode", click: () => { app.cycleMode(); app.render(); } }, { x: mx + mlen + dlen + 2, y: 0, w: slen, h: 1, kind: "sidebar", click: () => { this.showSidebar = !this.showSidebar; this.invalidate(); } });
|
|
131
|
+
}
|
|
132
|
+
// ------------------------------------------------------------------ transcript
|
|
133
|
+
ctxFor(width, now) {
|
|
134
|
+
const app = this.app, brand = brandOfModel(app.cfg.model, app.cfg.provider, this.theme);
|
|
135
|
+
return { theme: this.theme, width, now, frame: app.frame, expandAll: this.expandAll, live: app.running, brand, diffMode: this.diffMode, kids: (id) => app.blocks.kids(id) };
|
|
136
|
+
}
|
|
137
|
+
sig(b, ctx) {
|
|
138
|
+
const base = `${ctx.theme.name}|${ctx.width}|${ctx.diffMode}|${ctx.expandAll}|${ctx.brand.icon}|`;
|
|
139
|
+
switch (b.kind) {
|
|
140
|
+
case "user": return base + `${b.text.length}|${b.note ?? ""}|${b.queued ? 1 : 0}`;
|
|
141
|
+
case "assistant": return base + `${b.text.length}|${b.streaming ? 1 + (ctx.frame >> 2) % 2 : 0}|${ctx.live}`;
|
|
142
|
+
case "tool": return base + (b.name === "task" ? (() => { const k = ctx.kids?.(b.callId) ?? []; return `k${k.length}:${k.at(-1)?.status ?? ""}:${k.at(-1)?.id ?? ""}|`; })() : "") + `${b.status}|${b.result?.length ?? 0}|${b.expanded}|${b.more}|${b.edit?.diff.length ?? 0}|${b.ms ?? 0}|${b.status === "running" ? Math.floor(ctx.now / 100) + ":" + ctx.frame : ""}`;
|
|
143
|
+
case "thinking": return base + `${b.text.length}|${b.active}|${b.expanded}|${b.active ? ctx.frame >> 1 : 0}`;
|
|
144
|
+
case "notice":
|
|
145
|
+
case "summary": return base + b.text.length;
|
|
146
|
+
case "raw": return base + b.lines.length + ":" + (b.lines[0]?.length ?? 0);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
drawTranscript(c, t, x0, y0, w, h, now) {
|
|
150
|
+
const app = this.app, W = w - 1, ctx = this.ctxFor(W, now); // last column is the scrollbar
|
|
151
|
+
this.items = [];
|
|
152
|
+
let total = 0;
|
|
153
|
+
if (!app.blocks.blocks.length) {
|
|
154
|
+
this.drawEmpty(c, t, x0, y0, w, h);
|
|
155
|
+
this.viewTop = y0;
|
|
156
|
+
this.viewH = h;
|
|
157
|
+
this.viewX = x0;
|
|
158
|
+
this.viewW = w;
|
|
159
|
+
this.total = 0;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const live = new Set();
|
|
163
|
+
const blocks = app.blocks.blocks.filter((b) => !(b.kind === "tool" && (b.name === "todo_write" || (b.parent && b.agent && b.agent !== "lead")))); // todos live in the sidebar; a subagent's steps are drawn inside its card
|
|
164
|
+
let prevTool = false;
|
|
165
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
166
|
+
const b = blocks[i];
|
|
167
|
+
live.add(b.id);
|
|
168
|
+
let run = 1;
|
|
169
|
+
if (isQuiet(b))
|
|
170
|
+
while (i + run < blocks.length && isQuiet(blocks[i + run]))
|
|
171
|
+
run++;
|
|
172
|
+
const tctx = { ...ctx, tight: prevTool };
|
|
173
|
+
if (run >= 2) {
|
|
174
|
+
const members = blocks.slice(i, i + run), open = this.expandAll || this.groupOpen.has(b.id);
|
|
175
|
+
const key = `g|${tctx.tight}|${open}|${ctx.theme.name}|${ctx.width}|` + members.map((m) => `${m.id}:${m.status}:${m.ms ?? 0}`).join(",") + (members.some((m) => m.status === "running") ? `|${ctx.frame}` : ""), hit = this.cache.get(b.id);
|
|
176
|
+
const r = hit && hit.key === key ? hit.r : (() => { const rr = renderGroup(members, open, tctx); this.cache.set(b.id, { key, r: rr }); return rr; })();
|
|
177
|
+
for (const m of members)
|
|
178
|
+
live.add(m.id);
|
|
179
|
+
this.items.push({ b, r, start: total, group: true });
|
|
180
|
+
total += r.rows.length;
|
|
181
|
+
i += run - 1;
|
|
182
|
+
prevTool = r.rows.length <= 2;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const key = this.sig(b, tctx) + (tctx.tight ? "|t" : ""), hit = this.cache.get(b.id);
|
|
186
|
+
const r = hit && hit.key === key ? hit.r : (() => { const rr = renderBlock(b, tctx); this.cache.set(b.id, { key, r: rr }); return rr; })();
|
|
187
|
+
this.items.push({ b, r, start: total });
|
|
188
|
+
total += r.rows.length;
|
|
189
|
+
prevTool = b.kind === "tool" && (!b.agent || b.agent === "lead") && r.rows.length <= 2;
|
|
190
|
+
}
|
|
191
|
+
for (const id of this.cache.keys())
|
|
192
|
+
if (!live.has(id))
|
|
193
|
+
this.cache.delete(id);
|
|
194
|
+
if (this.scroll > 0 && total > this.prevTotal) {
|
|
195
|
+
this.scroll += total - this.prevTotal;
|
|
196
|
+
this.unseen += total - this.prevTotal;
|
|
197
|
+
} // keep the view anchored while scrolled up
|
|
198
|
+
this.prevTotal = total;
|
|
199
|
+
const maxScroll = Math.max(0, total - h);
|
|
200
|
+
this.scroll = Math.max(0, Math.min(this.scroll, maxScroll));
|
|
201
|
+
if (this.scroll === 0)
|
|
202
|
+
this.unseen = 0;
|
|
203
|
+
const start = total <= h ? 0 : total - h - this.scroll;
|
|
204
|
+
this.viewTop = y0;
|
|
205
|
+
this.viewH = h;
|
|
206
|
+
this.viewX = x0;
|
|
207
|
+
this.viewW = w;
|
|
208
|
+
this.total = total;
|
|
209
|
+
let yy = 0;
|
|
210
|
+
for (const it of this.items) {
|
|
211
|
+
if (it.start + it.r.rows.length <= start)
|
|
212
|
+
continue;
|
|
213
|
+
if (it.start >= start + h)
|
|
214
|
+
break;
|
|
215
|
+
for (let k = 0; k < it.r.rows.length; k++) {
|
|
216
|
+
const row = it.start + k - start;
|
|
217
|
+
if (row >= 0 && row < h)
|
|
218
|
+
c.put(x0, y0 + row, it.r.rows[k], W);
|
|
219
|
+
}
|
|
220
|
+
for (const hh of it.r.hits) {
|
|
221
|
+
const row = it.start + hh.row - start;
|
|
222
|
+
if (row >= 0 && row < h)
|
|
223
|
+
this.regions.push({ x: x0 + hh.x0, y: y0 + row, w: hh.x1 - hh.x0, h: 1, kind: hh.kind, click: () => this.hitClick(hh, it.b, it.group) });
|
|
224
|
+
}
|
|
225
|
+
yy = it.start + it.r.rows.length;
|
|
226
|
+
}
|
|
227
|
+
void yy;
|
|
228
|
+
// scrollbar
|
|
229
|
+
if (total > h) {
|
|
230
|
+
const thumb = Math.max(1, Math.round((h * h) / total)), top = Math.round(((start) / Math.max(1, total - h)) * (h - thumb));
|
|
231
|
+
for (let r = 0; r < h; r++)
|
|
232
|
+
c.text(x0 + W, y0 + r, r >= top && r < top + thumb ? "┃" : "│", { fg: r >= top && r < top + thumb ? t.dim : t.border });
|
|
233
|
+
this.regions.push({ x: x0 + W, y: y0, w: 1, h, kind: "scrollbar", click: () => this.scrollTo(0) });
|
|
234
|
+
}
|
|
235
|
+
// sticky prompt: the last user message that scrolled off the top
|
|
236
|
+
if (this.scroll > 0) {
|
|
237
|
+
const above = [...this.items].reverse().find((i) => i.b.kind === "user" && i.start + i.r.rows.length <= start);
|
|
238
|
+
if (above && above.b.kind === "user") {
|
|
239
|
+
const text = clipWidth(above.b.text.replace(/\s+/g, " "), w - 18);
|
|
240
|
+
c.ansi(x0, y0, fit(" " + F(t.accent, "▎") + " " + F(t.dim, text) + F(t.faint, " ↑ prompt"), W, mix(t.surface2, t.bg, 0.2)));
|
|
241
|
+
this.regions.push({ x: x0, y: y0, w: W, h: 1, kind: "sticky", click: () => this.scrollToBlock(above.b.id) });
|
|
242
|
+
}
|
|
243
|
+
const label = this.unseen > 0 ? ` ↓ Jump to bottom · ${this.unseen} new rows ` : " ↓ Jump to bottom ", lw = cellWidth(label), px = x0 + Math.floor((W - lw) / 2);
|
|
244
|
+
c.text(px, y0 + h - 1, label, { fg: t.onAccent, bg: t.accent, attr: A.bold });
|
|
245
|
+
this.regions.push({ x: px, y: y0 + h - 1, w: lw, h: 1, kind: "jump", click: () => this.scrollTo(0) });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
hitClick(h, b, group = false) {
|
|
249
|
+
const before = this.total;
|
|
250
|
+
if (h.kind === "copy" && h.text !== undefined) {
|
|
251
|
+
this.copy(h.text);
|
|
252
|
+
this.toast(`copied code block (${h.text.length} characters)`, "ok");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (group) {
|
|
256
|
+
if (this.groupOpen.has(b.id))
|
|
257
|
+
this.groupOpen.delete(b.id);
|
|
258
|
+
else
|
|
259
|
+
this.groupOpen.add(b.id);
|
|
260
|
+
}
|
|
261
|
+
else if (h.kind === "toggle")
|
|
262
|
+
this.app.blocks.toggle(b.id);
|
|
263
|
+
else if (b.kind === "tool") {
|
|
264
|
+
b.more = true;
|
|
265
|
+
this.app.blocks.version++;
|
|
266
|
+
}
|
|
267
|
+
this.cache.delete(b.id);
|
|
268
|
+
this.app.render();
|
|
269
|
+
void before;
|
|
270
|
+
}
|
|
271
|
+
scrollTo(fromBottom) { this.scroll = Math.max(0, fromBottom); if (this.scroll === 0)
|
|
272
|
+
this.unseen = 0; this.app.render(); }
|
|
273
|
+
scrollBy(rows) { this.scroll = Math.max(0, this.scroll + rows); if (this.scroll === 0)
|
|
274
|
+
this.unseen = 0; this.app.render(); }
|
|
275
|
+
scrollToBlock(id) {
|
|
276
|
+
const it = this.items.find((i) => i.b.id === id);
|
|
277
|
+
if (!it)
|
|
278
|
+
return;
|
|
279
|
+
this.scroll = Math.max(0, this.total - this.viewH - it.start);
|
|
280
|
+
this.app.render();
|
|
281
|
+
}
|
|
282
|
+
/** Keep a toggled block's header where it is on screen. */
|
|
283
|
+
pin(blockId, fn) {
|
|
284
|
+
const it = this.items.find((i) => i.b.id === blockId), startBefore = it?.start ?? 0, totalBefore = this.total;
|
|
285
|
+
fn();
|
|
286
|
+
void startBefore;
|
|
287
|
+
void totalBefore;
|
|
288
|
+
}
|
|
289
|
+
// ------------------------------------------------------------------ empty state
|
|
290
|
+
drawEmpty(c, t, x0, y0, w, h) {
|
|
291
|
+
const app = this.app, lines = [];
|
|
292
|
+
const lw0 = Math.max(...LOGO_ART.map((l) => cellWidth(l)));
|
|
293
|
+
for (const [i, l] of LOGO_ART.entries())
|
|
294
|
+
lines.push(gradientText(l + " ".repeat(lw0 - cellWidth(l)), t.gradient, i * 0.06));
|
|
295
|
+
lines.push("", F(t.dim, "reliable agents for any model"), "");
|
|
296
|
+
const chips = [["Explain this repo", "Explain what this repository does and how it is organised. Keep it short."], ["Find & fix bugs", "Look for likely bugs in this repository and fix the clearest ones, verifying each fix."],
|
|
297
|
+
["Write tests", "Find the least-tested important code here and add focused tests for it."], ["Plan a feature", "__plan__"]];
|
|
298
|
+
const y = y0 + Math.max(1, Math.floor((h - lines.length - 9) / 3));
|
|
299
|
+
lines.forEach((l, i) => { const lw = i < LOGO_ART.length ? lw0 : cellWidth(l.replace(/\x1b\[[0-9;]*m/g, "")); c.ansi(x0 + Math.floor((w - lw) / 2), y + i, l); });
|
|
300
|
+
let cx = 0;
|
|
301
|
+
const widths = chips.map(([label]) => label.length + 4), totalW = widths.reduce((a, b) => a + b + 2, -2), startX = x0 + Math.floor((w - totalW) / 2), cy = y + lines.length;
|
|
302
|
+
chips.forEach(([label, prompt], i) => {
|
|
303
|
+
const ch = chip(label, t.text, t.surface2);
|
|
304
|
+
c.ansi(startX + cx, cy, ch);
|
|
305
|
+
const px = startX + cx, pw = widths[i];
|
|
306
|
+
cx += pw + 2;
|
|
307
|
+
this.regions.push({ x: px, y: cy, w: pw, h: 1, kind: "action", click: () => { if (prompt === "__plan__") {
|
|
308
|
+
app.setMode("plan");
|
|
309
|
+
app.editor.setText("Plan how to add: ");
|
|
310
|
+
}
|
|
311
|
+
else
|
|
312
|
+
app.editor.setText(prompt); app.render(); } });
|
|
313
|
+
});
|
|
314
|
+
const recent = app.store.list().slice(0, 3);
|
|
315
|
+
let ry = cy + 2;
|
|
316
|
+
if (recent.length) {
|
|
317
|
+
const hd = F(t.faint, BOLD("RECENT"));
|
|
318
|
+
c.ansi(x0 + Math.floor((w - 6) / 2), ry++, hd);
|
|
319
|
+
for (const s of recent) {
|
|
320
|
+
const label = ` ↻ ${clipWidth(s.title || "(untitled)", 44)} ${F(t.faint, `${s.turns} turn${s.turns === 1 ? "" : "s"} · ${ago(s.updated)}`)} `, lw = cellWidth(label.replace(/\x1b\[[0-9;]*m/g, ""));
|
|
321
|
+
const rx = x0 + Math.floor((w - lw) / 2);
|
|
322
|
+
c.ansi(rx, ry, F(t.dim, label));
|
|
323
|
+
this.regions.push({ x: rx, y: ry, w: lw, h: 1, kind: "recent", click: () => void app.resumeById(s.id) });
|
|
324
|
+
ry++;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const hint = F(t.faint, "drag to select & copy · Shift+drag for the terminal's own selection · /copy copies the last reply");
|
|
328
|
+
c.ansi(x0 + Math.max(0, Math.floor((w - 94) / 2)), Math.min(y0 + h - 2, ry + 4), hint);
|
|
329
|
+
const tip = F(t.faint, "tip ") + F(t.dim, TIPS[this.tipIdx]);
|
|
330
|
+
c.ansi(x0 + Math.max(0, Math.floor((w - cellWidth(TIPS[this.tipIdx]) - 5) / 2)), Math.min(y0 + h - 2, ry + 2), tip);
|
|
331
|
+
}
|
|
332
|
+
// ------------------------------------------------------------------ sidebar
|
|
333
|
+
drawSidebar(c, t, x0, y0, w, h) {
|
|
334
|
+
const app = this.app, u = app.session.usage, win = contextWindow(app.cfg.model), now = Date.now();
|
|
335
|
+
const mkey = `${app.session.messages.length}|${app.lastPrompt}`;
|
|
336
|
+
if (this.ctxCache?.key !== mkey) {
|
|
337
|
+
const m = contextMap(app.session.messages, app.lastPrompt, JSON.stringify(app.agent.registry.visible({ step: 0, messages: [] }).map((x) => x.name + x.description)).length);
|
|
338
|
+
const tot = Math.max(1, m.total), cols = { tools: t.tool.read, assistant: t.tool.edit, you: t.ok, system: t.accent, defs: t.accent2 };
|
|
339
|
+
this.ctxCache = { key: mkey, map: m.parts.map((p) => ({ label: p.label, pct: Math.round((p.tokens / tot) * 100), color: cols[p.key] })).sort((a, b) => b.pct - a.pct) };
|
|
340
|
+
}
|
|
341
|
+
const changes = new Map();
|
|
342
|
+
for (const b of app.blocks.blocks)
|
|
343
|
+
if (b.kind === "tool" && b.edit) {
|
|
344
|
+
const p = changes.get(b.edit.path);
|
|
345
|
+
changes.set(b.edit.path, { path: b.edit.path, added: (p?.added ?? 0) + b.edit.added, removed: (p?.removed ?? 0) + b.edit.removed, created: p?.created ?? b.edit.created, blockId: b.id });
|
|
346
|
+
}
|
|
347
|
+
const used = app.lastPrompt || this.ctxCache.map.reduce((n, m) => n + m.pct, 0) * 0;
|
|
348
|
+
const { rows, hits } = renderSidebar({
|
|
349
|
+
title: app.session.title, model: app.cfg.model, provider: app.cfg.provider, api: app.model.api?.(), branch: app.branch, cwd: shortPath(app.cwd), mode: app.mode, effort: app.cfg.effort,
|
|
350
|
+
pluginStatus: app.pluginStatus(), budget: { used: Math.round(app.budget.effective), max: app.budget.maxTokens }, ctxPct: app.ctxPct, ctxUsed: used, ctxMax: win, series: u.series ?? [], map: this.ctxCache.map,
|
|
351
|
+
usage: { prompt: u.prompt, completion: u.completion, calls: u.calls, cached: u.cached ?? 0, reasoning: u.reasoning ?? 0 }, cost: estimateCost(app.cfg.model, u.prompt, u.completion, u.cached ?? 0),
|
|
352
|
+
todos: app.todos, changes: [...changes.values()].reverse(), tools: app.agent.registry.visible({ step: 0, messages: [] }).length, mcp: app.mcp.length, memory: app.memory?.list().length ?? 0,
|
|
353
|
+
sandbox: app.cfg.sandbox,
|
|
354
|
+
procs: app.agent.procs?.list().map((p) => ({ id: p.id, name: p.name, status: p.status, secs: Math.round((now - p.startedAt) / 1000) })),
|
|
355
|
+
running: app.running, elapsedMs: now - this.startedAt, tip: TIPS[this.tipIdx],
|
|
356
|
+
run: app.running ? { phase: app.phase?.phase ?? "model", step: app.phase?.step ?? 0, max: app.phase?.max ?? 30, elapsedMs: now - app.runStart, frame: app.frame,
|
|
357
|
+
live: [...app.inflight.values()].map((x) => ({ label: toolVerb(x.name, x.args), ms: now - x.t0, agent: x.agent })), done: app.activity.map((x) => ({ label: toolVerb(x.name, x.args, true), ok: x.ok, ms: x.ms })) } : undefined,
|
|
358
|
+
}, t, w, h);
|
|
359
|
+
rows.forEach((r, i) => c.put(x0 + 1, y0 + i, r, w - 1));
|
|
360
|
+
for (let i = 0; i < h; i++)
|
|
361
|
+
c.text(x0, y0 + i, "│", { fg: t.border, bg: t.surface });
|
|
362
|
+
for (const hh of hits)
|
|
363
|
+
if (hh.row < h)
|
|
364
|
+
this.regions.push({ x: x0, y: y0 + hh.row, w, h: 1, kind: "change", click: () => hh.blockId !== undefined && this.scrollToBlock(hh.blockId) });
|
|
365
|
+
}
|
|
366
|
+
// ------------------------------------------------------------------ status line, input, footer
|
|
367
|
+
drawStatus(c, t, x, y, w, now) {
|
|
368
|
+
const app = this.app, f = app.frame, glyph = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"][f % 10];
|
|
369
|
+
if (app.running) {
|
|
370
|
+
const fl = [...app.inflight.values()], ph = app.phase?.phase;
|
|
371
|
+
const verb = app.overlay ? "Waiting for you" : ph === "verify" ? "Verifying" : ph === "compact" ? "Compacting" : fl.length > 1 ? `Running ${fl.length} tools in parallel` : fl.length === 1 ? toolVerb(fl[0].name, fl[0].args)
|
|
372
|
+
: app.mode === "plan" ? "Planning" : app.thinkBuf.trim() ? "Thinking" : app.streamBuf ? "Writing" : "Working", chars = [...clipWidth(verb, Math.max(8, w - 44)) + "…"];
|
|
373
|
+
const shim = chars.map((ch, i) => F(mix(t.accent, 0xffffff, Math.max(0, 1 - Math.abs(i - ((f * 0.9) % (chars.length + 8) - 4)) / 3)), ch)).join("");
|
|
374
|
+
const spark = app.runSeries.length > 1 ? " " + F(t.accent2, app.runSeries.slice(-10).map((v) => BARS[Math.min(7, Math.round((v / Math.max(1, ...app.runSeries)) * 7))]).join("")) : "";
|
|
375
|
+
c.ansi(x + 1, y, F(mix(t.accent, t.accent2, (f % 20) / 20), glyph) + " " + BOLD(shim) + F(t.dim, ` ${fmtDuration(now - app.runStart)} · ↓ ${fmtTokens(app.runTokens)}`) + (app.phase ? F(t.faint, ` · step ${app.phase.step + 1}/${app.phase.max}`) : "") + spark + F(t.faint, " esc to interrupt"), {}, w - 2);
|
|
376
|
+
if (app.thinkBuf.trim() && app.queued.length === 0) { /* reasoning summary lives in the transcript's thinking block */ }
|
|
377
|
+
}
|
|
378
|
+
app.queued.slice(0, 2).forEach((q, i) => c.ansi(x + 1, y + (app.running ? 1 : 0) + i, F(t.warn, "▸ queued ") + F(t.dim, clipWidth(q.text.replace(/\s+/g, " "), w - 14)), {}, w - 2));
|
|
379
|
+
}
|
|
380
|
+
editorRows(width) { const r = this.app.editor.render(Math.max(10, width), "", ""); return Math.max(1, Math.min(8, r.lines.length)); }
|
|
381
|
+
drawInput(c, t, x, y, w, h) {
|
|
382
|
+
const app = this.app, mode = app.mode, col = mode === "yolo" ? t.err : mode === "plan" ? t.info : mode === "acceptEdits" ? t.ok : t.accent;
|
|
383
|
+
const dimBorder = app.running ? mix(col, t.bg, 0.55) : col;
|
|
384
|
+
c.box(x, y, w, h, { border: dimBorder, bg: t.surface, style: "round" });
|
|
385
|
+
const label = ` ${MODE_LABEL[mode] ?? mode} `;
|
|
386
|
+
c.text(x + 2, y, label, { fg: t.onAccent, bg: col, attr: A.bold });
|
|
387
|
+
this.regions.push({ x: x + 2, y, w: label.length, h: 1, kind: "mode", click: () => { app.cycleMode(); app.render(); } });
|
|
388
|
+
const hint = app.running ? " enter queues · esc interrupts " : " ⇧⇥ mode ⌥↵ newline ";
|
|
389
|
+
c.text(x + w - cellWidth(hint) - 2, y, hint, { fg: t.faint, bg: t.surface });
|
|
390
|
+
const ed = app.editor, innerW = w - 6, r = ed.render(innerW, "", ""), rows = Math.min(8, r.lines.length);
|
|
391
|
+
const first = Math.max(0, Math.min(r.cursor.row - rows + 1, r.lines.length - rows)), shown = r.lines.slice(first, first + rows);
|
|
392
|
+
const empty = ed.empty;
|
|
393
|
+
shown.forEach((ln, i) => {
|
|
394
|
+
const yy = y + 1 + i, prompt = i === 0 && first === 0 ? F(col, BOLD("❯")) : " ";
|
|
395
|
+
c.ansi(x + 2, yy, prompt, { bg: t.surface });
|
|
396
|
+
if (empty && i === 0)
|
|
397
|
+
c.ansi(x + 4, yy, F(t.faint, ITAL(app.running ? "Type to steer the agent…" : "Ask anything… @ files · / commands · !cmd run a command · # remember")), { bg: t.surface }, innerW);
|
|
398
|
+
else
|
|
399
|
+
c.ansi(x + 4, yy, ln.replace(/(@[^\s]+)/g, (m) => F(t.accent2, m)).replace(/\[Pasted text #\d+ \+\d+ lines?\]/g, (m) => F(t.warn, m)), { bg: t.surface, fg: t.text }, innerW);
|
|
400
|
+
});
|
|
401
|
+
if (!app.overlay)
|
|
402
|
+
c.cursor = { x: x + 4 + r.cursor.col, y: y + 1 + (r.cursor.row - first) };
|
|
403
|
+
this.regions.push({ x: x + 1, y: y + 1, w: w - 2, h: rows, kind: "input", click: () => { } });
|
|
404
|
+
}
|
|
405
|
+
drawSuggestions(c, t, x, inputY, mainW) {
|
|
406
|
+
const s = this.app.sugg;
|
|
407
|
+
if (!s || !s.items.length || this.app.overlay)
|
|
408
|
+
return;
|
|
409
|
+
const rows = Math.min(8, s.items.length), w = Math.min(70, mainW), top = Math.max(0, Math.min(s.sel - 3, s.items.length - rows)), y = inputY - rows - 2;
|
|
410
|
+
if (y < 1)
|
|
411
|
+
return;
|
|
412
|
+
c.box(x + 1, y, w, rows + 2, { border: t.border, bg: t.surface, style: "round", title: s.kind === "slash" ? "commands" : "files", titleFg: t.dim });
|
|
413
|
+
s.items.slice(top, top + rows).forEach((it, i) => {
|
|
414
|
+
const idx = top + i, on = idx === s.sel, bg = on ? t.selection : t.surface;
|
|
415
|
+
const label = s.kind === "file" ? (() => { const n = it.label.split("/").pop(), d = it.label.slice(0, it.label.length - n.length); return F(t.faint, d) + BOLD(F(t.text, n)); })() : BOLD(F(t.accent2, it.label));
|
|
416
|
+
c.ansi(x + 2, y + 1 + i, fit((on ? F(t.accent, "▌") : " ") + " " + label + (it.detail ? " " + F(t.faint, it.detail) : ""), w - 2, bg), {}, w - 2);
|
|
417
|
+
this.regions.push({ x: x + 2, y: y + 1 + i, w: w - 2, h: 1, kind: "sugg", click: () => { s.sel = idx; this.app.acceptSuggPublic(true); } });
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
drawFooter(c, t, W, y, now) {
|
|
421
|
+
const app = this.app, hints = app.overlay ? [] : app.running ? [["esc", "interrupt"], ["enter", "queue message"], ["ctrl+o", "expand all"]]
|
|
422
|
+
: app.editor.empty ? [["enter", "send"], ["ctrl+p", "commands"], ["ctrl+o", "expand tools"], ["ctrl+b", "sidebar"], ["pgup/dn", "scroll"], ["?", "help"]] : [["enter", "send"], ["⌥enter", "newline"], ["tab", "complete"], ["esc esc", "clear"]];
|
|
423
|
+
const left = hints.map(([k, l]) => F(t.dim, k) + " " + F(t.faint, l)).join(F(t.border, " · "));
|
|
424
|
+
const p = app.ctxPct, cost = app.cost, right = [p !== null ? F(p > 85 ? t.err : p > 60 ? t.warn : t.dim, `ctx ${p}%`) : "", cost ? F(t.dim, cost) : "", this.mouseOn ? "" : F(t.warn, "mouse off")].filter(Boolean).join(F(t.border, " · "));
|
|
425
|
+
c.ansi(0, y, fit(" " + (app.notice && now < app.noticeUntil ? F(t.warn, app.notice) : left), W - cellWidth(right) - 1, t.bg));
|
|
426
|
+
if (right)
|
|
427
|
+
c.ansi(W - cellWidth(right) - 1, y, right);
|
|
428
|
+
}
|
|
429
|
+
drawToasts(c, t, W) {
|
|
430
|
+
this.toasts.forEach((ts, i) => {
|
|
431
|
+
const col = ts.kind === "ok" ? t.ok : ts.kind === "warn" ? t.warn : ts.kind === "error" ? t.err : t.accent2, icon = ts.kind === "ok" ? "✓" : ts.kind === "warn" ? "⚠" : ts.kind === "error" ? "✗" : "●";
|
|
432
|
+
const n = this.toasts.length, text = clipWidth(` ${icon} ${ts.text} `, Math.max(10, this.mainW - 6)), w = cellWidth(text) + 2, x = 2, y = this.toastBase - 3 * (n - i); // bottom-left of the transcript: never covers the header or sidebar
|
|
433
|
+
c.box(x, y, w, 3, { border: col, bg: t.surface2, style: "round" });
|
|
434
|
+
c.text(x + 1, y + 1, text, { fg: t.text, bg: t.surface2 });
|
|
435
|
+
c.text(x + 2, y + 1, icon, { fg: col, bg: t.surface2 });
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
drawSelection(c, t) {
|
|
439
|
+
const s = this.sel;
|
|
440
|
+
if (!s)
|
|
441
|
+
return;
|
|
442
|
+
const [a, b] = ordered(s);
|
|
443
|
+
if (s.mode === "block") {
|
|
444
|
+
const lo = Math.min(s.ax, s.hx), hi = Math.max(s.ax, s.hx);
|
|
445
|
+
for (let y = a.y; y <= b.y; y++)
|
|
446
|
+
c.tint(lo, y, hi - lo + 1, 1, t.selection);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
for (let y = a.y; y <= b.y; y++) {
|
|
450
|
+
const x0 = y === a.y ? a.x : this.viewX, x1 = y === b.y ? b.x : this.viewX + this.viewW - 2;
|
|
451
|
+
c.tint(x0, y, x1 - x0 + 1, 1, t.selection);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// ------------------------------------------------------------------ mouse
|
|
455
|
+
mouse(e) {
|
|
456
|
+
const app = this.app;
|
|
457
|
+
if (e.action === "wheelup" || e.action === "wheeldown") {
|
|
458
|
+
const now = Date.now(), step = now - this.lastWheel < 70 ? 5 : 3;
|
|
459
|
+
this.lastWheel = now;
|
|
460
|
+
const o = app.overlay;
|
|
461
|
+
if (o instanceof PickerOverlay) {
|
|
462
|
+
o.key({ name: e.action === "wheelup" ? "up" : "down" });
|
|
463
|
+
app.render();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (o instanceof TextOverlay) {
|
|
467
|
+
o.scroll += e.action === "wheelup" ? -3 : 3;
|
|
468
|
+
app.render();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (o)
|
|
472
|
+
return;
|
|
473
|
+
this.scrollBy(e.action === "wheelup" ? step : -step);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (e.button !== "left")
|
|
477
|
+
return;
|
|
478
|
+
const inTr = e.x >= this.viewX && e.x < this.viewX + this.viewW - 1 && e.y >= this.viewTop && e.y < this.viewTop + this.viewH;
|
|
479
|
+
if (e.action === "press") {
|
|
480
|
+
const inTr0 = !app.overlay && e.x >= this.viewX && e.x < this.viewX + this.viewW - 1 && e.y >= this.viewTop && e.y < this.viewTop + this.viewH;
|
|
481
|
+
this.down = { x: e.x, y: e.y, moved: false, mode: inTr0 ? "lines" : "block" };
|
|
482
|
+
this.sel = undefined;
|
|
483
|
+
if (this.input(e))
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
else if (e.action === "drag" && this.down) {
|
|
487
|
+
if (Math.abs(e.x - this.down.x) + Math.abs(e.y - this.down.y) > 0) {
|
|
488
|
+
this.down.moved = true;
|
|
489
|
+
const lines = this.down.mode === "lines";
|
|
490
|
+
const y = lines ? Math.max(this.viewTop, Math.min(this.viewTop + this.viewH - 1, e.y)) : Math.max(0, Math.min(this.canvas.h - 1, e.y)), x = lines ? Math.max(this.viewX, Math.min(this.viewX + this.viewW - 2, e.x)) : Math.max(0, Math.min(this.canvas.w - 1, e.x));
|
|
491
|
+
this.sel = { ax: this.down.x, ay: lines ? Math.max(this.viewTop, Math.min(this.viewTop + this.viewH - 1, this.down.y)) : this.down.y, hx: x, hy: y, done: false, mode: this.down.mode };
|
|
492
|
+
if (lines) {
|
|
493
|
+
if (e.y < this.viewTop)
|
|
494
|
+
this.scrollBy(1);
|
|
495
|
+
else if (e.y >= this.viewTop + this.viewH)
|
|
496
|
+
this.scrollBy(-1);
|
|
497
|
+
}
|
|
498
|
+
app.render();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else if (e.action === "release" && this.down) {
|
|
502
|
+
const d = this.down;
|
|
503
|
+
this.down = undefined;
|
|
504
|
+
if (d.moved && this.sel) {
|
|
505
|
+
this.sel.done = true;
|
|
506
|
+
this.copySelection();
|
|
507
|
+
app.render();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (d.moved)
|
|
511
|
+
return;
|
|
512
|
+
const now = Date.now(), n = now - this.lastClick.t < 450 && this.lastClick.x === e.x && this.lastClick.y === e.y ? this.lastClick.n + 1 : 1;
|
|
513
|
+
this.lastClick = { t: now, x: e.x, y: e.y, n };
|
|
514
|
+
const handled = this.click(e.x, e.y);
|
|
515
|
+
if (!handled && n >= 2)
|
|
516
|
+
this.selectAt(e.x, e.y, n >= 3 ? "line" : "word");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/** Double click selects the word under the pointer, triple click the whole line; both copy at once. */
|
|
520
|
+
selectAt(x, y, what) {
|
|
521
|
+
const lo = x >= this.sideX ? this.sideX : this.viewX, hi = x >= this.sideX ? this.canvas.w - 1 : this.viewX + this.viewW - 2;
|
|
522
|
+
const row = this.canvas.readRow(y, lo, hi + 1), at = x - lo;
|
|
523
|
+
if (at < 0 || at >= row.length)
|
|
524
|
+
return;
|
|
525
|
+
let a = 0, z = row.length - 1;
|
|
526
|
+
if (what === "word") {
|
|
527
|
+
if (/\s/.test(row[at]))
|
|
528
|
+
return;
|
|
529
|
+
a = at;
|
|
530
|
+
z = at;
|
|
531
|
+
while (a > 0 && !/\s/.test(row[a - 1]))
|
|
532
|
+
a--;
|
|
533
|
+
while (z < row.length - 1 && !/\s/.test(row[z + 1]))
|
|
534
|
+
z++;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
while (z > 0 && /\s/.test(row[z]))
|
|
538
|
+
z--;
|
|
539
|
+
while (a < z && /\s/.test(row[a]))
|
|
540
|
+
a++;
|
|
541
|
+
}
|
|
542
|
+
this.sel = { ax: lo + a, ay: y, hx: lo + z, hy: y, done: true, mode: "block" };
|
|
543
|
+
this.copySelection();
|
|
544
|
+
this.app.render();
|
|
545
|
+
}
|
|
546
|
+
/** Clicks inside the input panel move the text cursor. */
|
|
547
|
+
input(e) { void e; return false; }
|
|
548
|
+
click(x, y) {
|
|
549
|
+
for (let i = this.regions.length - 1; i >= 0; i--) {
|
|
550
|
+
const r = this.regions[i];
|
|
551
|
+
if (x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h && r.click) {
|
|
552
|
+
r.click();
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
modalClick(h) {
|
|
559
|
+
const o = this.app.overlay;
|
|
560
|
+
if (o instanceof ChoiceOverlay && h.kind === "option")
|
|
561
|
+
o.done(h.index);
|
|
562
|
+
else if (o instanceof PickerOverlay && h.kind === "item")
|
|
563
|
+
o.pick(h.index);
|
|
564
|
+
this.app.render();
|
|
565
|
+
}
|
|
566
|
+
copySelection() {
|
|
567
|
+
const s = this.sel;
|
|
568
|
+
if (!s)
|
|
569
|
+
return;
|
|
570
|
+
const [a, b] = ordered(s);
|
|
571
|
+
const lines = [];
|
|
572
|
+
for (let y = a.y; y <= b.y; y++) {
|
|
573
|
+
if (s.mode === "block")
|
|
574
|
+
lines.push(this.canvas.readRow(y, Math.min(s.ax, s.hx), Math.max(s.ax, s.hx) + 1));
|
|
575
|
+
else
|
|
576
|
+
lines.push(this.canvas.readRow(y, y === a.y ? a.x : this.viewX, (y === b.y ? b.x : this.viewX + this.viewW - 2) + 1));
|
|
577
|
+
}
|
|
578
|
+
const text = lines.map((l) => l.replace(/\s+$/, "")).join("\n").replace(/\s+$/g, "");
|
|
579
|
+
if (!text)
|
|
580
|
+
return;
|
|
581
|
+
this.copy(text);
|
|
582
|
+
this.toast(`copied ${text.length} characters`, "ok");
|
|
583
|
+
}
|
|
584
|
+
copy(text) { copyToClipboard(this.app.io.output, text, this.app.persist); }
|
|
585
|
+
clearSelection() { if (this.sel) {
|
|
586
|
+
this.sel = undefined;
|
|
587
|
+
return true;
|
|
588
|
+
} return false; }
|
|
589
|
+
/** Scroll keys; returns whether the key was used. */
|
|
590
|
+
key(name, ctrl) {
|
|
591
|
+
const page = Math.max(3, Math.floor(this.viewH / 2));
|
|
592
|
+
if (name === "pageup") {
|
|
593
|
+
this.scrollBy(page);
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
if (name === "pagedown") {
|
|
597
|
+
this.scrollBy(-page);
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
if (ctrl && name === "home") {
|
|
601
|
+
this.scrollTo(Math.max(0, this.total));
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
if (ctrl && name === "end") {
|
|
605
|
+
this.scrollTo(0);
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const ordered = (s) => (s.ay < s.hy || (s.ay === s.hy && s.ax <= s.hx) ? [{ x: s.ax, y: s.ay }, { x: s.hx, y: s.hy }] : [{ x: s.hx, y: s.hy }, { x: s.ax, y: s.ay }]);
|
|
612
|
+
export function gradientText(text, stops, phase = 0) {
|
|
613
|
+
const chars = [...text], n = Math.max(1, chars.length - 1);
|
|
614
|
+
return chars.map((ch, i) => (ch === " " ? ch : F(sample(stops, (i / n + phase) % 1), ch))).join("");
|
|
615
|
+
}
|
|
616
|
+
const sample = (stops, x) => { const p = Math.max(0, Math.min(1, x)) * (stops.length - 1), i = Math.min(stops.length - 2, Math.floor(p)); return stops.length === 1 ? stops[0] : mix(stops[i], stops[i + 1], p - i); };
|
|
617
|
+
const VERBS = { read_file: "Reading", list_files: "Listing", grep: "Searching", write_file: "Writing", edit_file: "Editing", sh: "Running", web_search: "Searching the web for", fetch_url: "Fetching", task: "Delegating", memory_save: "Saving note", memory_search: "Recalling", todo_write: "Planning" };
|
|
618
|
+
/** "Reading src/app.ts": what a tool call is doing, for the status line and the run panel. */
|
|
619
|
+
export function toolVerb(name, args, past = false) {
|
|
620
|
+
const a = argSummary(name, args), v = VERBS[name] ?? toolLabel(name);
|
|
621
|
+
return past ? `${toolLabel(name)} ${a}`.trim() : `${v} ${a}`.trim();
|
|
622
|
+
}
|
|
623
|
+
const shortPath = (p) => { const h = process.env.HOME ?? ""; return h && p.startsWith(h) ? "~" + p.slice(h.length) : p; };
|
|
624
|
+
function ago(iso) { const s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000); return s < 90 ? "just now" : s < 5400 ? `${Math.round(s / 60)}m ago` : s < 129600 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago`; }
|
|
625
|
+
void InputOverlay;
|
|
626
|
+
void ITAL;
|
|
627
|
+
void BOLD;
|
|
628
|
+
void split;
|
|
629
|
+
void estimateCost;
|
|
630
|
+
void {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type MouseEv = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
button: "left" | "middle" | "right" | "none";
|
|
5
|
+
action: "press" | "release" | "drag" | "move" | "wheelup" | "wheeldown";
|
|
6
|
+
shift?: boolean;
|
|
7
|
+
alt?: boolean;
|
|
8
|
+
ctrl?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type Key = {
|
|
11
|
+
name: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
ctrl?: boolean;
|
|
14
|
+
alt?: boolean;
|
|
15
|
+
shift?: boolean;
|
|
16
|
+
mouse?: MouseEv;
|
|
17
|
+
};
|
|
18
|
+
export declare const PASTE_ON = "\u001B[?2004h", PASTE_OFF = "\u001B[?2004l";
|
|
19
|
+
/** Mouse reporting: button events + drag motion, SGR encoding (so coordinates beyond 223 work). */
|
|
20
|
+
export declare const MOUSE_ON = "\u001B[?1000h\u001B[?1002h\u001B[?1006h", MOUSE_OFF = "\u001B[?1006l\u001B[?1002l\u001B[?1000l";
|
|
21
|
+
export declare class KeyParser {
|
|
22
|
+
private buf;
|
|
23
|
+
private pasting;
|
|
24
|
+
/** Feed raw input; returns the keys that are complete. A trailing lone ESC (or partial sequence) is held until `flush()`. */
|
|
25
|
+
feed(chunk: string): Key[];
|
|
26
|
+
/** Called after a short idle: whatever is pending (a lone ESC) is a real key press. */
|
|
27
|
+
flush(): Key[];
|
|
28
|
+
get pending(): boolean;
|
|
29
|
+
private one;
|
|
30
|
+
private ctrlOrChar;
|
|
31
|
+
}
|