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,103 @@
|
|
|
1
|
+
// Conversation persistence per project, plus undo/rewind of the file changes each turn made.
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
export const newSession = (cwd, model) => ({
|
|
5
|
+
id: new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14) + "-" + Math.random().toString(36).slice(2, 6),
|
|
6
|
+
created: new Date().toISOString(), updated: new Date().toISOString(), title: "", cwd, model, messages: [], turns: [], usage: { prompt: 0, completion: 0, calls: 0, series: [] },
|
|
7
|
+
});
|
|
8
|
+
export class SessionStore {
|
|
9
|
+
dir;
|
|
10
|
+
constructor(cwd) { this.dir = join(cwd, ".amness", "chats"); }
|
|
11
|
+
file(id) { return join(this.dir, id.replace(/[^\w-]/g, "") + ".json"); }
|
|
12
|
+
save(s) { mkdirSync(this.dir, { recursive: true }); s.updated = new Date().toISOString(); writeFileSync(this.file(s.id), JSON.stringify(s)); }
|
|
13
|
+
load(id) { try {
|
|
14
|
+
return JSON.parse(readFileSync(this.file(id), "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
} }
|
|
19
|
+
list() {
|
|
20
|
+
if (!existsSync(this.dir))
|
|
21
|
+
return [];
|
|
22
|
+
return readdirSync(this.dir).filter((f) => f.endsWith(".json")).flatMap((f) => {
|
|
23
|
+
try {
|
|
24
|
+
const s = JSON.parse(readFileSync(join(this.dir, f), "utf8"));
|
|
25
|
+
return s.turns.length ? [{ id: s.id, title: s.title, updated: s.updated, turns: s.turns.length, model: s.model }] : [];
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}).sort((a, b) => b.updated.localeCompare(a.updated));
|
|
31
|
+
}
|
|
32
|
+
latest() { const l = this.list()[0]; return l ? this.load(l.id) : undefined; }
|
|
33
|
+
remove(id) { rmSync(this.file(id), { force: true }); }
|
|
34
|
+
}
|
|
35
|
+
/** Record the pre-edit content for `path` the first time this turn touches it. */
|
|
36
|
+
export function snapshot(turn, path, before) {
|
|
37
|
+
if (turn && before !== undefined && !(path in turn.snaps))
|
|
38
|
+
turn.snaps[path] = before;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Roll the conversation back to just before turn `idx`: truncate messages, drop later turns, and restore every file those turns changed
|
|
42
|
+
* (the earliest snapshot wins). Never touches paths outside `cwd`.
|
|
43
|
+
*/
|
|
44
|
+
export function rewind(s, cwd, idx) {
|
|
45
|
+
const root = resolve(cwd), restored = [], removed = [], skipped = [];
|
|
46
|
+
const target = s.turns[idx];
|
|
47
|
+
if (!target)
|
|
48
|
+
return { restored, removed, skipped };
|
|
49
|
+
const earliest = new Map();
|
|
50
|
+
for (const t of s.turns.slice(idx))
|
|
51
|
+
for (const [p, before] of Object.entries(t.snaps))
|
|
52
|
+
if (!earliest.has(p))
|
|
53
|
+
earliest.set(p, before);
|
|
54
|
+
for (const [p, before] of earliest) {
|
|
55
|
+
const abs = resolve(root, p);
|
|
56
|
+
if (abs !== root && !abs.startsWith(root + sep)) {
|
|
57
|
+
skipped.push(p);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (before === null) {
|
|
61
|
+
if (existsSync(abs) && statSync(abs).isFile()) {
|
|
62
|
+
rmSync(abs);
|
|
63
|
+
removed.push(p);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
68
|
+
writeFileSync(abs, before);
|
|
69
|
+
restored.push(p);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
s.messages = s.messages.slice(0, target.at);
|
|
73
|
+
s.turns = s.turns.slice(0, idx);
|
|
74
|
+
return { restored, removed, skipped };
|
|
75
|
+
}
|
|
76
|
+
/** Like `rewind`, but first puts the working tree back to the earliest shell snapshot in the turns being undone (undoing what commands did), and reports what could not be undone. */
|
|
77
|
+
export async function rewindFull(s, cwd, idx, snaps) {
|
|
78
|
+
const undone = s.turns.slice(idx), tree = undone.find((t) => t.shell?.tree)?.shell?.tree, irreversible = undone.flatMap((t) => t.shell?.irreversible ?? []);
|
|
79
|
+
let treeResult, treeError;
|
|
80
|
+
if (tree && snaps) {
|
|
81
|
+
try {
|
|
82
|
+
treeResult = await snaps.restore(tree);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
treeError = e.message;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { ...rewind(s, cwd, idx), tree: treeResult, irreversible, treeError };
|
|
89
|
+
}
|
|
90
|
+
/** A new conversation that continues from the end of turn `idx` (0-based) of `s`. The original is untouched. */
|
|
91
|
+
export function forkSession(s, idx) {
|
|
92
|
+
const keep = s.turns.slice(0, idx + 1), end = s.turns[idx + 1]?.at ?? s.messages.length, f = newSession(s.cwd, s.model);
|
|
93
|
+
return { ...f, title: (s.title || "(untitled)").replace(/ \(branch \d+\)$/, "") + ` (branch ${Math.random().toString(36).slice(2, 5)})`, messages: JSON.parse(JSON.stringify(s.messages.slice(0, end))), turns: JSON.parse(JSON.stringify(keep)), usage: { ...s.usage, series: [...(s.usage.series ?? [])] }, parent: { id: s.id, turn: idx } };
|
|
94
|
+
}
|
|
95
|
+
/** Every conversation connected to `id` by branching (its ancestors, descendants and siblings), `id` itself included. */
|
|
96
|
+
export function relatives(all, id) {
|
|
97
|
+
const parentOf = new Map(all.map((s) => [s.id, s.parent?.id])), root = (x) => { const seen = new Set(); while (parentOf.get(x) && !seen.has(x)) {
|
|
98
|
+
seen.add(x);
|
|
99
|
+
x = parentOf.get(x);
|
|
100
|
+
} return x; };
|
|
101
|
+
const r = root(id);
|
|
102
|
+
return all.filter((s) => root(s.id) === r).map((s) => s.id);
|
|
103
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type Cell } from "./ansi.ts";
|
|
2
|
+
import type { Theme } from "./themes.ts";
|
|
3
|
+
export type SidebarData = {
|
|
4
|
+
title: string;
|
|
5
|
+
model: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
api?: string;
|
|
8
|
+
branch?: string;
|
|
9
|
+
cwd: string;
|
|
10
|
+
mode: string;
|
|
11
|
+
effort?: string;
|
|
12
|
+
ctxPct: number | null;
|
|
13
|
+
ctxUsed: number;
|
|
14
|
+
ctxMax: number;
|
|
15
|
+
series: number[];
|
|
16
|
+
map: {
|
|
17
|
+
label: string;
|
|
18
|
+
pct: number;
|
|
19
|
+
color: number;
|
|
20
|
+
}[];
|
|
21
|
+
usage: {
|
|
22
|
+
prompt: number;
|
|
23
|
+
completion: number;
|
|
24
|
+
calls: number;
|
|
25
|
+
cached: number;
|
|
26
|
+
reasoning: number;
|
|
27
|
+
};
|
|
28
|
+
cost: number | null;
|
|
29
|
+
todos: {
|
|
30
|
+
content: string;
|
|
31
|
+
status: string;
|
|
32
|
+
}[];
|
|
33
|
+
changes: {
|
|
34
|
+
path: string;
|
|
35
|
+
added: number;
|
|
36
|
+
removed: number;
|
|
37
|
+
created: boolean;
|
|
38
|
+
blockId: number;
|
|
39
|
+
}[];
|
|
40
|
+
run?: {
|
|
41
|
+
phase: string;
|
|
42
|
+
step: number;
|
|
43
|
+
max: number;
|
|
44
|
+
elapsedMs: number;
|
|
45
|
+
frame: number;
|
|
46
|
+
live: {
|
|
47
|
+
label: string;
|
|
48
|
+
ms: number;
|
|
49
|
+
agent?: string;
|
|
50
|
+
}[];
|
|
51
|
+
done: {
|
|
52
|
+
label: string;
|
|
53
|
+
ok: boolean;
|
|
54
|
+
ms: number;
|
|
55
|
+
}[];
|
|
56
|
+
};
|
|
57
|
+
sandbox?: string;
|
|
58
|
+
/** one line per plugin status (see Plugin.status) */ pluginStatus?: string[];
|
|
59
|
+
/** the token cap for the current/last turn: tokens counted (cached input at a tenth), the cap, and whether usage was estimated */ budget?: {
|
|
60
|
+
used: number;
|
|
61
|
+
max: number;
|
|
62
|
+
};
|
|
63
|
+
procs?: {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
status: string;
|
|
67
|
+
secs: number;
|
|
68
|
+
}[];
|
|
69
|
+
tools: number;
|
|
70
|
+
mcp: number;
|
|
71
|
+
memory: number;
|
|
72
|
+
running: boolean;
|
|
73
|
+
elapsedMs: number;
|
|
74
|
+
tip?: string;
|
|
75
|
+
};
|
|
76
|
+
export type SideHit = {
|
|
77
|
+
row: number;
|
|
78
|
+
kind: "change" | "todos";
|
|
79
|
+
blockId?: number;
|
|
80
|
+
};
|
|
81
|
+
export declare function renderSidebar(d: SidebarData, t: Theme, width: number, height: number): {
|
|
82
|
+
rows: Cell[][];
|
|
83
|
+
hits: SideHit[];
|
|
84
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The right-hand panel: what is going on in this session at a glance.
|
|
2
|
+
import { A, mix, parseAnsi } from "./ansi.js";
|
|
3
|
+
import { BOLD, F, ITAL, fit, split } from "./rich.js";
|
|
4
|
+
import { cellWidth, clipWidth } from "./text.js";
|
|
5
|
+
import { brandOfModel } from "./brand.js";
|
|
6
|
+
import { fmtCost, fmtTokens } from "./pricing.js";
|
|
7
|
+
const BARS = "▁▂▃▄▅▆▇█";
|
|
8
|
+
export function renderSidebar(d, t, width, height) {
|
|
9
|
+
const inner = width - 4, out = [], hits = [], bg = t.surface;
|
|
10
|
+
const push = (s = "") => out.push(fit(" " + s, width - 1, bg));
|
|
11
|
+
const head = (title, right = "") => { if (out.length > 1)
|
|
12
|
+
push(); push(right ? split(F(t.faint, BOLD(title)), right, inner, bg) : F(t.faint, BOLD(title))); };
|
|
13
|
+
const brand = brandOfModel(d.model, d.provider, t);
|
|
14
|
+
push();
|
|
15
|
+
if (d.run) {
|
|
16
|
+
const r = d.run, SPIN = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏", stages = [["model", "think"], ["tools", "act"], ["verify", "verify"]];
|
|
17
|
+
head("RUN", F(t.dim, `${(r.elapsedMs / 1000).toFixed(0)}s`));
|
|
18
|
+
push(stages.map(([k, label], i) => (k === r.phase || (r.phase === "compact" && k === "model") ? BOLD(F(t.accent, "● " + label)) : F(t.faint, "○ " + label)) + (i < 2 ? F(t.border, " ─ ") : "")).join(""));
|
|
19
|
+
const lbl = `step ${r.step + 1}/${r.max}`, bw = Math.max(6, Math.min(24, inner - cellWidth(lbl) - 2)), f = Math.max(1, Math.round(((r.step + 1) / Math.max(1, r.max)) * bw));
|
|
20
|
+
push(F(r.step + 1 >= r.max - 2 ? t.warn : t.accent2, "▰".repeat(f)) + F(t.border, "▱".repeat(bw - f)) + F(t.dim, " " + lbl));
|
|
21
|
+
for (const x of r.live.slice(0, 5))
|
|
22
|
+
push(F(t.accent2, SPIN[r.frame % SPIN.length]) + " " + F(t.text, clipWidth(x.label, inner - 9)) + F(t.faint, ` ${fmtMs(x.ms)}`));
|
|
23
|
+
for (const x of r.done.slice(-4).reverse())
|
|
24
|
+
push(F(x.ok ? t.ok : t.err, x.ok ? "✓" : "✗") + " " + F(t.dim, clipWidth(x.label, inner - 9)) + F(t.faint, x.ms >= 50 ? ` ${fmtMs(x.ms)}` : ""));
|
|
25
|
+
}
|
|
26
|
+
head("SESSION");
|
|
27
|
+
for (const l of wrap(d.title || "New conversation", inner, 2))
|
|
28
|
+
push(BOLD(F(t.text, l)));
|
|
29
|
+
push(F(brand.color, brand.icon) + " " + F(t.text, clipWidth(d.model, inner - 4)));
|
|
30
|
+
push(F(t.faint, [d.api ? `${d.api} api` : "", d.effort ? `effort ${d.effort}` : ""].filter(Boolean).join(" · ") || " "));
|
|
31
|
+
push(F(t.dim, "⎇ " + (d.branch ?? "no git") + " ") + F(t.faint, tail(d.cwd, Math.max(6, inner - 4 - (d.branch?.length ?? 6)))));
|
|
32
|
+
head("CONTEXT", d.ctxPct === null ? "" : F(pctColor(d.ctxPct, t), `${d.ctxPct}%`));
|
|
33
|
+
const bw = inner, filled = Math.round(((d.ctxPct ?? 0) / 100) * bw);
|
|
34
|
+
push(F(pctColor(d.ctxPct ?? 0, t), "▰".repeat(filled)) + F(t.border, "▱".repeat(Math.max(0, bw - filled))));
|
|
35
|
+
push(F(t.dim, `${fmtTokens(d.ctxUsed)} / ${fmtTokens(d.ctxMax)} tokens`));
|
|
36
|
+
if (d.series.length > 1) {
|
|
37
|
+
const v = d.series.slice(-inner), mx = Math.max(1, ...v);
|
|
38
|
+
push(F(t.accent2, v.map((x) => BARS[Math.min(7, Math.round((x / mx) * 7))]).join("")) + F(t.faint, " per request"));
|
|
39
|
+
}
|
|
40
|
+
const mapRow = d.map.filter((m) => m.pct > 0).slice(0, 3).map((m) => F(m.color, "■") + F(t.dim, ` ${m.label} ${m.pct}%`)).join(" ");
|
|
41
|
+
if (mapRow)
|
|
42
|
+
for (const l of wrapPlain(mapRow, inner))
|
|
43
|
+
push(l);
|
|
44
|
+
head("USAGE");
|
|
45
|
+
push(F(t.dim, "in ") + F(t.text, fmtTokens(d.usage.prompt)) + F(t.dim, " out ") + F(t.text, fmtTokens(d.usage.completion)) + F(t.dim, " ") + F(t.faint, `${d.usage.calls} req`));
|
|
46
|
+
if (d.budget && Number.isFinite(d.budget.max) && d.budget.used > 0) {
|
|
47
|
+
const pct = Math.min(100, Math.round((d.budget.used / d.budget.max) * 100));
|
|
48
|
+
push(F(t.dim, "budget ") + F(pct >= 90 ? t.err : pct >= 75 ? t.warn : t.text, `${pct}%`) + F(t.faint, ` of ${fmtTokens(d.budget.max)} this turn`));
|
|
49
|
+
}
|
|
50
|
+
if (d.usage.cached)
|
|
51
|
+
push(F(t.dim, "cache ") + F(t.ok, `${Math.round((d.usage.cached / Math.max(1, d.usage.prompt)) * 100)}%`) + (d.usage.reasoning ? F(t.dim, " reasoning ") + F(t.text, fmtTokens(d.usage.reasoning)) : ""));
|
|
52
|
+
push(F(t.dim, "cost ") + (d.cost === null ? F(t.faint, "n/a") : BOLD(F(t.text, "≈ " + fmtCost(d.cost)))));
|
|
53
|
+
if (d.todos.length) {
|
|
54
|
+
const done = d.todos.filter((x) => x.status === "done").length;
|
|
55
|
+
head("TODOS", F(t.dim, `${done}/${d.todos.length}`));
|
|
56
|
+
for (const x of d.todos.slice(0, 7))
|
|
57
|
+
push((x.status === "done" ? F(t.ok, "✓ ") + F(t.faint, clipWidth(x.content, inner - 2)) : x.status === "in_progress" ? F(t.warn, "▶ ") + BOLD(F(t.text, clipWidth(x.content, inner - 2))) : F(t.faint, "○ ") + F(t.dim, clipWidth(x.content, inner - 2))));
|
|
58
|
+
if (d.todos.length > 7)
|
|
59
|
+
push(F(t.faint, `+${d.todos.length - 7} more`));
|
|
60
|
+
}
|
|
61
|
+
if (d.changes.length) {
|
|
62
|
+
head("CHANGES", F(t.dim, String(d.changes.length)));
|
|
63
|
+
for (const c of d.changes.slice(0, 8)) {
|
|
64
|
+
const name = c.path.split("/").pop() ?? c.path, dir = c.path.includes("/") ? c.path.slice(0, c.path.length - name.length) : "", stat = F(t.addFg, `+${c.added}`) + " " + F(t.delFg, `−${c.removed}`);
|
|
65
|
+
hits.push({ row: out.length, kind: "change", blockId: c.blockId });
|
|
66
|
+
push(split(F(c.created ? t.ok : t.warn, c.created ? "A " : "M ") + F(t.text, clipWidth(name, inner - 14)) + F(t.faint, " " + clipWidth(dir, 8)), stat, inner, bg));
|
|
67
|
+
}
|
|
68
|
+
if (d.changes.length > 8)
|
|
69
|
+
push(F(t.faint, `+${d.changes.length - 8} more files`));
|
|
70
|
+
}
|
|
71
|
+
if (d.procs?.length) {
|
|
72
|
+
head("PROCESSES");
|
|
73
|
+
for (const p of d.procs.slice(0, 5))
|
|
74
|
+
push((p.status === "running" ? F(t.ok, "● ") : F(t.faint, "○ ")) + F(t.text, clipWidth(`${p.id} ${p.name}`, inner - 10)) + F(t.faint, ` ${p.status === "running" ? p.secs + "s" : p.status}`));
|
|
75
|
+
}
|
|
76
|
+
head("WORKSPACE");
|
|
77
|
+
if (d.sandbox && d.sandbox !== "off")
|
|
78
|
+
push(F(t.ok, "⛨ ") + F(t.dim, `sandbox ${d.sandbox}`));
|
|
79
|
+
for (const s of d.pluginStatus ?? [])
|
|
80
|
+
push(F(t.accent2, "◇ ") + F(t.dim, clipWidth(s, inner - 3)));
|
|
81
|
+
push(F(t.dim, `${d.tools} tools`) + (d.mcp ? F(t.faint, ` · ${d.mcp} mcp`) : "") + (d.memory ? F(t.faint, ` · ${d.memory} notes`) : ""));
|
|
82
|
+
if (d.tip) {
|
|
83
|
+
push();
|
|
84
|
+
for (const l of wrap(d.tip, inner, 3))
|
|
85
|
+
push(F(t.faint, ITAL(l)));
|
|
86
|
+
}
|
|
87
|
+
const rows = out.slice(0, height).map((r) => parseAnsi(r));
|
|
88
|
+
while (rows.length < height)
|
|
89
|
+
rows.push(parseAnsi(fit("", width - 1, bg)));
|
|
90
|
+
return { rows, hits };
|
|
91
|
+
}
|
|
92
|
+
const tail = (s, w) => (cellWidth(s) <= w ? s : "…" + s.slice(-(w - 1)));
|
|
93
|
+
const fmtMs = (ms) => (ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`);
|
|
94
|
+
const pctColor = (p, t) => (p > 85 ? t.err : p > 60 ? t.warn : t.ok);
|
|
95
|
+
function wrap(s, w, maxLines) {
|
|
96
|
+
const words = s.split(/\s+/), lines = [""];
|
|
97
|
+
for (const word of words) {
|
|
98
|
+
const last = lines[lines.length - 1];
|
|
99
|
+
if (cellWidth(last + " " + word) <= w)
|
|
100
|
+
lines[lines.length - 1] = (last ? last + " " : "") + word;
|
|
101
|
+
else
|
|
102
|
+
lines.push(word);
|
|
103
|
+
}
|
|
104
|
+
const cut = lines.slice(0, maxLines);
|
|
105
|
+
if (lines.length > maxLines)
|
|
106
|
+
cut[maxLines - 1] = clipWidth(cut[maxLines - 1], w - 1) + "…";
|
|
107
|
+
return cut.map((l) => clipWidth(l, w));
|
|
108
|
+
}
|
|
109
|
+
const wrapPlain = (s, w) => (cellWidth(s.replace(/\x1b\[[0-9;]*m/g, "")) <= w ? [s] : s.split(" ").reduce((acc, part) => { const cur = acc[acc.length - 1]; if (cur !== undefined && cellWidth((cur + " " + part).replace(/\x1b\[[0-9;]*m/g, "")) <= w)
|
|
110
|
+
acc[acc.length - 1] = cur + " " + part;
|
|
111
|
+
else
|
|
112
|
+
acc.push(part); return acc; }, []));
|
|
113
|
+
void A;
|
|
114
|
+
void mix;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const strip: (s: string) => string;
|
|
2
|
+
/** Columns a single code point occupies. */
|
|
3
|
+
export declare function cpWidth(cp: number): number;
|
|
4
|
+
/** Display width of a string (ANSI codes ignored). */
|
|
5
|
+
export declare function cellWidth(s: string): number;
|
|
6
|
+
/** Cut a plain string to at most `max` columns. */
|
|
7
|
+
export declare function clipWidth(s: string, max: number): string;
|
|
8
|
+
/**
|
|
9
|
+
* Word-wrap one logical line (may contain ANSI codes) to `width` columns. Continuation lines get `indent` spaces.
|
|
10
|
+
* Words longer than the width are hard-cut. Styles active at a break are re-opened is NOT attempted; each wrapped
|
|
11
|
+
* row ends with a reset so colours never bleed, and the next row restarts plain (callers style before wrapping short spans).
|
|
12
|
+
*/
|
|
13
|
+
export declare function wrapAnsi(line: string, width: number, indent?: number): string[];
|
|
14
|
+
/** Cut an ANSI string to at most `max` columns, keeping escape codes intact and closing any open style. */
|
|
15
|
+
export declare function clipAnsi(line: string, max: number): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Terminal text primitives: ANSI stripping, display width (wide chars / emoji), and ANSI-aware wrapping.
|
|
2
|
+
export const strip = (s) => s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
|
|
3
|
+
/** Columns a single code point occupies. */
|
|
4
|
+
export function cpWidth(cp) {
|
|
5
|
+
if (cp === 0 || cp < 32 || (cp >= 0x7f && cp < 0xa0))
|
|
6
|
+
return 0;
|
|
7
|
+
if ((cp >= 0x300 && cp <= 0x36f) || (cp >= 0x200b && cp <= 0x200f) || (cp >= 0xfe00 && cp <= 0xfe0f) || (cp >= 0x20d0 && cp <= 0x20ff) || cp === 0x200d)
|
|
8
|
+
return 0;
|
|
9
|
+
if ((cp >= 0x1100 && cp <= 0x115f) || (cp >= 0x2e80 && cp <= 0xa4cf) || (cp >= 0xac00 && cp <= 0xd7a3) || (cp >= 0xf900 && cp <= 0xfaff) ||
|
|
10
|
+
(cp >= 0xfe30 && cp <= 0xfe6f) || (cp >= 0xff00 && cp <= 0xff60) || (cp >= 0xffe0 && cp <= 0xffe6) || (cp >= 0x1f300 && cp <= 0x1f64f) ||
|
|
11
|
+
(cp >= 0x1f680 && cp <= 0x1f6ff) || (cp >= 0x1f900 && cp <= 0x1f9ff) || (cp >= 0x20000 && cp <= 0x3fffd))
|
|
12
|
+
return 2;
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
/** Display width of a string (ANSI codes ignored). */
|
|
16
|
+
export function cellWidth(s) {
|
|
17
|
+
let w = 0;
|
|
18
|
+
for (const ch of strip(s))
|
|
19
|
+
w += cpWidth(ch.codePointAt(0));
|
|
20
|
+
return w;
|
|
21
|
+
}
|
|
22
|
+
/** Cut a plain string to at most `max` columns. */
|
|
23
|
+
export function clipWidth(s, max) {
|
|
24
|
+
let w = 0, out = "";
|
|
25
|
+
for (const ch of s) {
|
|
26
|
+
const cw = cpWidth(ch.codePointAt(0));
|
|
27
|
+
if (w + cw > max)
|
|
28
|
+
break;
|
|
29
|
+
out += ch;
|
|
30
|
+
w += cw;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Word-wrap one logical line (may contain ANSI codes) to `width` columns. Continuation lines get `indent` spaces.
|
|
36
|
+
* Words longer than the width are hard-cut. Styles active at a break are re-opened is NOT attempted; each wrapped
|
|
37
|
+
* row ends with a reset so colours never bleed, and the next row restarts plain (callers style before wrapping short spans).
|
|
38
|
+
*/
|
|
39
|
+
export function wrapAnsi(line, width, indent = 0) {
|
|
40
|
+
width = Math.max(4, width);
|
|
41
|
+
if (cellWidth(line) <= width)
|
|
42
|
+
return [line];
|
|
43
|
+
const reset = "\x1b[0m", hasAnsi = line.includes("\x1b");
|
|
44
|
+
const rows = [];
|
|
45
|
+
let cur = "", vis = 0, lastSpace = -1, lastSpaceVis = 0;
|
|
46
|
+
const pad = " ".repeat(indent);
|
|
47
|
+
for (const tok of line.match(/\x1b\[[0-9;?]*[A-Za-z]|[\s\S]/gu) ?? []) {
|
|
48
|
+
if (tok[0] === "\x1b" && tok.length > 1) {
|
|
49
|
+
cur += tok;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const w = cpWidth(tok.codePointAt(0));
|
|
53
|
+
if (vis + w > width) {
|
|
54
|
+
if (lastSpace > 0) {
|
|
55
|
+
rows.push(cur.slice(0, lastSpace).replace(/\s+$/, "") + (hasAnsi ? reset : ""));
|
|
56
|
+
cur = pad + cur.slice(lastSpace + 1);
|
|
57
|
+
vis = cellWidth(cur);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
rows.push(cur + (hasAnsi ? reset : ""));
|
|
61
|
+
cur = pad;
|
|
62
|
+
vis = indent;
|
|
63
|
+
}
|
|
64
|
+
lastSpace = -1;
|
|
65
|
+
}
|
|
66
|
+
if (tok === " ") {
|
|
67
|
+
lastSpace = cur.length;
|
|
68
|
+
lastSpaceVis = vis;
|
|
69
|
+
}
|
|
70
|
+
cur += tok;
|
|
71
|
+
vis += w;
|
|
72
|
+
}
|
|
73
|
+
void lastSpaceVis;
|
|
74
|
+
rows.push(cur);
|
|
75
|
+
return rows;
|
|
76
|
+
}
|
|
77
|
+
/** Cut an ANSI string to at most `max` columns, keeping escape codes intact and closing any open style. */
|
|
78
|
+
export function clipAnsi(line, max) {
|
|
79
|
+
if (cellWidth(line) <= max)
|
|
80
|
+
return line;
|
|
81
|
+
let out = "", w = 0;
|
|
82
|
+
for (const tok of line.match(/\x1b\[[0-9;?]*[A-Za-z]|[\s\S]/gu) ?? []) {
|
|
83
|
+
if (tok[0] === "\x1b" && tok.length > 1) {
|
|
84
|
+
out += tok;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const cw = cpWidth(tok.codePointAt(0));
|
|
88
|
+
if (w + cw > max - 1)
|
|
89
|
+
break;
|
|
90
|
+
out += tok;
|
|
91
|
+
w += cw;
|
|
92
|
+
}
|
|
93
|
+
return out + "…" + (line.includes("\x1b") ? "\x1b[0m" : "");
|
|
94
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type RGB = [number, number, number];
|
|
2
|
+
export declare function detectLevel(env?: Record<string, string | undefined>, tty?: boolean): 0 | 1 | 2 | 3;
|
|
3
|
+
export declare let level: 0 | 2 | 1 | 3;
|
|
4
|
+
/** Tests (and `--color` style flags) can override detection. */
|
|
5
|
+
export declare const setLevel: (l: 0 | 1 | 2 | 3) => void;
|
|
6
|
+
/** Foreground colour; resets only the foreground so bold/dim keep working around it. */
|
|
7
|
+
export declare function fg(c: RGB, s: string): string;
|
|
8
|
+
export declare const bold: (s: string) => string;
|
|
9
|
+
export declare const dimmed: (s: string) => string;
|
|
10
|
+
export declare const lerp: (a: RGB, b: RGB, t: number) => RGB;
|
|
11
|
+
/** Colour at position t∈[0,1] along a multi-stop gradient. */
|
|
12
|
+
export declare function at(stops: RGB[], t: number): RGB;
|
|
13
|
+
/** Paint each character along a gradient (a single colour at low colour depth, so it stays readable). */
|
|
14
|
+
export declare function gradient(text: string, stops: RGB[], phase?: number): string;
|
|
15
|
+
export declare const palette: {
|
|
16
|
+
brand: RGB[];
|
|
17
|
+
claude: RGB;
|
|
18
|
+
openai: RGB;
|
|
19
|
+
local: RGB;
|
|
20
|
+
other: RGB;
|
|
21
|
+
read: RGB;
|
|
22
|
+
edit: RGB;
|
|
23
|
+
shell: RGB;
|
|
24
|
+
web: RGB;
|
|
25
|
+
memory: RGB;
|
|
26
|
+
plan: RGB;
|
|
27
|
+
explore: RGB;
|
|
28
|
+
generic: RGB;
|
|
29
|
+
ok: RGB;
|
|
30
|
+
warn: RGB;
|
|
31
|
+
err: RGB;
|
|
32
|
+
muted: RGB;
|
|
33
|
+
gauge: RGB[];
|
|
34
|
+
};
|
|
35
|
+
export declare const toolColor: (name: string) => RGB;
|
|
36
|
+
/** Provider/model identity: a glyph and an accent colour. */
|
|
37
|
+
export declare function brandOf(model: string, provider?: string): {
|
|
38
|
+
icon: string;
|
|
39
|
+
color: RGB;
|
|
40
|
+
};
|
|
41
|
+
/** ▰▰▰▱▱▱▱▱ context gauge. Filled cells shift green → amber → red the fuller the window gets. */
|
|
42
|
+
export declare function gauge(pct: number, width?: number): string;
|
|
43
|
+
export declare const gaugeColor: (pct: number) => RGB;
|
|
44
|
+
/** Tiny bar chart of the last `width` values. */
|
|
45
|
+
export declare function sparkline(values: number[], width?: number, color?: RGB): string;
|
|
46
|
+
/** A moving highlight sweeping across `text` (the "thinking" shimmer). Plain bold when colours are limited. */
|
|
47
|
+
export declare function shimmer(text: string, frame: number, base?: RGB, hi?: RGB): string;
|
|
48
|
+
/** Stacked horizontal bar from weighted segments (for the context map). */
|
|
49
|
+
export declare function stackedBar(parts: {
|
|
50
|
+
label: string;
|
|
51
|
+
value: number;
|
|
52
|
+
color: RGB;
|
|
53
|
+
}[], width?: number): {
|
|
54
|
+
bar: string;
|
|
55
|
+
legend: string[];
|
|
56
|
+
};
|
|
57
|
+
/** Pixel mascot + half-block wordmark, painted along the brand gradient. */
|
|
58
|
+
export declare function logo(version: string): string[];
|
|
59
|
+
export declare const TIPS: string[];
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export function detectLevel(env = process.env, tty = !!process.stdout.isTTY) {
|
|
2
|
+
if (env.NO_COLOR)
|
|
3
|
+
return 0;
|
|
4
|
+
const f = env.FORCE_COLOR;
|
|
5
|
+
if (f !== undefined && f !== "")
|
|
6
|
+
return f === "0" || f === "false" ? 0 : f === "2" ? 2 : f === "3" ? 3 : 1;
|
|
7
|
+
if (!tty)
|
|
8
|
+
return 0;
|
|
9
|
+
if (/truecolor|24bit/i.test(env.COLORTERM ?? "") || /^(iTerm\.app|vscode|WezTerm|Hyper|ghostty)$/i.test(env.TERM_PROGRAM ?? "") || /kitty|alacritty/i.test(env.TERM ?? ""))
|
|
10
|
+
return 3; // Apple Terminal is 256-colour only
|
|
11
|
+
if (/256/.test(env.TERM ?? ""))
|
|
12
|
+
return 2;
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
export let level = detectLevel();
|
|
16
|
+
/** Tests (and `--color` style flags) can override detection. */
|
|
17
|
+
export const setLevel = (l) => { level = l; };
|
|
18
|
+
const to256 = ([r, g, b]) => {
|
|
19
|
+
if (Math.abs(r - g) < 10 && Math.abs(g - b) < 10) {
|
|
20
|
+
const v = Math.round(((r + g + b) / 3 - 8) / 10);
|
|
21
|
+
return v < 0 ? 16 : v > 23 ? 231 : 232 + v;
|
|
22
|
+
} // greys
|
|
23
|
+
const c = (x) => Math.round((x / 255) * 5);
|
|
24
|
+
return 16 + 36 * c(r) + 6 * c(g) + c(b);
|
|
25
|
+
};
|
|
26
|
+
const to16 = ([r, g, b]) => {
|
|
27
|
+
const bright = Math.max(r, g, b) > 200, idx = (r > 127 ? 1 : 0) | (g > 127 ? 2 : 0) | (b > 127 ? 4 : 0);
|
|
28
|
+
return (bright ? 90 : 30) + (idx === 0 && !bright ? 7 : idx);
|
|
29
|
+
};
|
|
30
|
+
/** Foreground colour; resets only the foreground so bold/dim keep working around it. */
|
|
31
|
+
export function fg(c, s) {
|
|
32
|
+
if (!level || !s)
|
|
33
|
+
return s;
|
|
34
|
+
return level === 3 ? `\x1b[38;2;${c[0]};${c[1]};${c[2]}m${s}\x1b[39m` : level === 2 ? `\x1b[38;5;${to256(c)}m${s}\x1b[39m` : `\x1b[${to16(c)}m${s}\x1b[39m`;
|
|
35
|
+
}
|
|
36
|
+
export const bold = (s) => (level ? `\x1b[1m${s}\x1b[22m` : s);
|
|
37
|
+
export const dimmed = (s) => (level ? `\x1b[2m${s}\x1b[22m` : s);
|
|
38
|
+
export const lerp = (a, b, t) => [Math.round(a[0] + (b[0] - a[0]) * t), Math.round(a[1] + (b[1] - a[1]) * t), Math.round(a[2] + (b[2] - a[2]) * t)];
|
|
39
|
+
/** Colour at position t∈[0,1] along a multi-stop gradient. */
|
|
40
|
+
export function at(stops, t) {
|
|
41
|
+
if (stops.length === 1)
|
|
42
|
+
return stops[0];
|
|
43
|
+
const x = Math.max(0, Math.min(1, t)) * (stops.length - 1), i = Math.min(stops.length - 2, Math.floor(x));
|
|
44
|
+
return lerp(stops[i], stops[i + 1], x - i);
|
|
45
|
+
}
|
|
46
|
+
/** Paint each character along a gradient (a single colour at low colour depth, so it stays readable). */
|
|
47
|
+
export function gradient(text, stops, phase = 0) {
|
|
48
|
+
if (level < 2)
|
|
49
|
+
return fg(stops[Math.floor(stops.length / 2)], text);
|
|
50
|
+
const chars = [...text], n = Math.max(1, chars.length - 1);
|
|
51
|
+
return chars.map((ch, i) => (ch === " " ? ch : fg(at(stops, ((i / n) + phase) % 1), ch))).join("");
|
|
52
|
+
}
|
|
53
|
+
export const palette = {
|
|
54
|
+
brand: [[124, 92, 255], [0, 180, 255], [0, 229, 160]], // violet → blue → mint
|
|
55
|
+
claude: [217, 119, 87], openai: [16, 163, 127], local: [154, 165, 177], other: [138, 180, 248],
|
|
56
|
+
read: [91, 157, 255], edit: [255, 180, 84], shell: [199, 146, 234], web: [77, 208, 225],
|
|
57
|
+
memory: [126, 231, 135], plan: [255, 121, 198], explore: [255, 158, 100], generic: [160, 174, 192],
|
|
58
|
+
ok: [86, 211, 100], warn: [255, 196, 0], err: [255, 85, 85], muted: [110, 118, 129],
|
|
59
|
+
gauge: [[63, 220, 140], [255, 200, 60], [255, 85, 85]],
|
|
60
|
+
};
|
|
61
|
+
const CATEGORY = {
|
|
62
|
+
read_file: palette.read, list_files: palette.read, grep: palette.read, memory_search: palette.read,
|
|
63
|
+
write_file: palette.edit, edit_file: palette.edit, sh: palette.shell, web_search: palette.web, fetch_url: palette.web,
|
|
64
|
+
memory_save: palette.memory, memory_forget: palette.memory, todo_write: palette.plan, exit_plan_mode: palette.plan, task: palette.explore,
|
|
65
|
+
};
|
|
66
|
+
export const toolColor = (name) => CATEGORY[name] ?? (name.includes("__") ? palette.web : palette.generic);
|
|
67
|
+
/** Provider/model identity: a glyph and an accent colour. */
|
|
68
|
+
export function brandOf(model, provider = "") {
|
|
69
|
+
if (/claude|fable|mythos/i.test(model) || provider === "anthropic")
|
|
70
|
+
return { icon: "✻", color: palette.claude };
|
|
71
|
+
if (/gpt|o[134]-|openai|chatgpt/i.test(model) || provider === "openai")
|
|
72
|
+
return { icon: "◈", color: palette.openai };
|
|
73
|
+
if (/llama|qwen|mistral|gemma|phi|deepseek|ollama/i.test(model) || provider === "ollama")
|
|
74
|
+
return { icon: "⬢", color: palette.local };
|
|
75
|
+
return { icon: "◆", color: palette.other };
|
|
76
|
+
}
|
|
77
|
+
// ---------------------------------------------------------------- token meters
|
|
78
|
+
/** ▰▰▰▱▱▱▱▱ context gauge. Filled cells shift green → amber → red the fuller the window gets. */
|
|
79
|
+
export function gauge(pct, width = 10) {
|
|
80
|
+
const p = Math.max(0, Math.min(100, pct)), filled = Math.round((p / 100) * width);
|
|
81
|
+
let s = "";
|
|
82
|
+
for (let i = 0; i < width; i++) {
|
|
83
|
+
if (i < filled)
|
|
84
|
+
s += level >= 2 ? fg(at(palette.gauge, (i + 0.5) / width), "▰") : fg(p > 85 ? palette.err : p > 60 ? palette.warn : palette.ok, "▰"); // a cell's colour is its place on the bar, so a low gauge stays green
|
|
85
|
+
else
|
|
86
|
+
s += fg(palette.muted, "▱");
|
|
87
|
+
}
|
|
88
|
+
return s;
|
|
89
|
+
}
|
|
90
|
+
export const gaugeColor = (pct) => (pct > 85 ? palette.err : pct > 60 ? palette.warn : palette.ok);
|
|
91
|
+
const BARS = "▁▂▃▄▅▆▇█";
|
|
92
|
+
/** Tiny bar chart of the last `width` values. */
|
|
93
|
+
export function sparkline(values, width = 12, color = palette.brand[1]) {
|
|
94
|
+
const v = values.slice(-width), max = Math.max(1, ...v);
|
|
95
|
+
return v.map((x) => fg(level >= 2 ? at(palette.brand, x / max) : color, BARS[Math.min(7, Math.round((x / max) * 7))])).join("");
|
|
96
|
+
}
|
|
97
|
+
/** A moving highlight sweeping across `text` (the "thinking" shimmer). Plain bold when colours are limited. */
|
|
98
|
+
export function shimmer(text, frame, base = palette.brand[0], hi = [255, 255, 255]) {
|
|
99
|
+
if (level < 2)
|
|
100
|
+
return bold(text);
|
|
101
|
+
const chars = [...text], pos = (frame * 0.9) % (chars.length + 8) - 4;
|
|
102
|
+
return chars.map((ch, i) => fg(lerp(base, hi, Math.max(0, 1 - Math.abs(i - pos) / 3)), ch)).join("");
|
|
103
|
+
}
|
|
104
|
+
/** Stacked horizontal bar from weighted segments (for the context map). */
|
|
105
|
+
export function stackedBar(parts, width = 40) {
|
|
106
|
+
const total = Math.max(1, parts.reduce((n, p) => n + p.value, 0));
|
|
107
|
+
const cells = parts.map((p) => Math.round((p.value / total) * width));
|
|
108
|
+
let diff = width - cells.reduce((a, b) => a + b, 0);
|
|
109
|
+
for (let i = 0; diff !== 0 && i < cells.length * 2; i++) {
|
|
110
|
+
const k = i % cells.length;
|
|
111
|
+
if (diff > 0) {
|
|
112
|
+
cells[k]++;
|
|
113
|
+
diff--;
|
|
114
|
+
}
|
|
115
|
+
else if (cells[k] > 0) {
|
|
116
|
+
cells[k]--;
|
|
117
|
+
diff++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
bar: parts.map((p, i) => fg(p.color, "█".repeat(cells[i]))).join(""),
|
|
122
|
+
legend: parts.filter((p) => p.value > 0).map((p) => `${fg(p.color, "■")} ${p.label} ${Math.round((p.value / total) * 100)}%`),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------- logo
|
|
126
|
+
/** Pixel mascot + half-block wordmark, painted along the brand gradient. */
|
|
127
|
+
export function logo(version) {
|
|
128
|
+
const eyes = fg(palette.brand[2], "◖ ◗");
|
|
129
|
+
const wm1 = "▄▀█ █▀▄▀█ █▄ █ █▀▀ █▀ █▀", wm2 = "█▀█ █ ▀ █ █ ▀█ ██▄ ▄█ ▄█";
|
|
130
|
+
const g = (s, p = 0) => gradient(s, palette.brand, p);
|
|
131
|
+
return [
|
|
132
|
+
`${g(" ▗▄▄▄▄▄▖", 0)} ${g(wm1, 0)}`,
|
|
133
|
+
`${g(" █", 0.05)} ${eyes} ${g("█", 0.2)} ${g(wm2, 0)}`,
|
|
134
|
+
`${g(" ▝▀▀▀▀▀▘", 0.1)} ${fg(palette.muted, `v${version} · reliable agents for any model`)}`,
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
export const TIPS = [
|
|
138
|
+
"Shift+Tab cycles permissions: ask → accept edits → plan",
|
|
139
|
+
"Ctrl+P opens the command palette; Alt+M switches model",
|
|
140
|
+
"Type @ to mention a file; its contents are attached for you",
|
|
141
|
+
"Esc Esc rewinds the conversation and restores your files",
|
|
142
|
+
"Keep typing while it works; Enter steers the agent mid-task",
|
|
143
|
+
"/tokens shows what is eating your context window",
|
|
144
|
+
"/plan makes it read-only until you approve a plan",
|
|
145
|
+
"#note saves a memory; !ls runs a command right now",
|
|
146
|
+
"Drop a markdown file in .amness/commands/ to add your own /command",
|
|
147
|
+
"/verify npm test makes \"done\" mean the tests actually pass",
|
|
148
|
+
];
|