@agjs/tsforge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tsforge.js +2 -0
- package/package.json +35 -0
- package/src/agent/agent.constants.ts +382 -0
- package/src/agent/agent.types.ts +34 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/model-agent.ts +297 -0
- package/src/agent/tool-repair.ts +194 -0
- package/src/agent/tools.ts +190 -0
- package/src/browser/checks.ts +96 -0
- package/src/browser/index.ts +8 -0
- package/src/browser/oracle.ts +303 -0
- package/src/classify.ts +48 -0
- package/src/cli.ts +1333 -0
- package/src/config/config.constants.ts +9 -0
- package/src/config/flags.ts +32 -0
- package/src/config/index.ts +8 -0
- package/src/config/tsforge-config.ts +301 -0
- package/src/constitution/baseline.ts +257 -0
- package/src/detect-gate.ts +498 -0
- package/src/eval/eval.types.ts +36 -0
- package/src/eval/index.ts +3 -0
- package/src/eval/judge.ts +62 -0
- package/src/eval/score.ts +39 -0
- package/src/files/create.ts +22 -0
- package/src/files/edit.ts +193 -0
- package/src/files/files.constants.ts +11 -0
- package/src/files/files.types.ts +81 -0
- package/src/files/hashline-format.ts +110 -0
- package/src/files/hashline.ts +689 -0
- package/src/files/index.ts +19 -0
- package/src/index.ts +8 -0
- package/src/inference/index.ts +6 -0
- package/src/inference/inference.constants.ts +34 -0
- package/src/inference/inference.types.ts +123 -0
- package/src/inference/openai-compatible.ts +113 -0
- package/src/inference/stream-guard.ts +161 -0
- package/src/inference/stream.ts +370 -0
- package/src/inference/transport.ts +78 -0
- package/src/inference/wire.ts +0 -0
- package/src/lib/fs/fs.ts +126 -0
- package/src/lib/fs/fs.types.ts +5 -0
- package/src/lib/fs/index.ts +3 -0
- package/src/lib/fs/process.ts +146 -0
- package/src/lib/guards/guards.ts +9 -0
- package/src/lib/guards/index.ts +1 -0
- package/src/lib/json/index.ts +1 -0
- package/src/lib/json/json.ts +12 -0
- package/src/lib/scope/index.ts +2 -0
- package/src/lib/scope/scope.constants.ts +3 -0
- package/src/lib/scope/scope.ts +40 -0
- package/src/loop/astgrep-fix.ts +228 -0
- package/src/loop/feedback/feedback.ts +138 -0
- package/src/loop/feedback/index.ts +8 -0
- package/src/loop/feedback/meta-rule-docs.ts +41 -0
- package/src/loop/feedback/meta-rule-feedback.ts +61 -0
- package/src/loop/feedback/rule-docs.generated.json +112 -0
- package/src/loop/feedback/rule-docs.ts +342 -0
- package/src/loop/index.ts +19 -0
- package/src/loop/loop.constants.ts +68 -0
- package/src/loop/loop.types.ts +99 -0
- package/src/loop/prompt/index.ts +2 -0
- package/src/loop/prompt/project-map.ts +69 -0
- package/src/loop/prompt/prompt.ts +107 -0
- package/src/loop/quality.ts +174 -0
- package/src/loop/rule-docs.generated.json +367 -0
- package/src/loop/run-spec.ts +88 -0
- package/src/loop/run.ts +400 -0
- package/src/loop/session.ts +1410 -0
- package/src/loop/tools/add-dependency.ts +71 -0
- package/src/loop/tools/condense.ts +498 -0
- package/src/loop/tools/edit-hashline.ts +80 -0
- package/src/loop/tools/execute-tool.ts +80 -0
- package/src/loop/tools/file-ops.ts +323 -0
- package/src/loop/tools/index.ts +2 -0
- package/src/loop/tools/lsp-ops.ts +222 -0
- package/src/loop/tools/scaffold-routes.ts +68 -0
- package/src/loop/tools/scaffold-ui.ts +62 -0
- package/src/loop/tools/scaffold-web.ts +35 -0
- package/src/loop/tools/tool-context.ts +126 -0
- package/src/loop/ttsr-defaults.ts +53 -0
- package/src/loop/ttsr.ts +322 -0
- package/src/loop/turn.ts +856 -0
- package/src/lsp/index.ts +2 -0
- package/src/lsp/lsp.types.ts +56 -0
- package/src/lsp/service.ts +500 -0
- package/src/meta-rules/context.ts +195 -0
- package/src/meta-rules/index.ts +9 -0
- package/src/meta-rules/meta-rules.types.ts +47 -0
- package/src/meta-rules/parsers/package-json-parser.ts +51 -0
- package/src/meta-rules/registry.ts +37 -0
- package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
- package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
- package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
- package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
- package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
- package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
- package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
- package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
- package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
- package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
- package/src/meta-rules/runner.ts +64 -0
- package/src/models-config.ts +196 -0
- package/src/render/ansi.ts +289 -0
- package/src/render/banner.ts +113 -0
- package/src/render/box.ts +134 -0
- package/src/render/index.ts +7 -0
- package/src/render/markdown.ts +123 -0
- package/src/render/render.types.ts +21 -0
- package/src/render/stream-markdown.ts +128 -0
- package/src/render/style.ts +26 -0
- package/src/rule-packs/bullmq/index.ts +39 -0
- package/src/rule-packs/bullmq/rules/index.ts +7 -0
- package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
- package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
- package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
- package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
- package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
- package/src/rule-packs/bullmq/utils.ts +334 -0
- package/src/rule-packs/code-flow/index.ts +25 -0
- package/src/rule-packs/code-flow/rules/index.ts +3 -0
- package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
- package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
- package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
- package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
- package/src/rule-packs/comment-hygiene/index.ts +25 -0
- package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
- package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
- package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
- package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
- package/src/rule-packs/create-rule.ts +9 -0
- package/src/rule-packs/drizzle/index.ts +41 -0
- package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
- package/src/rule-packs/drizzle/rules/index.ts +8 -0
- package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
- package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
- package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
- package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
- package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
- package/src/rule-packs/drizzle/utils.ts +115 -0
- package/src/rule-packs/elysia/index.ts +43 -0
- package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
- package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
- package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
- package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
- package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
- package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
- package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
- package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
- package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
- package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
- package/src/rule-packs/env-access/index.ts +23 -0
- package/src/rule-packs/env-access/rules/index.ts +2 -0
- package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
- package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
- package/src/rule-packs/i18n-keys/index.ts +19 -0
- package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
- package/src/rule-packs/index.ts +139 -0
- package/src/rule-packs/jwt-cookies/index.ts +25 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
- package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
- package/src/rule-packs/jwt-cookies/utils.ts +188 -0
- package/src/rule-packs/oauth-security/index.ts +25 -0
- package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
- package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
- package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
- package/src/rule-packs/oauth-security/utils.ts +127 -0
- package/src/rule-packs/react-component-architecture/index.ts +35 -0
- package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
- package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
- package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
- package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
- package/src/rule-packs/react-component-architecture/utils.ts +47 -0
- package/src/rule-packs/rule-packs.types.ts +18 -0
- package/src/rule-packs/structured-logging/index.ts +26 -0
- package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
- package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
- package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
- package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
- package/src/rule-packs/tanstack-query/index.ts +20 -0
- package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
- package/src/rule-packs/test-conventions/index.ts +23 -0
- package/src/rule-packs/test-conventions/rules/index.ts +2 -0
- package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
- package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
- package/src/rule-packs/utils.ts +142 -0
- package/src/session-store.ts +359 -0
- package/src/spec/generate-tests.ts +213 -0
- package/src/spec/index.ts +5 -0
- package/src/spec/parse.ts +152 -0
- package/src/spec/review-tests.ts +162 -0
- package/src/spec/spec.constants.ts +13 -0
- package/src/spec/spec.types.ts +79 -0
- package/src/stack-detection/detect.ts +246 -0
- package/src/stack-detection/index.ts +3 -0
- package/src/stack-detection/packs.ts +174 -0
- package/src/stack-detection/stack-detection.types.ts +47 -0
- package/src/validate/accept.ts +49 -0
- package/src/validate/errors.ts +35 -0
- package/src/validate/index.ts +12 -0
- package/src/validate/parse.ts +148 -0
- package/src/validate/run-tests.ts +59 -0
- package/src/validate/validate.ts +40 -0
- package/src/validate/validate.types.ts +52 -0
- package/src/web-components.ts +638 -0
- package/src/web-coverage.ts +89 -0
- package/src/web-routes.ts +151 -0
- package/src/web-templates.ts +1011 -0
- package/strict.eslint.config.mjs +84 -0
- package/strict.web.eslint.config.mjs +185 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { IRenderOptions, IStatusInfo } from "./render.types";
|
|
2
|
+
import type { ILoopEvent } from "../loop";
|
|
3
|
+
import type { IChatMessage } from "../inference";
|
|
4
|
+
import { STYLE, paint } from "./style";
|
|
5
|
+
import { box, GLYPH } from "./box";
|
|
6
|
+
import { renderMarkdown, highlightCode } from "./markdown";
|
|
7
|
+
import { StreamingMarkdown } from "./stream-markdown";
|
|
8
|
+
|
|
9
|
+
/** Split highlighted/plain text into the body-line array a box expects. */
|
|
10
|
+
function bodyLines(text: string): string[] {
|
|
11
|
+
return text.replace(/\n$/, "").split("\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A single glyph-prefixed line — the compact form for events with no body. */
|
|
15
|
+
function glyphLine(
|
|
16
|
+
glyph: string,
|
|
17
|
+
text: string,
|
|
18
|
+
accent: string,
|
|
19
|
+
color: boolean
|
|
20
|
+
): string {
|
|
21
|
+
return `\n ${paint(`${glyph} ${text}`, `${accent}${STYLE.bold}`, color)}\n`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Compact token count: 1234 → "1.2k", 14000 → "14k". */
|
|
25
|
+
function humanCount(n: number): string {
|
|
26
|
+
if (n < 1000) {
|
|
27
|
+
return String(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const k = n / 1000;
|
|
31
|
+
|
|
32
|
+
return `${k < 10 ? k.toFixed(1) : Math.round(k)}k`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Compact duration: 9000 → "9s", 84000 → "1m24s". */
|
|
36
|
+
function humanDuration(ms: number): string {
|
|
37
|
+
const total = Math.round(ms / 1000);
|
|
38
|
+
|
|
39
|
+
if (total < 60) {
|
|
40
|
+
return `${total}s`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return `${Math.floor(total / 60)}m${String(total % 60).padStart(2, "0")}s`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The post-turn status line — model, context-window usage, turns, elapsed, last
|
|
48
|
+
* outcome, scope — the at-a-glance summary modern CLIs keep on screen. Dim, one
|
|
49
|
+
* line, printed after a turn settles.
|
|
50
|
+
*/
|
|
51
|
+
export function renderStatus(
|
|
52
|
+
info: IStatusInfo,
|
|
53
|
+
opts: IRenderOptions = {}
|
|
54
|
+
): string {
|
|
55
|
+
const color = opts.color ?? true;
|
|
56
|
+
const pct =
|
|
57
|
+
info.contextWindow > 0
|
|
58
|
+
? Math.round((info.contextTokens / info.contextWindow) * 100)
|
|
59
|
+
: 0;
|
|
60
|
+
const bits = [
|
|
61
|
+
info.model,
|
|
62
|
+
`ctx ~${humanCount(info.contextTokens)}/${humanCount(info.contextWindow)} ${pct}%`,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Only show turn/elapsed once a turn has actually run (skip the "0 turns · 0s"
|
|
66
|
+
// noise on the very first prompt).
|
|
67
|
+
if (info.turns > 0) {
|
|
68
|
+
bits.push(
|
|
69
|
+
`${info.turns} turn${info.turns === 1 ? "" : "s"}`,
|
|
70
|
+
humanDuration(info.elapsedMs)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
bits.push(info.status, info.scope);
|
|
75
|
+
|
|
76
|
+
return `${paint(` ⎯ ${bits.join(" · ")}`, STYLE.dim, color)}\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Replay one stored conversation message — used to show the prior transcript on
|
|
81
|
+
* `--continue`. User turns are echoed at the prompt marker, assistant answers
|
|
82
|
+
* get markdown/code highlighting, tool calls collapse to a one-line summary, and
|
|
83
|
+
* the system prompt + raw tool output are omitted (context, not conversation).
|
|
84
|
+
*/
|
|
85
|
+
export function renderMessage(
|
|
86
|
+
message: IChatMessage,
|
|
87
|
+
opts: IRenderOptions = {}
|
|
88
|
+
): string {
|
|
89
|
+
const color = opts.color ?? true;
|
|
90
|
+
|
|
91
|
+
if (message.role === "system" || message.role === "tool") {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (message.role === "user") {
|
|
96
|
+
return `\n${paint("›", STYLE.brand + STYLE.bold, color)} ${message.content}\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const parts: string[] = [];
|
|
100
|
+
|
|
101
|
+
if (message.content.length > 0) {
|
|
102
|
+
parts.push(renderMarkdown(message.content, color));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (message.toolCalls !== undefined && message.toolCalls.length > 0) {
|
|
106
|
+
const names = message.toolCalls.map((c) => c.name).join(", ");
|
|
107
|
+
|
|
108
|
+
parts.push(paint(` · used ${names}`, STYLE.dim, color));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return parts.length > 0 ? `\n${parts.join("\n")}\n` : "";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function highlightTs(code: string, color: boolean): string {
|
|
115
|
+
return highlightCode(code, "typescript", color);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function diff(oldString: string, newString: string, color: boolean): string {
|
|
119
|
+
const minus = oldString
|
|
120
|
+
.split("\n")
|
|
121
|
+
.map((l) => paint(`- ${l}`, STYLE.red, color))
|
|
122
|
+
.join("\n");
|
|
123
|
+
const plus = newString
|
|
124
|
+
.split("\n")
|
|
125
|
+
.map((l) => paint(`+ ${l}`, STYLE.green, color))
|
|
126
|
+
.join("\n");
|
|
127
|
+
|
|
128
|
+
return `${minus}\n${plus}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Format a loop event for a terminal (ANSI) or a plain log (`color: false`).
|
|
133
|
+
* The library emits structured events; this renderer is the only place that
|
|
134
|
+
* knows about colors/layout — a web UI could render the same events differently.
|
|
135
|
+
*/
|
|
136
|
+
/** Latch so a run of `reasoning` tokens collapses to ONE "thinking…" line
|
|
137
|
+
* instead of streaming the model's full chain-of-thought. Reset by any other
|
|
138
|
+
* event (a tool marker, gate output, a message), so the next thinking burst
|
|
139
|
+
* re-announces. The raw reasoning is still written verbatim to the --log. */
|
|
140
|
+
let thinkingShown = false;
|
|
141
|
+
|
|
142
|
+
/** Live answer stream — content tokens render incrementally through this; the
|
|
143
|
+
* settled `message` event then skips its duplicate full render (sawContent). */
|
|
144
|
+
const stream = new StreamingMarkdown();
|
|
145
|
+
|
|
146
|
+
/** Render a streamed token. The answer (channel `content`) streams live through
|
|
147
|
+
* the incremental markdown renderer; the chain-of-thought (`reasoning`)
|
|
148
|
+
* collapses to a compact indicator (on a TTY the CLI spinner owns it); tool
|
|
149
|
+
* markers (✎) and gate output print normally. */
|
|
150
|
+
function renderToken(event: ILoopEvent, color: boolean): string {
|
|
151
|
+
if (event.channel === "reasoning") {
|
|
152
|
+
// On a live TTY the CLI's animated spinner is the thinking indicator; the
|
|
153
|
+
// static one-time line is for piped color output and plain logs.
|
|
154
|
+
if (color && process.stdout.isTTY) {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (thinkingShown) {
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
thinkingShown = true;
|
|
163
|
+
|
|
164
|
+
return `\n ${paint("⋯ thinking", STYLE.dim, color)}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (event.channel === "content") {
|
|
168
|
+
// Plain/log mode stays quiet here — the consolidated `message` event is
|
|
169
|
+
// the log's record, so agent.log keeps its exact pre-streaming shape.
|
|
170
|
+
return color ? stream.push(event.message, true) : "";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return paint(event.message, STYLE.dim, color);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A shell-command event as a box — exit status drives the accent + glyph (a
|
|
177
|
+
* non-zero exit goes red ✗); no output → a one-liner. */
|
|
178
|
+
function renderRun(event: ILoopEvent, color: boolean): string {
|
|
179
|
+
const ok = event.exitCode === undefined || event.exitCode === 0;
|
|
180
|
+
const title =
|
|
181
|
+
event.exitCode === undefined
|
|
182
|
+
? event.message
|
|
183
|
+
: `${event.message} (exit ${event.exitCode})`;
|
|
184
|
+
const accent = ok ? STYLE.yellow : STYLE.red;
|
|
185
|
+
const glyph = ok ? GLYPH.run : GLYPH.fail;
|
|
186
|
+
|
|
187
|
+
if (event.output === undefined || event.output.length === 0) {
|
|
188
|
+
return glyphLine(glyph, title, accent, color);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return `\n${box(title, bodyLines(event.output), { glyph, accent, color })}\n`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function renderEvent(
|
|
195
|
+
event: ILoopEvent,
|
|
196
|
+
opts: IRenderOptions = {}
|
|
197
|
+
): string {
|
|
198
|
+
const color = opts.color ?? true;
|
|
199
|
+
|
|
200
|
+
// Any event that is NOT a reasoning token ends the current thinking burst.
|
|
201
|
+
if (event.kind !== "token" || event.channel !== "reasoning") {
|
|
202
|
+
thinkingShown = false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Any event that is NOT a content token first flushes the live answer stream
|
|
206
|
+
// (the closing table/fence, or a partial line on abort) so nothing is lost.
|
|
207
|
+
const isContentToken = event.kind === "token" && event.channel === "content";
|
|
208
|
+
const pending = color && !isContentToken ? stream.flush(true) : "";
|
|
209
|
+
|
|
210
|
+
return pending + renderEventBody(event, color);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function renderEventBody(event: ILoopEvent, color: boolean): string {
|
|
214
|
+
switch (event.kind) {
|
|
215
|
+
case "token":
|
|
216
|
+
return renderToken(event, color);
|
|
217
|
+
|
|
218
|
+
case "message":
|
|
219
|
+
// The model's actual answer. When it already streamed live (content
|
|
220
|
+
// tokens), emit just a closing separator — the text is on screen; the
|
|
221
|
+
// full render here would print it twice. Without streamed content
|
|
222
|
+
// (non-streaming provider, replayed events, plain logs) render in full.
|
|
223
|
+
if (color && stream.sawContent) {
|
|
224
|
+
stream.reset();
|
|
225
|
+
|
|
226
|
+
return "\n";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return event.message.length > 0
|
|
230
|
+
? `\n${renderMarkdown(event.message, color)}\n`
|
|
231
|
+
: "";
|
|
232
|
+
|
|
233
|
+
case "start":
|
|
234
|
+
case "fix":
|
|
235
|
+
return `\n${paint(event.message, STYLE.dim, color)}\n`;
|
|
236
|
+
|
|
237
|
+
case "cycle":
|
|
238
|
+
// On screen the turn divider is just noise (the status line carries the
|
|
239
|
+
// count); keep a minimal boundary only in the plain log for `tail -f`.
|
|
240
|
+
return color
|
|
241
|
+
? ""
|
|
242
|
+
: `\n── ${event.message.replace(/:?\s*asking model\s*$/i, "")} ──\n`;
|
|
243
|
+
|
|
244
|
+
case "create":
|
|
245
|
+
return event.content === undefined
|
|
246
|
+
? glyphLine(GLYPH.create, event.message, STYLE.green, color)
|
|
247
|
+
: `\n${box(event.message, bodyLines(highlightTs(event.content, color)), { glyph: GLYPH.create, accent: STYLE.green, color })}\n`;
|
|
248
|
+
|
|
249
|
+
case "edit": {
|
|
250
|
+
if (event.oldString === undefined || event.newString === undefined) {
|
|
251
|
+
return glyphLine(GLYPH.edit, event.message, STYLE.brand, color);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const body = bodyLines(diff(event.oldString, event.newString, color));
|
|
255
|
+
|
|
256
|
+
return `\n${box(event.message, body, { glyph: GLYPH.edit, accent: STYLE.brand, color })}\n`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case "red":
|
|
260
|
+
case "stuck":
|
|
261
|
+
return `\n${paint(`${GLYPH.fail} ${event.message}`, STYLE.red + STYLE.bold, color)}\n`;
|
|
262
|
+
|
|
263
|
+
case "validated":
|
|
264
|
+
return event.passed === true
|
|
265
|
+
? `${paint(` ${GLYPH.done} ${event.message}`, STYLE.green, color)}\n`
|
|
266
|
+
: `${paint(` ${GLYPH.bullet} ${event.message}`, STYLE.yellow, color)}\n`;
|
|
267
|
+
|
|
268
|
+
case "done":
|
|
269
|
+
return `\n${paint(`${GLYPH.done} ${event.message}`, STYLE.green + STYLE.bold, color)}\n`;
|
|
270
|
+
|
|
271
|
+
case "run":
|
|
272
|
+
return renderRun(event, color);
|
|
273
|
+
|
|
274
|
+
case "usage":
|
|
275
|
+
// Logged for the metrics analyzer, but not shown — the status line already
|
|
276
|
+
// surfaces context usage on screen.
|
|
277
|
+
return "";
|
|
278
|
+
|
|
279
|
+
case "tool":
|
|
280
|
+
return ` ${paint(event.message, STYLE.dim, color)}\n`;
|
|
281
|
+
|
|
282
|
+
case "timing":
|
|
283
|
+
// Noise on screen (the status line shows turns + elapsed); log only.
|
|
284
|
+
return color ? "" : ` ${event.message}\n`;
|
|
285
|
+
|
|
286
|
+
default:
|
|
287
|
+
return `\n${event.message}\n`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { STYLE, paint } from "./style";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Welcome banner for the interactive CLI — solid forge emblem, wordmark,
|
|
5
|
+
* model/endpoint. Centering uses visible (un-painted) length.
|
|
6
|
+
*/
|
|
7
|
+
export interface IBannerInfo {
|
|
8
|
+
model: string;
|
|
9
|
+
endpoint: string;
|
|
10
|
+
color?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Chars between the two vertical borders. */
|
|
14
|
+
const INNER = 58;
|
|
15
|
+
|
|
16
|
+
interface ISegment {
|
|
17
|
+
text: string;
|
|
18
|
+
code?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ILine {
|
|
22
|
+
text?: string;
|
|
23
|
+
code?: string;
|
|
24
|
+
segments?: readonly ISegment[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const BLANK: ILine = { text: "" };
|
|
28
|
+
|
|
29
|
+
/** Compact solid anvil — filled blocks, horn + face + base (~9 cols). */
|
|
30
|
+
const EMBLEM: readonly ILine[] = [
|
|
31
|
+
{ text: "· ✦ ✦ ·", code: STYLE.brandLight },
|
|
32
|
+
{ text: "▄▀▀▀▄", code: STYLE.brandLight + STYLE.bold },
|
|
33
|
+
{ text: "███████", code: STYLE.brand + STYLE.bold },
|
|
34
|
+
{ text: "▀▀▀▀▀▀▀▀▀", code: STYLE.brandDark + STYLE.bold },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** Split wordmark under the emblem. */
|
|
38
|
+
const WORDMARK: ILine = {
|
|
39
|
+
segments: [
|
|
40
|
+
{ text: "ts", code: STYLE.brandLight + STYLE.bold },
|
|
41
|
+
{ text: "forge", code: STYLE.brand + STYLE.bold },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function welcomeBanner(info: IBannerInfo): string {
|
|
46
|
+
const color = info.color ?? true;
|
|
47
|
+
|
|
48
|
+
const lines: ILine[] = [
|
|
49
|
+
BLANK,
|
|
50
|
+
...EMBLEM,
|
|
51
|
+
BLANK,
|
|
52
|
+
WORDMARK,
|
|
53
|
+
BLANK,
|
|
54
|
+
{ text: "strict TypeScript, gate-driven", code: STYLE.dim },
|
|
55
|
+
BLANK,
|
|
56
|
+
{ text: info.model, code: STYLE.brand + STYLE.bold },
|
|
57
|
+
{ text: info.endpoint, code: STYLE.dim },
|
|
58
|
+
BLANK,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const body = lines.map((line) => boxLine(line, color)).join("\n");
|
|
62
|
+
|
|
63
|
+
return `${topBorder(color)}\n${body}\n${bottomBorder()}\n`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function topBorder(color: boolean): string {
|
|
67
|
+
const label = "─── tsforge ";
|
|
68
|
+
const fill = "─".repeat(Math.max(0, INNER - label.length));
|
|
69
|
+
const frame = `╭${label}${fill}╮`;
|
|
70
|
+
|
|
71
|
+
return color
|
|
72
|
+
? paint("╭", STYLE.dim, color) +
|
|
73
|
+
paint(label, STYLE.brandDark, color) +
|
|
74
|
+
paint(fill + "╮", STYLE.dim, color)
|
|
75
|
+
: frame;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function bottomBorder(): string {
|
|
79
|
+
return `╰${"─".repeat(INNER)}╯`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function visibleText(line: ILine): string {
|
|
83
|
+
if (line.segments !== undefined) {
|
|
84
|
+
return line.segments.map((s) => s.text).join("");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return line.text ?? "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderContent(line: ILine, color: boolean): string {
|
|
91
|
+
if (line.segments !== undefined) {
|
|
92
|
+
return line.segments
|
|
93
|
+
.map((s) =>
|
|
94
|
+
s.code === undefined ? s.text : paint(s.text, s.code, color)
|
|
95
|
+
)
|
|
96
|
+
.join("");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const text = line.text ?? "";
|
|
100
|
+
|
|
101
|
+
return line.code === undefined ? text : paint(text, line.code, color);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Center `line` within INNER and frame it with the vertical borders. */
|
|
105
|
+
function boxLine(line: ILine, color: boolean): string {
|
|
106
|
+
const visible = visibleText(line);
|
|
107
|
+
const pad = Math.max(0, INNER - visible.length);
|
|
108
|
+
const left = Math.floor(pad / 2);
|
|
109
|
+
const right = pad - left;
|
|
110
|
+
const content = renderContent(line, color);
|
|
111
|
+
|
|
112
|
+
return `│${" ".repeat(left)}${content}${" ".repeat(right)}│`;
|
|
113
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { STYLE, paint } from "./style";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Terminal drawing primitives — title-tabbed boxes and box-drawn tables, the look
|
|
5
|
+
* a modern coding CLI uses to make tool output legible. Pure string→string (no
|
|
6
|
+
* cursor control, no streaming, no readline interaction), so they render discrete
|
|
7
|
+
* events ONLY and can never disturb input. `color: false` (logs / non-TTY)
|
|
8
|
+
* degrades to plain indented text.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** One source of truth for status → glyph, so every event renders consistently. */
|
|
12
|
+
export const GLYPH = {
|
|
13
|
+
done: "✓",
|
|
14
|
+
fail: "✗",
|
|
15
|
+
warn: "⚠",
|
|
16
|
+
info: "●",
|
|
17
|
+
run: "→",
|
|
18
|
+
create: "✚",
|
|
19
|
+
edit: "✎",
|
|
20
|
+
bullet: "•",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
const DEFAULT_WIDTH = 80;
|
|
24
|
+
const MIN_WIDTH = 48;
|
|
25
|
+
const MAX_WIDTH = 100;
|
|
26
|
+
|
|
27
|
+
/** Terminal width, clamped to a sane band — a stable default off a TTY. */
|
|
28
|
+
function termWidth(): number {
|
|
29
|
+
const cols = process.stdout.columns;
|
|
30
|
+
|
|
31
|
+
return Number.isFinite(cols)
|
|
32
|
+
? Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, cols))
|
|
33
|
+
: DEFAULT_WIDTH;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IBoxOptions {
|
|
37
|
+
glyph?: string;
|
|
38
|
+
accent?: string;
|
|
39
|
+
color?: boolean;
|
|
40
|
+
width?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A title-tabbed block: a top rule that opens with `┌─ <glyph> <title> ` and runs
|
|
45
|
+
* to the terminal width, each body line on a `│ ` gutter, closed by a `└` rule.
|
|
46
|
+
* Body lines may already contain ANSI — the right edge is never padded, so no
|
|
47
|
+
* fragile visible-width math is needed.
|
|
48
|
+
*/
|
|
49
|
+
export function box(
|
|
50
|
+
title: string,
|
|
51
|
+
bodyLines: readonly string[],
|
|
52
|
+
opts: IBoxOptions = {}
|
|
53
|
+
): string {
|
|
54
|
+
const {
|
|
55
|
+
glyph = "",
|
|
56
|
+
accent = STYLE.brand,
|
|
57
|
+
color = true,
|
|
58
|
+
width = termWidth(),
|
|
59
|
+
} = opts;
|
|
60
|
+
const head = glyph.length > 0 ? `${glyph} ${title}` : title;
|
|
61
|
+
|
|
62
|
+
if (!color) {
|
|
63
|
+
const body = bodyLines.map((l) => ` ${l}`).join("\n");
|
|
64
|
+
|
|
65
|
+
return bodyLines.length > 0 ? ` ${head}\n${body}` : ` ${head}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const opener = "┌─ ";
|
|
69
|
+
const rule = "─".repeat(Math.max(0, width - opener.length - head.length - 1));
|
|
70
|
+
const top = `${paint(opener, STYLE.dim, color)}${paint(head, `${accent}${STYLE.bold}`, color)} ${paint(rule, STYLE.dim, color)}`;
|
|
71
|
+
const bar = paint("│", STYLE.dim, color);
|
|
72
|
+
const bottom = paint(
|
|
73
|
+
`└${"─".repeat(Math.max(0, width - 1))}`,
|
|
74
|
+
STYLE.dim,
|
|
75
|
+
color
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (bodyLines.length === 0) {
|
|
79
|
+
return `${top}\n${bottom}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const body = bodyLines.map((l) => `${bar} ${l}`).join("\n");
|
|
83
|
+
|
|
84
|
+
return `${top}\n${body}\n${bottom}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render rows as a real box-drawn table — `rows[0]` is the header. Columns auto-
|
|
89
|
+
* size to their widest cell; the header is accented. The model answers with GFM
|
|
90
|
+
* markdown tables constantly, which print as raw `|` soup otherwise.
|
|
91
|
+
*/
|
|
92
|
+
export function table(
|
|
93
|
+
rows: readonly (readonly string[])[],
|
|
94
|
+
color = true
|
|
95
|
+
): string {
|
|
96
|
+
if (rows.length === 0) {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const cols = Math.max(...rows.map((r) => r.length));
|
|
101
|
+
const widths = Array.from({ length: cols }, (_, c) =>
|
|
102
|
+
Math.max(1, ...rows.map((r) => (r[c] ?? "").length))
|
|
103
|
+
);
|
|
104
|
+
const bar = paint("│", STYLE.dim, color);
|
|
105
|
+
|
|
106
|
+
const rule = (left: string, mid: string, right: string): string =>
|
|
107
|
+
paint(
|
|
108
|
+
`${left}${widths.map((w) => "─".repeat(w + 2)).join(mid)}${right}`,
|
|
109
|
+
STYLE.dim,
|
|
110
|
+
color
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const renderRow = (cells: readonly string[], header: boolean): string => {
|
|
114
|
+
const inner = widths
|
|
115
|
+
.map((w, c) => {
|
|
116
|
+
const cell = (cells[c] ?? "").padEnd(w);
|
|
117
|
+
|
|
118
|
+
return ` ${header ? paint(cell, `${STYLE.brand}${STYLE.bold}`, color) : cell} `;
|
|
119
|
+
})
|
|
120
|
+
.join(bar);
|
|
121
|
+
|
|
122
|
+
return `${bar}${inner}${bar}`;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const [header, ...body] = rows;
|
|
126
|
+
|
|
127
|
+
return [
|
|
128
|
+
rule("┌", "┬", "┐"),
|
|
129
|
+
renderRow(header ?? [], true),
|
|
130
|
+
rule("├", "┼", "┤"),
|
|
131
|
+
...body.map((r) => renderRow(r, false)),
|
|
132
|
+
rule("└", "┴", "┘"),
|
|
133
|
+
].join("\n");
|
|
134
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./render.types";
|
|
2
|
+
export { renderEvent, renderMessage, renderStatus } from "./ansi";
|
|
3
|
+
export { welcomeBanner, type IBannerInfo } from "./banner";
|
|
4
|
+
export { box, table, GLYPH } from "./box";
|
|
5
|
+
export { renderMarkdown, formatTables, highlightCode } from "./markdown";
|
|
6
|
+
export { StreamingMarkdown } from "./stream-markdown";
|
|
7
|
+
export { STYLE, RESET, paint } from "./style";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { highlight } from "cli-highlight";
|
|
2
|
+
import { STYLE, paint } from "./style";
|
|
3
|
+
import { table } from "./box";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Markdown → terminal formatting, shared by the settled-message renderer
|
|
7
|
+
* (ansi.ts) and the live streaming renderer (stream-markdown.ts) so a streamed
|
|
8
|
+
* answer and a re-rendered one look identical.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Render an assistant message: fenced ```code``` blocks syntax-highlighted, and
|
|
13
|
+
* GitHub-flavored markdown TABLES drawn as real box tables (the model answers with
|
|
14
|
+
* `| a | b |` tables constantly — raw they're unreadable pipe soup). Other prose
|
|
15
|
+
* passes through.
|
|
16
|
+
*/
|
|
17
|
+
export function renderMarkdown(text: string, color: boolean): string {
|
|
18
|
+
return text
|
|
19
|
+
.split(/(```[\s\S]*?```)/g)
|
|
20
|
+
.map((part) => {
|
|
21
|
+
const fence = /^```([\w-]*)\n?([\s\S]*?)\n?```$/.exec(part);
|
|
22
|
+
|
|
23
|
+
if (fence === null) {
|
|
24
|
+
return formatTables(part, color);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const lang =
|
|
28
|
+
fence[1] !== undefined && fence[1].length > 0 ? fence[1] : "typescript";
|
|
29
|
+
|
|
30
|
+
return highlightCode(fence[2] ?? "", lang, color);
|
|
31
|
+
})
|
|
32
|
+
.join("");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A markdown table separator row, e.g. `|----|:--:|---|`. */
|
|
36
|
+
function isTableSeparator(line: string | undefined): boolean {
|
|
37
|
+
return (
|
|
38
|
+
line !== undefined &&
|
|
39
|
+
line.includes("|") &&
|
|
40
|
+
line.includes("-") &&
|
|
41
|
+
/^\s*\|?[\s:|-]*-[\s:|-]*\|?\s*$/.test(line)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Split a `| a | b |` row into trimmed cells (tolerates missing edge pipes). */
|
|
46
|
+
function tableCells(line: string): string[] {
|
|
47
|
+
return line
|
|
48
|
+
.trim()
|
|
49
|
+
.replace(/^\|/, "")
|
|
50
|
+
.replace(/\|$/, "")
|
|
51
|
+
.split("|")
|
|
52
|
+
.map((c) => c.trim());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Replace each GFM table block in `text` with a box-drawn table; leave the rest. */
|
|
56
|
+
export function formatTables(text: string, color: boolean): string {
|
|
57
|
+
const lines = text.split("\n");
|
|
58
|
+
const out: string[] = [];
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < lines.length; ) {
|
|
61
|
+
const header = lines[i];
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
header !== undefined &&
|
|
65
|
+
header.includes("|") &&
|
|
66
|
+
isTableSeparator(lines[i + 1])
|
|
67
|
+
) {
|
|
68
|
+
const rows: string[][] = [tableCells(header)];
|
|
69
|
+
let j = i + 2;
|
|
70
|
+
|
|
71
|
+
while (j < lines.length && (lines[j]?.includes("|") ?? false)) {
|
|
72
|
+
rows.push(tableCells(lines[j] ?? ""));
|
|
73
|
+
j += 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
out.push(table(rows, color));
|
|
77
|
+
i = j;
|
|
78
|
+
} else {
|
|
79
|
+
out.push(header ?? "");
|
|
80
|
+
i += 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return out.join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function highlightCode(
|
|
88
|
+
code: string,
|
|
89
|
+
lang: string,
|
|
90
|
+
color: boolean
|
|
91
|
+
): string {
|
|
92
|
+
if (!color) {
|
|
93
|
+
return code;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
return highlight(code, { language: lang, ignoreIllegals: true });
|
|
98
|
+
} catch {
|
|
99
|
+
return code;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Cheap inline styling for one streamed prose line — `#` headings and
|
|
104
|
+
* `**bold**` brighten, `code` spans use brand light. No-op without color. */
|
|
105
|
+
export function styleInline(line: string, color: boolean): string {
|
|
106
|
+
if (!color) {
|
|
107
|
+
return line;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(line);
|
|
111
|
+
|
|
112
|
+
if (heading !== null) {
|
|
113
|
+
return paint(heading[1] ?? "", STYLE.bold, color);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return line
|
|
117
|
+
.replace(/\*\*([^*]+)\*\*/g, (_m: string, t: string) =>
|
|
118
|
+
paint(t, STYLE.bold, color)
|
|
119
|
+
)
|
|
120
|
+
.replace(/`([^`]+)`/g, (_m: string, t: string) =>
|
|
121
|
+
paint(t, STYLE.brandLight, color)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface IRenderOptions {
|
|
2
|
+
/** Emit ANSI color codes (terminal) vs plain text (log files). Default true. */
|
|
3
|
+
color?: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** A compact post-turn status line — the "where am I" summary modern CLIs show. */
|
|
7
|
+
export interface IStatusInfo {
|
|
8
|
+
model: string;
|
|
9
|
+
/** Estimated tokens of conversation context currently held. */
|
|
10
|
+
contextTokens: number;
|
|
11
|
+
/** The model's context window (for the used/total ratio). */
|
|
12
|
+
contextWindow: number;
|
|
13
|
+
/** Turns the last send took. */
|
|
14
|
+
turns: number;
|
|
15
|
+
/** Wall-clock of the last send, in ms. */
|
|
16
|
+
elapsedMs: number;
|
|
17
|
+
/** Outcome of the last send (responded / done / stuck / interrupted). */
|
|
18
|
+
status: string;
|
|
19
|
+
/** Editable scope label. */
|
|
20
|
+
scope: string;
|
|
21
|
+
}
|