jeo-code 0.1.0 → 0.4.4
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/README.ja.md +160 -0
- package/README.ko.md +160 -0
- package/README.md +115 -297
- package/README.zh.md +160 -0
- package/package.json +11 -6
- package/scripts/install.sh +28 -28
- package/scripts/uninstall.sh +17 -15
- package/src/AGENTS.md +50 -0
- package/src/agent/AGENTS.md +49 -0
- package/src/agent/bash-fixups.ts +103 -0
- package/src/agent/compaction.ts +410 -19
- package/src/agent/config-schema.ts +119 -5
- package/src/agent/context-files.ts +314 -17
- package/src/agent/dev/AGENTS.md +36 -0
- package/src/agent/dev/advanced-analyzer.ts +12 -0
- package/src/agent/dev/evolution-bridge.ts +82 -0
- package/src/agent/dev/evolution-logger.ts +41 -0
- package/src/agent/dev/self-analysis.ts +64 -0
- package/src/agent/dev/self-improve.ts +24 -0
- package/src/agent/dev/spec-automation.ts +49 -0
- package/src/agent/engine.ts +804 -54
- package/src/agent/hooks.ts +273 -0
- package/src/agent/loop.ts +21 -1
- package/src/agent/memory.ts +201 -0
- package/src/agent/model-recency.ts +32 -0
- package/src/agent/output-minimizer.ts +108 -0
- package/src/agent/output-util.ts +64 -0
- package/src/agent/plan.ts +187 -0
- package/src/agent/seed.ts +52 -0
- package/src/agent/session.ts +235 -21
- package/src/agent/state.ts +286 -39
- package/src/agent/step-budget.ts +232 -0
- package/src/agent/subagents.ts +223 -26
- package/src/agent/task-tool.ts +272 -0
- package/src/agent/todo-tool.ts +87 -0
- package/src/agent/tokenizer.ts +117 -0
- package/src/agent/tool-registry.ts +54 -0
- package/src/agent/tools.ts +562 -103
- package/src/agent/web-search.ts +538 -0
- package/src/ai/AGENTS.md +44 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/model-catalog-compat.ts +3 -1
- package/src/ai/model-catalog.ts +74 -9
- package/src/ai/model-discovery.ts +215 -17
- package/src/ai/model-manager.ts +346 -32
- package/src/ai/model-picker.ts +1 -1
- package/src/ai/model-registry.ts +4 -2
- package/src/ai/pricing.ts +84 -0
- package/src/ai/provider-registry.ts +23 -0
- package/src/ai/provider-status.ts +60 -16
- package/src/ai/providers/AGENTS.md +42 -0
- package/src/ai/providers/anthropic.ts +250 -31
- package/src/ai/providers/antigravity.ts +219 -0
- package/src/ai/providers/errors.ts +15 -1
- package/src/ai/providers/gemini.ts +196 -13
- package/src/ai/providers/ollama.ts +37 -7
- package/src/ai/providers/openai-responses.ts +173 -0
- package/src/ai/providers/openai.ts +64 -12
- package/src/ai/sse.ts +4 -1
- package/src/ai/types.ts +18 -1
- package/src/auth/AGENTS.md +41 -0
- package/src/auth/callback-server.ts +6 -1
- package/src/auth/flows/AGENTS.md +32 -0
- package/src/auth/flows/antigravity.ts +151 -0
- package/src/auth/flows/google-project.ts +190 -0
- package/src/auth/flows/google.ts +39 -18
- package/src/auth/flows/index.ts +15 -5
- package/src/auth/flows/openai.ts +2 -2
- package/src/auth/oauth.ts +8 -0
- package/src/auth/refresh.ts +44 -27
- package/src/auth/storage.ts +149 -26
- package/src/auth/types.ts +1 -1
- package/src/autopilot.ts +362 -0
- package/src/bun-imports.d.ts +4 -0
- package/src/cli/AGENTS.md +39 -0
- package/src/cli/runner.ts +148 -14
- package/src/cli.ts +13 -4
- package/src/commands/AGENTS.md +40 -0
- package/src/commands/approve.ts +62 -3
- package/src/commands/auth.ts +167 -25
- package/src/commands/chat.ts +37 -8
- package/src/commands/deep-interview.ts +633 -175
- package/src/commands/doctor.ts +84 -37
- package/src/commands/evolve-core.ts +18 -0
- package/src/commands/evolve.ts +2 -1
- package/src/commands/export.ts +176 -0
- package/src/commands/gjc.ts +52 -0
- package/src/commands/launch.ts +3549 -240
- package/src/commands/mcp.ts +3 -3
- package/src/commands/ooo-seed.ts +19 -0
- package/src/commands/ralplan.ts +253 -35
- package/src/commands/resume.ts +1 -1
- package/src/commands/session.ts +183 -0
- package/src/commands/setup-helpers.ts +10 -3
- package/src/commands/setup.ts +57 -16
- package/src/commands/skills.ts +78 -18
- package/src/commands/state.ts +198 -0
- package/src/commands/status.ts +84 -0
- package/src/commands/team.ts +340 -212
- package/src/commands/ultragoal.ts +122 -61
- package/src/commands/update.ts +244 -0
- package/src/ledger.ts +270 -0
- package/src/mcp/AGENTS.md +38 -0
- package/src/mcp/server.ts +115 -14
- package/src/mcp/tools.ts +42 -22
- package/src/md-modules.d.ts +4 -0
- package/src/prompts/AGENTS.md +41 -0
- package/src/prompts/agents/AGENTS.md +35 -0
- package/src/prompts/agents/architect.md +35 -0
- package/src/prompts/agents/critic.md +37 -0
- package/src/prompts/agents/executor.md +36 -0
- package/src/prompts/agents/planner.md +37 -0
- package/src/prompts/skills/AGENTS.md +36 -0
- package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
- package/src/prompts/skills/deep-dive/SKILL.md +13 -0
- package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
- package/src/prompts/skills/deep-interview/SKILL.md +12 -0
- package/src/prompts/skills/gjc/AGENTS.md +31 -0
- package/src/prompts/skills/gjc/SKILL.md +15 -0
- package/src/prompts/skills/ralplan/AGENTS.md +31 -0
- package/src/prompts/skills/ralplan/SKILL.md +11 -0
- package/src/prompts/skills/team/AGENTS.md +31 -0
- package/src/prompts/skills/team/SKILL.md +11 -0
- package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
- package/src/prompts/skills/ultragoal/SKILL.md +11 -0
- package/src/skills/AGENTS.md +38 -0
- package/src/skills/catalog.ts +565 -31
- package/src/tui/AGENTS.md +43 -0
- package/src/tui/app.ts +1181 -92
- package/src/tui/components/AGENTS.md +42 -0
- package/src/tui/components/ascii-art.ts +257 -15
- package/src/tui/components/autocomplete.ts +98 -16
- package/src/tui/components/autopilot-status.ts +65 -0
- package/src/tui/components/category-index.ts +49 -0
- package/src/tui/components/code-view.ts +54 -11
- package/src/tui/components/color.ts +171 -2
- package/src/tui/components/config-panel.ts +82 -15
- package/src/tui/components/duration.ts +38 -0
- package/src/tui/components/evolution.ts +3 -3
- package/src/tui/components/footer.ts +91 -42
- package/src/tui/components/forge.ts +426 -31
- package/src/tui/components/hints.ts +54 -0
- package/src/tui/components/hud.ts +73 -0
- package/src/tui/components/index.ts +4 -0
- package/src/tui/components/input-box.ts +150 -0
- package/src/tui/components/layout.ts +11 -3
- package/src/tui/components/live-model-picker.ts +108 -0
- package/src/tui/components/markdown-table.ts +140 -0
- package/src/tui/components/markdown-text.ts +97 -0
- package/src/tui/components/meter.ts +4 -1
- package/src/tui/components/model-picker.ts +3 -2
- package/src/tui/components/provider-picker.ts +3 -2
- package/src/tui/components/section.ts +70 -0
- package/src/tui/components/select-list.ts +40 -10
- package/src/tui/components/skill-picker.ts +25 -0
- package/src/tui/components/slash.ts +244 -21
- package/src/tui/components/status.ts +272 -11
- package/src/tui/components/step-timeline.ts +218 -0
- package/src/tui/components/stream.ts +26 -9
- package/src/tui/components/themes.ts +212 -6
- package/src/tui/components/todo-card.ts +47 -0
- package/src/tui/components/tool-list.ts +58 -12
- package/src/tui/components/transcript.ts +120 -0
- package/src/tui/components/update-box.ts +31 -0
- package/src/tui/components/welcome.ts +162 -0
- package/src/tui/components/width.ts +163 -0
- package/src/tui/monitoring/AGENTS.md +31 -0
- package/src/tui/monitoring/hud-view.ts +55 -0
- package/src/tui/renderer.ts +112 -3
- package/src/tui/terminal.ts +40 -33
- package/src/util/AGENTS.md +39 -0
- package/src/util/clipboard-image.ts +118 -0
- package/src/util/env.ts +12 -0
- package/src/util/provider-error.ts +78 -0
- package/src/util/retry.ts +91 -6
- package/src/util/update-check.ts +64 -0
- package/src/commands/models.ts +0 -104
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { BOX_ASCII, BOX_UNICODE, padLineTo, type BoxGlyphs } from "./layout";
|
|
3
|
-
import { stripAnsi, visibleWidth } from "./color";
|
|
3
|
+
import { stripAnsi, visibleWidth, animatedGradientText } from "./color";
|
|
4
|
+
import { truncateToWidth } from "./width";
|
|
5
|
+
import { lightHighlightLine } from "./code-view";
|
|
6
|
+
import { type UiCategory } from "./category-index";
|
|
4
7
|
|
|
5
8
|
export interface ForgeSummary {
|
|
6
9
|
title: string;
|
|
@@ -13,10 +16,45 @@ export interface ForgeBoxOptions {
|
|
|
13
16
|
maxLines?: number;
|
|
14
17
|
unicode?: boolean;
|
|
15
18
|
paint?: (s: string) => string;
|
|
19
|
+
/** Shaded-edge painter (bottom border + right edge). Defaults to a dimmed `paint`
|
|
20
|
+
* when color is on — the lit/shaded two-tone gives the card visible depth. */
|
|
21
|
+
paintShadow?: (s: string) => string;
|
|
22
|
+
index?: number;
|
|
23
|
+
category?: UiCategory;
|
|
24
|
+
color?: boolean;
|
|
25
|
+
/** DNA-flow border animation: a flowing gradient painted over the top/bottom
|
|
26
|
+
* border glyphs (content rows untouched). Stateless — the caller drives
|
|
27
|
+
* `phase` per tick, so nothing is retained between frames. Below TrueColor
|
|
28
|
+
* (`colorLevel < 3`) this degrades to the static `paint`/`paintShadow`. */
|
|
29
|
+
flow?: { palette: readonly string[]; phase: number; colorLevel: number };
|
|
30
|
+
/** Width-1 mark prepended to the border title (e.g. the DNA claw beat glyph). */
|
|
31
|
+
titleMark?: string;
|
|
32
|
+
/** Themed +/- painters for `language: "patch"` cards (edit diffs): applied to
|
|
33
|
+
* the FULL padded row so added/removed lines read as background-tinted
|
|
34
|
+
* stripes — block-level contrast inside the card. */
|
|
35
|
+
diffPaint?: { add: (s: string) => string; del: (s: string) => string };
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
const SECRET_VALUE_RE = /(api[_-]?key|authorization|bearer|password|secret|token)(\s*[:=]\s*)(["']?)[^"'\s,}]+/gi;
|
|
19
39
|
const SECRET_JSON_RE = /("(?:api[_-]?key|authorization|password|secret|token)"\s*:\s*")[^"]+(")/gi;
|
|
40
|
+
const EXT_TO_LANG: Record<string, string> = {
|
|
41
|
+
ts: "typescript",
|
|
42
|
+
js: "javascript",
|
|
43
|
+
tsx: "typescript",
|
|
44
|
+
jsx: "javascript",
|
|
45
|
+
json: "json",
|
|
46
|
+
md: "markdown",
|
|
47
|
+
sh: "bash",
|
|
48
|
+
py: "python",
|
|
49
|
+
yaml: "yaml",
|
|
50
|
+
yml: "yaml",
|
|
51
|
+
toml: "toml",
|
|
52
|
+
css: "css",
|
|
53
|
+
html: "html",
|
|
54
|
+
rs: "rust",
|
|
55
|
+
go: "go",
|
|
56
|
+
};
|
|
57
|
+
|
|
20
58
|
|
|
21
59
|
export function redactSecrets(input: string): string {
|
|
22
60
|
return input
|
|
@@ -24,6 +62,19 @@ export function redactSecrets(input: string): string {
|
|
|
24
62
|
.replace(SECRET_JSON_RE, "$1<redacted>$2");
|
|
25
63
|
}
|
|
26
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Sentinel for a labeled in-box divider (e.g. the gjc-style `Output` rule between a command
|
|
67
|
+
* echo and its output body). It is `#`-prefixed so app-side helpers that scan summary lines for
|
|
68
|
+
* the command (skipping `#` notes) never surface it; formatForgeBox rewrites it into a real
|
|
69
|
+
* bordered divider row at render time, where the unicode/ASCII glyph set is known.
|
|
70
|
+
*/
|
|
71
|
+
const FORGE_DIVIDER_PREFIX = "#\u0000fdiv:";
|
|
72
|
+
|
|
73
|
+
/** Build a labeled-divider sentinel line for inclusion in a ForgeSummary's `lines`. */
|
|
74
|
+
export function forgeDivider(label: string): string {
|
|
75
|
+
return FORGE_DIVIDER_PREFIX + label;
|
|
76
|
+
}
|
|
77
|
+
|
|
27
78
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
28
79
|
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
29
80
|
}
|
|
@@ -53,6 +104,96 @@ function previewLines(text: string, maxLines: number, maxChars: number): string[
|
|
|
53
104
|
return out.length > 0 ? out : [""];
|
|
54
105
|
}
|
|
55
106
|
|
|
107
|
+
/** jeo-ref file-content preview: a line-number gutter (` 1│ #…`) before each
|
|
108
|
+
* previewed row, closed by `… N more lines` when clipped. ANSI-free so cards
|
|
109
|
+
* stay byte-stable across color modes; `│` degrades to `|` without unicode. */
|
|
110
|
+
function numberedPreviewLines(text: string, maxLines: number, maxChars: number, unicode = true): string[] {
|
|
111
|
+
const clean = redactSecrets(text.replace(/\r\n/g, "\n"));
|
|
112
|
+
const raw = clean.split("\n");
|
|
113
|
+
const gutter = unicode ? "│" : "|";
|
|
114
|
+
const width = String(Math.min(raw.length, maxLines)).length;
|
|
115
|
+
const out: string[] = [];
|
|
116
|
+
let used = 0;
|
|
117
|
+
for (let i = 0; i < raw.length; i++) {
|
|
118
|
+
if (out.length >= maxLines || used >= maxChars) break;
|
|
119
|
+
const remaining = Math.max(0, maxChars - used);
|
|
120
|
+
const line = raw[i]!;
|
|
121
|
+
const next = line.length > remaining ? `${line.slice(0, Math.max(0, remaining - 1))}…` : line;
|
|
122
|
+
out.push(`${String(i + 1).padStart(width)}${gutter} ${next}`);
|
|
123
|
+
used += next.length + 1;
|
|
124
|
+
}
|
|
125
|
+
const hidden = raw.length - out.length;
|
|
126
|
+
if (hidden > 0) out.push(`… ${hidden} more line${hidden === 1 ? "" : "s"}`);
|
|
127
|
+
return out.length > 0 ? out : [""];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Render an edit tool's `editBlock` as gjc-style diff lines for the forge card:
|
|
132
|
+
* - SEARCH/REPLACE hunks → `- old` / `+ new` rows per hunk (capped), closed by a
|
|
133
|
+
* `~N hunk(s) · +A −R line(s)` summary divider row.
|
|
134
|
+
* - `≔` line-directives → the payload as `+ added` rows with a `+A line(s)` summary.
|
|
135
|
+
* Returns null for formats it does not recognize (caller falls back to a raw preview).
|
|
136
|
+
* Mirrors agent/tools.ts' parse rules WITHOUT importing the agent layer (TUI stays
|
|
137
|
+
* dependency-light); plain +/- prefixes keep the card ANSI-free in every color mode.
|
|
138
|
+
*/
|
|
139
|
+
export function editBlockDiffLines(editBlock: string, maxRows = 10): string[] | null {
|
|
140
|
+
const block = editBlock.replace(/\r\n/g, "\n");
|
|
141
|
+
const trimFrame = (s: string): string => {
|
|
142
|
+
let t = s;
|
|
143
|
+
if (t.startsWith("\n")) t = t.slice(1);
|
|
144
|
+
if (t.endsWith("\n")) t = t.slice(0, -1);
|
|
145
|
+
return t;
|
|
146
|
+
};
|
|
147
|
+
const rows: string[] = [];
|
|
148
|
+
let added = 0;
|
|
149
|
+
let removed = 0;
|
|
150
|
+
const push = (prefix: "+" | "-", text: string): void => {
|
|
151
|
+
if (prefix === "+") added++;
|
|
152
|
+
else removed++;
|
|
153
|
+
if (rows.length < maxRows) {
|
|
154
|
+
const redacted = redactSecrets(text);
|
|
155
|
+
const colored = prefix === "+" ? chalk.green(`+ ${redacted}`) : chalk.red(`- ${redacted}`);
|
|
156
|
+
rows.push(colored);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (block.includes("<<<<<<< SEARCH")) {
|
|
161
|
+
const segs = block.split("<<<<<<< SEARCH").slice(1);
|
|
162
|
+
let hunks = 0;
|
|
163
|
+
for (const seg of segs) {
|
|
164
|
+
const eq = seg.indexOf("=======");
|
|
165
|
+
if (eq === -1) return null; // malformed — let the raw preview show it
|
|
166
|
+
const gt = seg.indexOf(">>>>>>>", eq);
|
|
167
|
+
if (gt === -1) return null;
|
|
168
|
+
hunks++;
|
|
169
|
+
const search = trimFrame(seg.slice(0, eq));
|
|
170
|
+
const replace = trimFrame(seg.slice(eq + 7, gt));
|
|
171
|
+
if (search) for (const l of search.split("\n")) push("-", l);
|
|
172
|
+
if (replace) for (const l of replace.split("\n")) push("+", l);
|
|
173
|
+
}
|
|
174
|
+
if (hunks === 0) return null;
|
|
175
|
+
const hidden = added + removed - rows.length;
|
|
176
|
+
if (hidden > 0) rows.push(`… ${hidden} more change line(s)`);
|
|
177
|
+
rows.push(FORGE_DIVIDER_PREFIX + "Summary");
|
|
178
|
+
rows.push(`~${hunks} hunk(s) · +${added} −${removed} line(s)`);
|
|
179
|
+
return rows;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (block.startsWith("≔")) {
|
|
183
|
+
const directive = block.split("\n", 1)[0] ?? "≔";
|
|
184
|
+
const payload = block.includes("\n") ? block.slice(block.indexOf("\n") + 1) : "";
|
|
185
|
+
rows.push(redactSecrets(directive));
|
|
186
|
+
if (payload) for (const l of payload.split("\n")) push("+", l);
|
|
187
|
+
const hidden = added - Math.max(0, rows.length - 1);
|
|
188
|
+
if (hidden > 0) rows.push(`… ${hidden} more line(s)`);
|
|
189
|
+
rows.push(FORGE_DIVIDER_PREFIX + "Summary");
|
|
190
|
+
rows.push(`${directive.slice(0, 24)} · +${added} line(s)`);
|
|
191
|
+
return rows;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
56
197
|
function jsonPreview(args: Record<string, unknown>): string[] {
|
|
57
198
|
try {
|
|
58
199
|
return previewLines(JSON.stringify(args, null, 2), 6, 500);
|
|
@@ -61,24 +202,40 @@ function jsonPreview(args: Record<string, unknown>): string[] {
|
|
|
61
202
|
}
|
|
62
203
|
}
|
|
63
204
|
|
|
64
|
-
export function summarizeForgeInvocation(tool: string, rawArgs: unknown): ForgeSummary {
|
|
205
|
+
export function summarizeForgeInvocation(tool: string, rawArgs: unknown, opts: { unicode?: boolean } = {}): ForgeSummary {
|
|
65
206
|
const args = asRecord(rawArgs);
|
|
66
|
-
const
|
|
207
|
+
const safeTool = tool || "(no tool)";
|
|
208
|
+
const normalized = safeTool.toLowerCase();
|
|
67
209
|
if (normalized === "bash") {
|
|
68
210
|
const command = stringArg(args, "command", "cmd") ?? "";
|
|
69
211
|
const timeout = stringArg(args, "timeoutMs", "timeout");
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
212
|
+
// gjc-style command echo: prefix the (redacted, capped) command with `$ `.
|
|
213
|
+
const commandLines = previewLines(command, 8, 800);
|
|
214
|
+
const lines = [`$ ${commandLines[0] ?? ""}`, ...commandLines.slice(1)];
|
|
215
|
+
const cwdKey = Object.keys(args).find(k => /^(cwd|workingdir|workingdirectory|subdir|dir)$/i.test(k));
|
|
216
|
+
if (cwdKey !== undefined) {
|
|
217
|
+
const cwdVal = args[cwdKey];
|
|
218
|
+
if (cwdVal !== undefined && cwdVal !== null && cwdVal !== "") {
|
|
219
|
+
lines.push(`# cwd-relative: ${cwdVal}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (timeout) {
|
|
223
|
+
const ms = Number(timeout);
|
|
224
|
+
const secs = Number.isFinite(ms) ? (ms % 1000 === 0 ? String(ms / 1000) : (ms / 1000).toFixed(1)) : timeout;
|
|
225
|
+
const open = opts.unicode === false ? "[" : "⟦";
|
|
226
|
+
const close = opts.unicode === false ? "]" : "⟧";
|
|
227
|
+
lines.push(`${open}Timeout: ${secs}s${close}`);
|
|
228
|
+
}
|
|
229
|
+
return { title: "Bash", language: "bash", lines };
|
|
73
230
|
}
|
|
74
231
|
|
|
75
232
|
if (normalized === "read") {
|
|
76
233
|
const filePath = stringArg(args, "filePath", "path") ?? "<missing path>";
|
|
77
234
|
const range = stringArg(args, "lineRange", "range");
|
|
78
235
|
return {
|
|
79
|
-
title: `
|
|
236
|
+
title: `Read ${filePath}${range ? `:${range}` : ""}`,
|
|
80
237
|
language: "path",
|
|
81
|
-
lines: [`path: ${filePath}
|
|
238
|
+
lines: [`path: ${filePath}`],
|
|
82
239
|
};
|
|
83
240
|
}
|
|
84
241
|
|
|
@@ -86,43 +243,180 @@ export function summarizeForgeInvocation(tool: string, rawArgs: unknown): ForgeS
|
|
|
86
243
|
const filePath = stringArg(args, "filePath", "path") ?? "<missing path>";
|
|
87
244
|
const content = typeof args.content === "string" ? args.content : "";
|
|
88
245
|
const lineCount = content.length === 0 ? 0 : content.split("\n").length;
|
|
246
|
+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
|
247
|
+
const lang = EXT_TO_LANG[ext];
|
|
248
|
+
const langTag = lang ? ` · ${lang}` : "";
|
|
89
249
|
return {
|
|
90
|
-
title: `
|
|
91
|
-
language: "text",
|
|
92
|
-
lines: [
|
|
250
|
+
title: `Write ${filePath}`,
|
|
251
|
+
language: lang || "text",
|
|
252
|
+
lines: [
|
|
253
|
+
...numberedPreviewLines(content, 6, 700, opts.unicode !== false),
|
|
254
|
+
FORGE_DIVIDER_PREFIX + "Summary",
|
|
255
|
+
`wrote ${lineCount} lines, ${content.length} bytes${langTag}`
|
|
256
|
+
],
|
|
93
257
|
};
|
|
94
258
|
}
|
|
95
|
-
|
|
96
259
|
if (normalized === "edit") {
|
|
97
260
|
const filePath = stringArg(args, "filePath", "path") ?? "<missing path>";
|
|
98
261
|
const editBlock = stringArg(args, "editBlock", "edit") ?? "";
|
|
262
|
+
// gjc-style diff view: SEARCH/REPLACE hunks render as -old/+new lines with a
|
|
263
|
+
// hunk/line summary; ≔ directives render their payload as +added lines. Raw
|
|
264
|
+
// fallback keeps unknown formats visible. Plain +/- prefixes (no ANSI) keep
|
|
265
|
+
// the card byte-stable across color modes.
|
|
266
|
+
const diff = editBlockDiffLines(editBlock);
|
|
99
267
|
return {
|
|
100
|
-
title: `
|
|
268
|
+
title: `Edit : ${filePath}`,
|
|
101
269
|
language: "patch",
|
|
102
|
-
lines:
|
|
270
|
+
lines: diff ?? [...previewLines(editBlock, 8, 800)],
|
|
103
271
|
};
|
|
104
272
|
}
|
|
105
|
-
|
|
106
273
|
if (normalized === "find") {
|
|
107
274
|
const pattern = stringArg(args, "globPattern", "pattern") ?? "<missing glob>";
|
|
108
|
-
return { title:
|
|
275
|
+
return { title: `Find: ${pattern}`, language: "glob", lines: [`glob: ${pattern}`] };
|
|
109
276
|
}
|
|
110
277
|
|
|
111
278
|
if (normalized === "search") {
|
|
112
279
|
const pattern = stringArg(args, "pattern") ?? "<missing pattern>";
|
|
113
280
|
const glob = stringArg(args, "globPattern", "path") ?? "*";
|
|
114
|
-
return { title:
|
|
281
|
+
return { title: `Search: ${pattern}`, language: "regex", lines: [`glob: ${glob}`] };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (normalized === "task") {
|
|
285
|
+
const role = stringArg(args, "role") ?? "executor";
|
|
286
|
+
const task = stringArg(args, "task", "prompt", "assignment") ?? "<missing task>";
|
|
287
|
+
const context = stringArg(args, "context");
|
|
288
|
+
return {
|
|
289
|
+
title: `Task: ${role}`,
|
|
290
|
+
language: "text",
|
|
291
|
+
lines: [...previewLines(task, 4, 500), ...(context ? ["context:", ...previewLines(context, 3, 300)] : [])],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (normalized === "web_search") {
|
|
296
|
+
const query = stringArg(args, "query") ?? "<missing query>";
|
|
297
|
+
const recency = stringArg(args, "recency");
|
|
298
|
+
const lines = [`query: ${query}`];
|
|
299
|
+
if (recency) lines.push(`recency: ${recency}`);
|
|
300
|
+
return {
|
|
301
|
+
title: `Web Search: ${query.length > 60 ? `${query.slice(0, 59)}…` : query}`,
|
|
302
|
+
language: "text",
|
|
303
|
+
lines,
|
|
304
|
+
};
|
|
115
305
|
}
|
|
116
306
|
|
|
117
|
-
return { title: `${
|
|
307
|
+
return { title: `${safeTool} arguments`, language: "json", lines: jsonPreview(args) };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Parsed gjc-style Web Search card pieces, reconstructed from the structured
|
|
311
|
+
* `web_search` tool output (see `formatWebSearchOutput` in agent/web-search.ts). */
|
|
312
|
+
export interface WebSearchCard {
|
|
313
|
+
/** `Provider · N sources` header meta for the card title (e.g. `Anthropic · 18 sources`). */
|
|
314
|
+
titleMeta: string;
|
|
315
|
+
lines: string[];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Build the gjc-style Web Search card body from the tool's structured output:
|
|
320
|
+
* `Query:` line, then `Answer` / `Sources` / `Metadata` divider sections with
|
|
321
|
+
* tree-glyph (`├─`/`└─`) rows and `… N more` truncation — visually mirroring
|
|
322
|
+
* gjc's web_search renderer. Returns null when the output does not carry the
|
|
323
|
+
* structured shape (errors fall back to the generic result card).
|
|
324
|
+
*/
|
|
325
|
+
export function webSearchCardLines(output: string, opts: { unicode?: boolean } = {}): WebSearchCard | null {
|
|
326
|
+
if (!output.startsWith("Query: ")) return null;
|
|
327
|
+
const uni = opts.unicode !== false;
|
|
328
|
+
const branch = uni ? "├─" : "|-";
|
|
329
|
+
const last = uni ? "└─" : "`-";
|
|
330
|
+
const cont = uni ? "│ " : "| ";
|
|
331
|
+
const ellipsis = uni ? "…" : "...";
|
|
332
|
+
|
|
333
|
+
// Section split on "## " heads; everything before the first head is the Query line(s).
|
|
334
|
+
const sections = new Map<string, string[]>();
|
|
335
|
+
let current = "__head__";
|
|
336
|
+
sections.set(current, []);
|
|
337
|
+
for (const line of output.split("\n")) {
|
|
338
|
+
const m = /^## (\w+)/.exec(line);
|
|
339
|
+
if (m) {
|
|
340
|
+
current = m[1]!;
|
|
341
|
+
sections.set(current, []);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
sections.get(current)!.push(line);
|
|
345
|
+
}
|
|
346
|
+
const answer = (sections.get("Answer") ?? []).filter(l => l.trim());
|
|
347
|
+
const metadata = (sections.get("Metadata") ?? []).filter(l => l.trim());
|
|
348
|
+
if (answer.length === 0 && metadata.length === 0) return null;
|
|
349
|
+
|
|
350
|
+
const lines: string[] = [];
|
|
351
|
+
const queryLine = (sections.get("__head__") ?? []).find(l => l.startsWith("Query: "));
|
|
352
|
+
if (queryLine) lines.push(queryLine);
|
|
353
|
+
|
|
354
|
+
// Answer: tree-glyph preview, capped like gjc's collapsed card.
|
|
355
|
+
const MAX_ANSWER = 3;
|
|
356
|
+
lines.push(forgeDivider("Answer"));
|
|
357
|
+
const answerShown = answer.slice(0, MAX_ANSWER);
|
|
358
|
+
answerShown.forEach((l, i) => {
|
|
359
|
+
const glyph = i === answerShown.length - 1 && answer.length <= MAX_ANSWER ? last : branch;
|
|
360
|
+
lines.push(`${glyph} ${l}`);
|
|
361
|
+
});
|
|
362
|
+
if (answer.length > MAX_ANSWER) lines.push(`${ellipsis} ${answer.length - MAX_ANSWER} more lines`);
|
|
363
|
+
|
|
364
|
+
// Sources: `[n] title (domain) · age` + indented url, capped.
|
|
365
|
+
const sourceRaw = (sections.get("Sources") ?? []).filter(l => l.trim());
|
|
366
|
+
const entries: { title: string; url?: string }[] = [];
|
|
367
|
+
for (const line of sourceRaw) {
|
|
368
|
+
const head = /^\[\d+\] (.*)$/.exec(line);
|
|
369
|
+
if (head) entries.push({ title: head[1]! });
|
|
370
|
+
else if (entries.length > 0 && line.startsWith(" ") && !entries[entries.length - 1]!.url) {
|
|
371
|
+
entries[entries.length - 1]!.url = line.trim();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const MAX_SOURCES = 6;
|
|
375
|
+
lines.push(forgeDivider("Sources"));
|
|
376
|
+
if (entries.length === 0) lines.push(`${last} No sources returned`);
|
|
377
|
+
const shown = entries.slice(0, MAX_SOURCES);
|
|
378
|
+
shown.forEach((src, i) => {
|
|
379
|
+
const isLast = i === shown.length - 1 && entries.length <= MAX_SOURCES;
|
|
380
|
+
lines.push(`${isLast && !src.url ? last : branch} ${src.title}`);
|
|
381
|
+
if (src.url) lines.push(`${isLast ? last : cont} ${src.url}`);
|
|
382
|
+
});
|
|
383
|
+
if (entries.length > MAX_SOURCES) lines.push(`${last} ${ellipsis} ${entries.length - MAX_SOURCES} more sources`);
|
|
384
|
+
|
|
385
|
+
// Metadata: verbatim key/value lines.
|
|
386
|
+
lines.push(forgeDivider("Metadata"));
|
|
387
|
+
lines.push(...metadata);
|
|
388
|
+
|
|
389
|
+
const sourceCount = entries.length;
|
|
390
|
+
const provider = metadata.find(l => l.startsWith("Provider: "))?.slice("Provider: ".length) ?? "web";
|
|
391
|
+
return { titleMeta: `${provider} · ${sourceCount} source${sourceCount === 1 ? "" : "s"}`, lines };
|
|
118
392
|
}
|
|
119
393
|
|
|
120
394
|
export function summarizeForgeResult(tool: string, success: boolean, output: string): ForgeSummary {
|
|
121
395
|
const status = success ? "ok" : "failed";
|
|
396
|
+
const safeTool = tool || "(no tool)";
|
|
397
|
+
const normalized = safeTool.toLowerCase();
|
|
398
|
+
let body = output || "<no output>";
|
|
399
|
+
let exitNote: string | null = null;
|
|
400
|
+
if (normalized === "bash") {
|
|
401
|
+
// gjc-style: the engine prefixes failed bash output with `Exit code N` — surface it
|
|
402
|
+
// as a trailing `Command exited with code N` line below the output body instead.
|
|
403
|
+
const m = body.match(/^Exit code (-?\d+)\n?/);
|
|
404
|
+
if (m) {
|
|
405
|
+
exitNote = `Command exited with code ${m[1]}`;
|
|
406
|
+
body = body.slice(m[0].length) || "<no output>";
|
|
407
|
+
} else if (!success) {
|
|
408
|
+
exitNote = "Command failed";
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const lines = previewLines(body, success ? 5 : 10, success ? 600 : 1200);
|
|
412
|
+
if (normalized === "bash") {
|
|
413
|
+
lines.unshift(forgeDivider("Output"));
|
|
414
|
+
if (exitNote) lines.push("", exitNote);
|
|
415
|
+
}
|
|
122
416
|
return {
|
|
123
|
-
title: `${
|
|
417
|
+
title: `${safeTool} result ${status}`,
|
|
124
418
|
language: "output",
|
|
125
|
-
lines
|
|
419
|
+
lines,
|
|
126
420
|
};
|
|
127
421
|
}
|
|
128
422
|
|
|
@@ -139,28 +433,129 @@ function borderGlyphs(unicode: boolean | undefined): BoxGlyphs {
|
|
|
139
433
|
return unicode === false ? BOX_ASCII : BOX_UNICODE;
|
|
140
434
|
}
|
|
141
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Pick as many WHOLE forge boxes as fit `budget` rows. `lines` is the flat render of one or
|
|
438
|
+
* more bordered boxes separated by a single blank line. Boxes are bordered, so a partial box
|
|
439
|
+
* looks broken — this includes only complete boxes, preferring the MOST RECENT (last) ones,
|
|
440
|
+
* and preserves display order. Returns [] when not even one box fits.
|
|
441
|
+
*/
|
|
442
|
+
export function fitForgeBoxes(lines: string[], budget: number): string[] {
|
|
443
|
+
if (budget <= 0 || lines.length === 0) return [];
|
|
444
|
+
if (lines.length <= budget) return lines;
|
|
445
|
+
const groups: string[][] = [];
|
|
446
|
+
let cur: string[] = [];
|
|
447
|
+
for (const line of lines) {
|
|
448
|
+
if (line === "") {
|
|
449
|
+
if (cur.length) { groups.push(cur); cur = []; }
|
|
450
|
+
} else {
|
|
451
|
+
cur.push(line);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (cur.length) groups.push(cur);
|
|
455
|
+
const kept: string[][] = [];
|
|
456
|
+
let used = 0;
|
|
457
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
458
|
+
const cost = groups[i]!.length + (kept.length ? 1 : 0); // +1 blank separator between boxes
|
|
459
|
+
if (used + cost > budget) break;
|
|
460
|
+
used += cost;
|
|
461
|
+
kept.unshift(groups[i]!);
|
|
462
|
+
}
|
|
463
|
+
const out: string[] = [];
|
|
464
|
+
for (let i = 0; i < kept.length; i++) {
|
|
465
|
+
if (i > 0) out.push("");
|
|
466
|
+
out.push(...kept[i]!);
|
|
467
|
+
}
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
|
|
142
471
|
export function formatForgeBox(summary: ForgeSummary, opts: ForgeBoxOptions = {}): string[] {
|
|
143
|
-
const
|
|
472
|
+
const innerWidth = opts.width ?? 80;
|
|
473
|
+
const floor = Math.min(24, innerWidth);
|
|
474
|
+
const width = Math.max(floor, Math.min(120, Math.trunc(innerWidth)));
|
|
144
475
|
const maxLines = Math.max(1, Math.trunc(opts.maxLines ?? 10));
|
|
145
476
|
const glyphs = borderGlyphs(opts.unicode);
|
|
146
477
|
const paint = opts.paint ?? chalk.gray;
|
|
478
|
+
const shadow = opts.paintShadow ?? (opts.color === false ? paint : (s: string) => chalk.dim(paint(s)));
|
|
479
|
+
// DNA-flow border painters: a flowing gradient over the border glyph runs, the
|
|
480
|
+
// bottom offset half a cycle so the helix appears to travel around the card.
|
|
481
|
+
// Pure functions of (text, phase) — no per-frame state is retained — and below
|
|
482
|
+
// TrueColor animatedGradientText returns the text unchanged, so the static
|
|
483
|
+
// paint/shadow path takes over byte-identically.
|
|
484
|
+
const flowOn = !!opts.flow && opts.color !== false;
|
|
485
|
+
const flowTop = (s: string): string => {
|
|
486
|
+
if (!flowOn) return paint(s);
|
|
487
|
+
const g = animatedGradientText(s, opts.flow!.palette, opts.flow!.phase, { colorLevel: opts.flow!.colorLevel });
|
|
488
|
+
return g === s ? paint(s) : g;
|
|
489
|
+
};
|
|
490
|
+
const flowBottom = (s: string): string => {
|
|
491
|
+
if (!flowOn) return shadow(s);
|
|
492
|
+
const g = animatedGradientText(s, opts.flow!.palette, opts.flow!.phase + 0.5, { colorLevel: opts.flow!.colorLevel });
|
|
493
|
+
return g === s ? shadow(s) : g;
|
|
494
|
+
};
|
|
147
495
|
const inner = Math.max(1, width - 2);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
496
|
+
// jeo-ref layout: the title rides ON the top border (`╭── ✗ Bash ──────╮`)
|
|
497
|
+
// instead of occupying a title row + separator — two chrome rows become one,
|
|
498
|
+
// and the card scans like the reference's labeled panel.
|
|
499
|
+
const mark = opts.titleMark ? `${opts.titleMark} ` : "";
|
|
500
|
+
const label = truncateToWidth(summary.title, Math.max(1, inner - 4 - visibleWidth(mark)));
|
|
501
|
+
const titleText = ` ${mark}${opts.color === false ? label : chalk.bold(label)} `;
|
|
502
|
+
const lead = glyphs.h.repeat(Math.min(2, inner));
|
|
503
|
+
const tail = Math.max(0, inner - visibleWidth(lead) - visibleWidth(titleText));
|
|
504
|
+
const top = flowTop(glyphs.tl + lead) + titleText + flowTop(glyphs.h.repeat(tail) + glyphs.tr);
|
|
505
|
+
const bottom = flowBottom(glyphs.bl + glyphs.h.repeat(inner) + glyphs.br);
|
|
506
|
+
const rendered: string[] = [top];
|
|
155
507
|
|
|
508
|
+
// jeo-ref readability: a one-column gutter between the left border and the
|
|
509
|
+
// content (`│ $ cmd …`), so text never touches the frame. Content word-wraps
|
|
510
|
+
// to the guttered width; a labeled divider still counts as one content row.
|
|
511
|
+
const gutterWidth = Math.max(1, inner - 1);
|
|
156
512
|
const content: string[] = [];
|
|
157
513
|
for (const line of summary.lines) {
|
|
158
|
-
|
|
514
|
+
if (line.startsWith(FORGE_DIVIDER_PREFIX)) { content.push(line); continue; }
|
|
515
|
+
for (const wrapped of wrapPlainLine(line, gutterWidth)) content.push(wrapped);
|
|
159
516
|
}
|
|
517
|
+
const renderDivider = (rawLabel: string): string => {
|
|
518
|
+
const text = rawLabel ? ` ${rawLabel} ` : "";
|
|
519
|
+
const rest = Math.max(0, inner - visibleWidth(lead) - visibleWidth(text));
|
|
520
|
+
const bar = `${lead}${text}${glyphs.h.repeat(rest)}`;
|
|
521
|
+
return paint(glyphs.v) + paint(padLineTo(bar, inner, "left")) + shadow(glyphs.v);
|
|
522
|
+
};
|
|
523
|
+
// Patch cards: +/- rows get the themed diff stripe painted over the FULL
|
|
524
|
+
// padded row (background tint spans the card width), so added/removed lines
|
|
525
|
+
// separate as blocks instead of relying on a colored sign alone.
|
|
526
|
+
const diffRows = summary.language === "patch" && opts.color !== false ? opts.diffPaint : undefined;
|
|
527
|
+
// Code cards (jeo-ref): conservative single-pass light highlight — comments,
|
|
528
|
+
// string literals, keywords — applied AFTER wrapping so padding math stays
|
|
529
|
+
// visible-width true. color:false stays byte-identical.
|
|
530
|
+
const CODE_LANGS = new Set(["bash", "typescript", "javascript", "python", "json", "yaml", "rust", "go"]);
|
|
531
|
+
const highlightLang = opts.color !== false && summary.language && CODE_LANGS.has(summary.language) ? summary.language : undefined;
|
|
532
|
+
const contentRow = (line: string): string => {
|
|
533
|
+
const lit = highlightLang ? lightHighlightLine(line, highlightLang) : line;
|
|
534
|
+
const padded = padLineTo(` ${lit}`, inner, "left");
|
|
535
|
+
const body = diffRows && line.startsWith("+")
|
|
536
|
+
? diffRows.add(padded)
|
|
537
|
+
: diffRows && line.startsWith("-")
|
|
538
|
+
? diffRows.del(padded)
|
|
539
|
+
: padded;
|
|
540
|
+
return paint(glyphs.v) + body + shadow(glyphs.v);
|
|
541
|
+
};
|
|
542
|
+
// gjc-style clip hint: the hidden remainder is reachable via Ctrl+O. Two clip
|
|
543
|
+
// layers can produce an overflow marker — previewLines() at the SUMMARIZE
|
|
544
|
+
// stage ("… N more line(s)") and the box's own maxLines cut below; both must
|
|
545
|
+
// carry the hint, so summarize-stage markers are rewritten here where the
|
|
546
|
+
// unicode capability is known.
|
|
547
|
+
const hint = opts.unicode === false ? "[Ctrl+O for more]" : "⟦Ctrl+O for more⟧";
|
|
548
|
+
const CLIP_MARKER_RE = /^… \d+ more line\(s\)$/;
|
|
160
549
|
const clipped = content.slice(0, maxLines);
|
|
161
|
-
for (const line of clipped)
|
|
550
|
+
for (const line of clipped) {
|
|
551
|
+
if (line.startsWith(FORGE_DIVIDER_PREFIX)) {
|
|
552
|
+
rendered.push(renderDivider(line.slice(FORGE_DIVIDER_PREFIX.length)));
|
|
553
|
+
} else {
|
|
554
|
+
rendered.push(contentRow(CLIP_MARKER_RE.test(line) ? `${line} ${hint}` : line));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
162
557
|
if (content.length > clipped.length) {
|
|
163
|
-
rendered.push(
|
|
558
|
+
rendered.push(contentRow(`… ${content.length - clipped.length} more lines ${hint}`));
|
|
164
559
|
}
|
|
165
560
|
rendered.push(bottom);
|
|
166
561
|
return rendered;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key-hint bar — a compact, colored row of keybinding/command hints for the TUI
|
|
3
|
+
* (e.g. "^C cancel · Tab complete · /help · /model · /exit"). Keys are
|
|
4
|
+
* highlighted, labels dimmed, the row is clamped to the terminal width, and an
|
|
5
|
+
* ASCII fallback drops fancy separators. Pure functions over a hint list (color
|
|
6
|
+
* via chalk), so they unit-test with an ANSI-stripping helper.
|
|
7
|
+
*/
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { truncate } from "../terminal";
|
|
10
|
+
|
|
11
|
+
export interface KeyHint {
|
|
12
|
+
/** The key/command token, e.g. "^C", "Tab", "/help". */
|
|
13
|
+
key: string;
|
|
14
|
+
/** What it does, e.g. "cancel", "complete". */
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The default interactive hint set. */
|
|
19
|
+
export const DEFAULT_HINTS: readonly KeyHint[] = [
|
|
20
|
+
{ key: "^C", label: "cancel" },
|
|
21
|
+
{ key: "Tab", label: "complete" },
|
|
22
|
+
{ key: "/help", label: "commands" },
|
|
23
|
+
{ key: "/model", label: "switch" },
|
|
24
|
+
{ key: "^O", label: "history" },
|
|
25
|
+
{ key: "/exit", label: "quit" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export interface HintBarOptions {
|
|
29
|
+
unicode?: boolean;
|
|
30
|
+
color?: boolean;
|
|
31
|
+
/** Clamp the rendered row to this many visible columns. */
|
|
32
|
+
cols?: number;
|
|
33
|
+
/** Indent prefix (default two spaces). */
|
|
34
|
+
indent?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Render one hint as "<key> <label>" with the key highlighted. */
|
|
38
|
+
export function formatHint(hint: KeyHint, color = true): string {
|
|
39
|
+
const key = color ? chalk.cyan.bold(hint.key) : hint.key;
|
|
40
|
+
const label = color ? chalk.gray(hint.label) : hint.label;
|
|
41
|
+
return `${key} ${label}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Render the hint bar; clamps to `cols` and falls back to ASCII separators. */
|
|
45
|
+
export function formatHintBar(hints: readonly KeyHint[] = DEFAULT_HINTS, opts: HintBarOptions = {}): string {
|
|
46
|
+
if (hints.length === 0) return "";
|
|
47
|
+
const unicode = opts.unicode !== false;
|
|
48
|
+
const color = opts.color !== false;
|
|
49
|
+
const indent = opts.indent ?? " ";
|
|
50
|
+
const sepRaw = unicode ? " · " : " | ";
|
|
51
|
+
const sep = color ? chalk.gray(sepRaw) : sepRaw;
|
|
52
|
+
const row = indent + hints.map(h => formatHint(h, color)).join(sep);
|
|
53
|
+
return opts.cols ? truncate(row, opts.cols) : row;
|
|
54
|
+
}
|