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.
Files changed (177) hide show
  1. package/README.ja.md +160 -0
  2. package/README.ko.md +160 -0
  3. package/README.md +115 -297
  4. package/README.zh.md +160 -0
  5. package/package.json +11 -6
  6. package/scripts/install.sh +28 -28
  7. package/scripts/uninstall.sh +17 -15
  8. package/src/AGENTS.md +50 -0
  9. package/src/agent/AGENTS.md +49 -0
  10. package/src/agent/bash-fixups.ts +103 -0
  11. package/src/agent/compaction.ts +410 -19
  12. package/src/agent/config-schema.ts +119 -5
  13. package/src/agent/context-files.ts +314 -17
  14. package/src/agent/dev/AGENTS.md +36 -0
  15. package/src/agent/dev/advanced-analyzer.ts +12 -0
  16. package/src/agent/dev/evolution-bridge.ts +82 -0
  17. package/src/agent/dev/evolution-logger.ts +41 -0
  18. package/src/agent/dev/self-analysis.ts +64 -0
  19. package/src/agent/dev/self-improve.ts +24 -0
  20. package/src/agent/dev/spec-automation.ts +49 -0
  21. package/src/agent/engine.ts +804 -54
  22. package/src/agent/hooks.ts +273 -0
  23. package/src/agent/loop.ts +21 -1
  24. package/src/agent/memory.ts +201 -0
  25. package/src/agent/model-recency.ts +32 -0
  26. package/src/agent/output-minimizer.ts +108 -0
  27. package/src/agent/output-util.ts +64 -0
  28. package/src/agent/plan.ts +187 -0
  29. package/src/agent/seed.ts +52 -0
  30. package/src/agent/session.ts +235 -21
  31. package/src/agent/state.ts +286 -39
  32. package/src/agent/step-budget.ts +232 -0
  33. package/src/agent/subagents.ts +223 -26
  34. package/src/agent/task-tool.ts +272 -0
  35. package/src/agent/todo-tool.ts +87 -0
  36. package/src/agent/tokenizer.ts +117 -0
  37. package/src/agent/tool-registry.ts +54 -0
  38. package/src/agent/tools.ts +562 -103
  39. package/src/agent/web-search.ts +538 -0
  40. package/src/ai/AGENTS.md +44 -0
  41. package/src/ai/index.ts +1 -0
  42. package/src/ai/model-catalog-compat.ts +3 -1
  43. package/src/ai/model-catalog.ts +74 -9
  44. package/src/ai/model-discovery.ts +215 -17
  45. package/src/ai/model-manager.ts +346 -32
  46. package/src/ai/model-picker.ts +1 -1
  47. package/src/ai/model-registry.ts +4 -2
  48. package/src/ai/pricing.ts +84 -0
  49. package/src/ai/provider-registry.ts +23 -0
  50. package/src/ai/provider-status.ts +60 -16
  51. package/src/ai/providers/AGENTS.md +42 -0
  52. package/src/ai/providers/anthropic.ts +250 -31
  53. package/src/ai/providers/antigravity.ts +219 -0
  54. package/src/ai/providers/errors.ts +15 -1
  55. package/src/ai/providers/gemini.ts +196 -13
  56. package/src/ai/providers/ollama.ts +37 -7
  57. package/src/ai/providers/openai-responses.ts +173 -0
  58. package/src/ai/providers/openai.ts +64 -12
  59. package/src/ai/sse.ts +4 -1
  60. package/src/ai/types.ts +18 -1
  61. package/src/auth/AGENTS.md +41 -0
  62. package/src/auth/callback-server.ts +6 -1
  63. package/src/auth/flows/AGENTS.md +32 -0
  64. package/src/auth/flows/antigravity.ts +151 -0
  65. package/src/auth/flows/google-project.ts +190 -0
  66. package/src/auth/flows/google.ts +39 -18
  67. package/src/auth/flows/index.ts +15 -5
  68. package/src/auth/flows/openai.ts +2 -2
  69. package/src/auth/oauth.ts +8 -0
  70. package/src/auth/refresh.ts +44 -27
  71. package/src/auth/storage.ts +149 -26
  72. package/src/auth/types.ts +1 -1
  73. package/src/autopilot.ts +362 -0
  74. package/src/bun-imports.d.ts +4 -0
  75. package/src/cli/AGENTS.md +39 -0
  76. package/src/cli/runner.ts +148 -14
  77. package/src/cli.ts +13 -4
  78. package/src/commands/AGENTS.md +40 -0
  79. package/src/commands/approve.ts +62 -3
  80. package/src/commands/auth.ts +167 -25
  81. package/src/commands/chat.ts +37 -8
  82. package/src/commands/deep-interview.ts +633 -175
  83. package/src/commands/doctor.ts +84 -37
  84. package/src/commands/evolve-core.ts +18 -0
  85. package/src/commands/evolve.ts +2 -1
  86. package/src/commands/export.ts +176 -0
  87. package/src/commands/gjc.ts +52 -0
  88. package/src/commands/launch.ts +3549 -240
  89. package/src/commands/mcp.ts +3 -3
  90. package/src/commands/ooo-seed.ts +19 -0
  91. package/src/commands/ralplan.ts +253 -35
  92. package/src/commands/resume.ts +1 -1
  93. package/src/commands/session.ts +183 -0
  94. package/src/commands/setup-helpers.ts +10 -3
  95. package/src/commands/setup.ts +57 -16
  96. package/src/commands/skills.ts +78 -18
  97. package/src/commands/state.ts +198 -0
  98. package/src/commands/status.ts +84 -0
  99. package/src/commands/team.ts +340 -212
  100. package/src/commands/ultragoal.ts +122 -61
  101. package/src/commands/update.ts +244 -0
  102. package/src/ledger.ts +270 -0
  103. package/src/mcp/AGENTS.md +38 -0
  104. package/src/mcp/server.ts +115 -14
  105. package/src/mcp/tools.ts +42 -22
  106. package/src/md-modules.d.ts +4 -0
  107. package/src/prompts/AGENTS.md +41 -0
  108. package/src/prompts/agents/AGENTS.md +35 -0
  109. package/src/prompts/agents/architect.md +35 -0
  110. package/src/prompts/agents/critic.md +37 -0
  111. package/src/prompts/agents/executor.md +36 -0
  112. package/src/prompts/agents/planner.md +37 -0
  113. package/src/prompts/skills/AGENTS.md +36 -0
  114. package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
  115. package/src/prompts/skills/deep-dive/SKILL.md +13 -0
  116. package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
  117. package/src/prompts/skills/deep-interview/SKILL.md +12 -0
  118. package/src/prompts/skills/gjc/AGENTS.md +31 -0
  119. package/src/prompts/skills/gjc/SKILL.md +15 -0
  120. package/src/prompts/skills/ralplan/AGENTS.md +31 -0
  121. package/src/prompts/skills/ralplan/SKILL.md +11 -0
  122. package/src/prompts/skills/team/AGENTS.md +31 -0
  123. package/src/prompts/skills/team/SKILL.md +11 -0
  124. package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
  125. package/src/prompts/skills/ultragoal/SKILL.md +11 -0
  126. package/src/skills/AGENTS.md +38 -0
  127. package/src/skills/catalog.ts +565 -31
  128. package/src/tui/AGENTS.md +43 -0
  129. package/src/tui/app.ts +1181 -92
  130. package/src/tui/components/AGENTS.md +42 -0
  131. package/src/tui/components/ascii-art.ts +257 -15
  132. package/src/tui/components/autocomplete.ts +98 -16
  133. package/src/tui/components/autopilot-status.ts +65 -0
  134. package/src/tui/components/category-index.ts +49 -0
  135. package/src/tui/components/code-view.ts +54 -11
  136. package/src/tui/components/color.ts +171 -2
  137. package/src/tui/components/config-panel.ts +82 -15
  138. package/src/tui/components/duration.ts +38 -0
  139. package/src/tui/components/evolution.ts +3 -3
  140. package/src/tui/components/footer.ts +91 -42
  141. package/src/tui/components/forge.ts +426 -31
  142. package/src/tui/components/hints.ts +54 -0
  143. package/src/tui/components/hud.ts +73 -0
  144. package/src/tui/components/index.ts +4 -0
  145. package/src/tui/components/input-box.ts +150 -0
  146. package/src/tui/components/layout.ts +11 -3
  147. package/src/tui/components/live-model-picker.ts +108 -0
  148. package/src/tui/components/markdown-table.ts +140 -0
  149. package/src/tui/components/markdown-text.ts +97 -0
  150. package/src/tui/components/meter.ts +4 -1
  151. package/src/tui/components/model-picker.ts +3 -2
  152. package/src/tui/components/provider-picker.ts +3 -2
  153. package/src/tui/components/section.ts +70 -0
  154. package/src/tui/components/select-list.ts +40 -10
  155. package/src/tui/components/skill-picker.ts +25 -0
  156. package/src/tui/components/slash.ts +244 -21
  157. package/src/tui/components/status.ts +272 -11
  158. package/src/tui/components/step-timeline.ts +218 -0
  159. package/src/tui/components/stream.ts +26 -9
  160. package/src/tui/components/themes.ts +212 -6
  161. package/src/tui/components/todo-card.ts +47 -0
  162. package/src/tui/components/tool-list.ts +58 -12
  163. package/src/tui/components/transcript.ts +120 -0
  164. package/src/tui/components/update-box.ts +31 -0
  165. package/src/tui/components/welcome.ts +162 -0
  166. package/src/tui/components/width.ts +163 -0
  167. package/src/tui/monitoring/AGENTS.md +31 -0
  168. package/src/tui/monitoring/hud-view.ts +55 -0
  169. package/src/tui/renderer.ts +112 -3
  170. package/src/tui/terminal.ts +40 -33
  171. package/src/util/AGENTS.md +39 -0
  172. package/src/util/clipboard-image.ts +118 -0
  173. package/src/util/env.ts +12 -0
  174. package/src/util/provider-error.ts +78 -0
  175. package/src/util/retry.ts +91 -6
  176. package/src/util/update-check.ts +64 -0
  177. package/src/commands/models.ts +0 -104
@@ -0,0 +1,73 @@
1
+ import chalk from "chalk";
2
+
3
+ export type JeoPhase = "thinking" | "planning" | "executing" | "reporting" | "done";
4
+
5
+ export interface HudOptions {
6
+ unicode?: boolean;
7
+ color?: boolean;
8
+ }
9
+
10
+ export function renderHud(phase: JeoPhase, opts: HudOptions = {}): string {
11
+ const unicode = opts.unicode !== false;
12
+ const color = opts.color !== false;
13
+
14
+ const phases: JeoPhase[] = ["thinking", "planning", "executing", "reporting", "done"];
15
+ const activeIndex = phases.indexOf(phase);
16
+
17
+ const arrow = unicode ? " → " : " -> ";
18
+ const renderedArrow = color ? chalk.dim(arrow) : arrow;
19
+
20
+ const parts = phases.map((p, i) => {
21
+ let glyph = "";
22
+ let formatted = "";
23
+
24
+ if (i < activeIndex) {
25
+ // Completed
26
+ glyph = unicode ? "✔" : "v";
27
+ formatted = `${glyph} ${p}`;
28
+ if (color) {
29
+ formatted = chalk.green(formatted);
30
+ }
31
+ } else if (i === activeIndex) {
32
+ // Active
33
+ glyph = unicode ? "●" : "*";
34
+ formatted = `${glyph} ${p}`;
35
+ if (color) {
36
+ formatted = chalk.cyan.bold(formatted);
37
+ }
38
+ } else {
39
+ // Future
40
+ glyph = unicode ? "○" : "o";
41
+ formatted = `${glyph} ${p}`;
42
+ if (color) {
43
+ formatted = chalk.dim(formatted);
44
+ }
45
+ }
46
+ return formatted;
47
+ });
48
+
49
+ return parts.join(renderedArrow);
50
+ }
51
+
52
+ export interface DerivePhaseInput {
53
+ thinking: boolean;
54
+ runningTool: boolean;
55
+ todosActive: boolean;
56
+ finished: boolean;
57
+ }
58
+
59
+ export function derivePhase(input: DerivePhaseInput): JeoPhase {
60
+ if (input.finished) {
61
+ return "done";
62
+ }
63
+ if (input.runningTool) {
64
+ return "executing";
65
+ }
66
+ if (input.thinking) {
67
+ return "thinking";
68
+ }
69
+ if (input.todosActive) {
70
+ return "planning";
71
+ }
72
+ return "reporting";
73
+ }
@@ -5,3 +5,7 @@ export * from "./footer";
5
5
  export * from "./ascii-art";
6
6
  export * from "./forge";
7
7
  export * from "./status";
8
+ export * from "./category-index";
9
+ export * from "./input-box";
10
+ export * from "./hud";
11
+ export * from "./autopilot-status";
@@ -0,0 +1,150 @@
1
+ import chalk from "chalk";
2
+ import { BOX_ASCII, BOX_UNICODE, boxBlock } from "./layout";
3
+ import { visibleWidth } from "./width";
4
+
5
+ export interface InputBoxOptions {
6
+ cols?: number;
7
+ color?: boolean;
8
+ unicode?: boolean;
9
+ cwdLabel?: string;
10
+ /** Pending clipboard-image attachments hint (e.g. "⧉ 1 image attached — ctrl+v"). */
11
+ attachmentLabel?: string;
12
+ placeholder?: string;
13
+ maxBodyRows?: number;
14
+ /** Caret offset in CHARACTERS into `line` (readline's rl.cursor). Defaults to end. */
15
+ cursor?: number;
16
+ /** Accent painter for the border + `>` prompt mark (theme accent); default red/blue. */
17
+ accent?: (s: string) => string;
18
+ /** Shadow painter for the bottom/right "shaded" edges; defaults to a dim accent.
19
+ * The lit-vs-shaded two-tone contrast gives the box visible depth. */
20
+ accentShadow?: (s: string) => string;
21
+ }
22
+
23
+ export interface InputFrame {
24
+ lines: string[];
25
+ /** Caret row relative to the box's FIRST line (0 = top border, 1 = first body row). */
26
+ cursorRow: number;
27
+ /** 1-based terminal column of the caret cell. */
28
+ cursorCol: number;
29
+ }
30
+
31
+ /**
32
+ * Wrap plain input text (readline lines carry no ANSI) by DISPLAY width and map the
33
+ * caret's character offset to its (row, columnWidth) cell in the same pass — so the
34
+ * box prompt can place the real terminal cursor exactly where the next glyph lands,
35
+ * and arrow-key movement (readline updates rl.cursor) repositions it visibly.
36
+ */
37
+ function wrapWithCursor(
38
+ text: string,
39
+ cursor: number,
40
+ width: number,
41
+ ): { rows: string[]; row: number; col: number } {
42
+ const rows: string[] = [];
43
+ let cur = "";
44
+ let curW = 0;
45
+ let row = 0;
46
+ let col = 0;
47
+ const chars = Array.from(text.replace(/\r/g, ""));
48
+ const pos = Math.max(0, Math.min(cursor, chars.length));
49
+ for (let i = 0; i <= chars.length; i++) {
50
+ const ch = i < chars.length ? chars[i]! : "";
51
+ const rendered = ch === "\t" ? " " : ch;
52
+ const w = ch === "" || ch === "\n" ? 0 : ch === "\t" ? 2 : visibleWidth(ch);
53
+ // Wrap BEFORE recording the caret so a caret on a wrapping char follows it down.
54
+ if (w > 0 && curW + w > width && curW > 0) {
55
+ rows.push(cur);
56
+ cur = "";
57
+ curW = 0;
58
+ }
59
+ if (i === pos) {
60
+ row = rows.length;
61
+ col = curW;
62
+ }
63
+ if (ch === "\n") {
64
+ rows.push(cur);
65
+ cur = "";
66
+ curW = 0;
67
+ continue;
68
+ }
69
+ if (ch !== "") {
70
+ cur += rendered;
71
+ curW += w;
72
+ }
73
+ }
74
+ rows.push(cur);
75
+ return { rows, row, col };
76
+ }
77
+
78
+ /**
79
+ * Boxed input prompt (gjc-style): a `>` marker leads the first body row, the typed
80
+ * text (or a dim placeholder) follows, and the caret cell is reported so the caller
81
+ * can park the REAL terminal cursor right after `>` — moving with the arrow keys.
82
+ */
83
+ export function renderInputFrame(line: string, opts: InputBoxOptions = {}): InputFrame {
84
+ const cols = Math.max(24, Math.trunc(opts.cols ?? 80));
85
+ const useColor = opts.color !== false;
86
+ const placeholder = opts.placeholder ?? "Type your message...";
87
+ const bodyWidth = Math.max(1, cols - 4);
88
+ const textWidth = Math.max(1, bodyWidth - 2); // "> " / " " prefix columns
89
+
90
+ let rows: string[];
91
+ let crow = 0;
92
+ let ccol = 0;
93
+ let placeholderRow = false;
94
+ if (line.length === 0) {
95
+ rows = [placeholder];
96
+ placeholderRow = true;
97
+ } else {
98
+ const wrapped = wrapWithCursor(line, opts.cursor ?? line.length, textWidth);
99
+ rows = wrapped.rows;
100
+ crow = wrapped.row;
101
+ ccol = wrapped.col;
102
+ }
103
+
104
+ // Tail-truncate to maxBodyRows (caret usually edits near the end); the first
105
+ // visible row carries an `…` marker when earlier rows are hidden.
106
+ const maxBodyRows = Math.max(1, Math.trunc(opts.maxBodyRows ?? rows.length));
107
+ let hidden = 0;
108
+ if (rows.length > maxBodyRows) {
109
+ hidden = rows.length - maxBodyRows;
110
+ rows = rows.slice(hidden);
111
+ rows[0] = `…${rows[0] ?? ""}`.slice(0, textWidth);
112
+ }
113
+ let visRow = Math.max(0, Math.min(crow - hidden, rows.length - 1));
114
+ if (hidden > 0 && crow - hidden === 0) ccol += 1; // shifted by the `…` marker
115
+ if (crow - hidden < 0) { visRow = 0; ccol = 0; }
116
+
117
+ const promptMark = "> ";
118
+ const paintPrompt = useColor ? (opts.accent ?? chalk.red) : (s: string) => s;
119
+ const paintGhost = useColor ? chalk.dim : (s: string) => s;
120
+ const body = rows.map((r, i) => {
121
+ const content = placeholderRow ? paintGhost(r) : r;
122
+ return i === 0 ? paintPrompt(promptMark) + content : " " + r;
123
+ });
124
+
125
+ const content = [...body];
126
+ if (opts.attachmentLabel) {
127
+ content.push(useColor ? chalk.cyan(opts.attachmentLabel) : opts.attachmentLabel);
128
+ }
129
+ if (opts.cwdLabel) {
130
+ content.push(useColor ? chalk.gray(opts.cwdLabel) : opts.cwdLabel);
131
+ }
132
+ const glyphs = opts.unicode === false ? BOX_ASCII : BOX_UNICODE;
133
+ // Depth cue: lit top/left edge (bright accent) vs shaded bottom/right edge (dim).
134
+ const paint = useColor ? (opts.accent ?? chalk.blueBright) : (s: string) => s;
135
+ const paintShadow = useColor ? (opts.accentShadow ?? ((s: string) => chalk.blue.dim(s))) : (s: string) => s;
136
+ const lines = boxBlock(content, cols, { glyphs, paint, paintShadow, align: "left" });
137
+
138
+ // Terminal columns: border at col 1, content starts col 2, text after "> " at col 4.
139
+ const cursorRow = 1 + visRow;
140
+ const cursorCol = 4 + (placeholderRow ? 0 : ccol);
141
+ return { lines, cursorRow, cursorCol };
142
+ }
143
+
144
+ /**
145
+ * Renders a boxed input box enclosing either the current typed text or a placeholder.
146
+ * If opts.cwdLabel is provided, a dim gray label line is appended after the text inside the box.
147
+ */
148
+ export function renderInputBox(line: string, opts: InputBoxOptions = {}): string[] {
149
+ return renderInputFrame(line, opts).lines;
150
+ }
@@ -6,6 +6,7 @@
6
6
  * pure functions over `string[]` blocks, injectable `cols`/`rows` for tests.
7
7
  */
8
8
  import { visibleWidth } from "./color";
9
+ import { truncate } from "../terminal";
9
10
 
10
11
  export type HAlign = "left" | "center" | "right";
11
12
  export type VAlign = "top" | "center" | "bottom";
@@ -82,24 +83,31 @@ export interface BoxGlyphs {
82
83
  export const BOX_UNICODE: BoxGlyphs = { tl: "\u256d", tr: "\u256e", bl: "\u2570", br: "\u256f", h: "\u2500", v: "\u2502" };
83
84
  export const BOX_ASCII: BoxGlyphs = { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" };
84
85
 
86
+ /**
87
+ * `paintShadow` (optional) paints the bottom border and right vertical edge —
88
+ * the "shaded" edges — while `paint` keeps the top border and left vertical
89
+ * "lit". The two-tone contrast gives flat boxes a pseudo-3D depth cue.
90
+ */
85
91
  export function boxBlock(
86
92
  lines: string[],
87
93
  width: number,
88
- opts: { glyphs?: BoxGlyphs; paint?: (s: string) => string; align?: HAlign } = {},
94
+ opts: { glyphs?: BoxGlyphs; paint?: (s: string) => string; paintShadow?: (s: string) => string; align?: HAlign } = {},
89
95
  ): string[] {
90
96
  const g = opts.glyphs ?? BOX_UNICODE;
91
97
  const paint = opts.paint ?? ((s: string) => s);
98
+ const shadow = opts.paintShadow ?? paint;
92
99
  const inner = Math.max(0, width - 2);
93
100
  const top = paint(g.tl + g.h.repeat(inner) + g.tr);
94
- const bottom = paint(g.bl + g.h.repeat(inner) + g.br);
101
+ const bottom = shadow(g.bl + g.h.repeat(inner) + g.br);
95
102
  const mid = lines.map(l => {
96
103
  if (l === "DIVIDER") {
97
104
  const leftChar = g.tl === "+" ? "+" : "├";
98
105
  const rightChar = g.tr === "+" ? "+" : "┤";
99
106
  return paint(leftChar + g.h.repeat(inner) + rightChar);
100
107
  }
108
+ const truncated = truncate(l, inner);
101
109
  const align = opts.align ?? "left";
102
- return paint(g.v) + padLineTo(l, inner, align) + paint(g.v);
110
+ return paint(g.v) + padLineTo(truncated, inner, align) + shadow(g.v);
103
111
  });
104
112
  return [top, ...mid, bottom];
105
113
  }
@@ -0,0 +1,108 @@
1
+ import chalk from "chalk";
2
+ import type { PickEntry } from "../../ai/model-picker";
3
+ import type { ProviderName } from "../../ai/types";
4
+ import { catalogMetadata, formatTokens } from "../../ai/model-catalog";
5
+ import { SelectList, renderSelectList, type RenderSelectOptions, type SelectItem } from "./select-list";
6
+
7
+ function liveModelHint(model: string, current?: string): string {
8
+ const meta = catalogMetadata(model);
9
+ const parts: string[] = [];
10
+ if (meta) {
11
+ parts.push(`${formatTokens(meta.contextTokens)} ctx`);
12
+ parts.push(`${formatTokens(meta.maxOutputTokens)} out`);
13
+ parts.push(meta.thinking.length ? meta.thinking.join(",") : "-");
14
+ parts.push(meta.images ? "img" : "text");
15
+ } else {
16
+ parts.push("unknown caps");
17
+ }
18
+ if (current === model) parts.push("current");
19
+ return parts.join(" · ");
20
+ }
21
+
22
+ export interface ModelAssignmentBadge {
23
+ /** Stable target id: "default" or a subagent role id. */
24
+ role: string;
25
+ /** Short badge text shown beside a matching model (DEFAULT, EXECUTOR, ...). */
26
+ label: string;
27
+ /** Configured model id for this target, qualified (`provider/model`) when possible. */
28
+ model: string;
29
+ /** Reasoning budget shown after the badge. Omit for no suffix; use "inherit" for inherited role thinking. */
30
+ thinking?: string;
31
+ /** Visual role color. Unknown values fall back to a neutral badge. */
32
+ color?: "default" | "executor" | "architect" | "planner" | "critic" | string;
33
+ }
34
+
35
+ export interface LiveModelPickerOptions {
36
+ current?: string;
37
+ /** Role/default assignments to render as badges on matching models. */
38
+ assignments?: readonly ModelAssignmentBadge[];
39
+ /** Disable ANSI badge styling for deterministic tests or plain terminals. */
40
+ color?: boolean;
41
+ /** Providers visible for context but not selectable because they cannot serve a turn. */
42
+ disabledProviders?: readonly ProviderName[];
43
+ disabledHint?: string;
44
+ }
45
+ function qualifiedModelId(entry: PickEntry): string {
46
+ return entry.model.includes("/") ? entry.model : `${entry.provider}/${entry.model}`;
47
+ }
48
+
49
+ function modelMatchesAssignment(entry: PickEntry, model: string): boolean {
50
+ const assigned = model.trim().toLowerCase();
51
+ if (!assigned) return false;
52
+ const bare = entry.model.toLowerCase();
53
+ const qualified = qualifiedModelId(entry).toLowerCase();
54
+ return assigned === bare || assigned === qualified;
55
+ }
56
+
57
+ function roleBadgeColor(label: string, color: ModelAssignmentBadge["color"]): string {
58
+ switch (color) {
59
+ case "default":
60
+ return chalk.bgGreen.black(` ${label} `);
61
+ case "executor":
62
+ return chalk.bgRedBright.black(` ${label} `);
63
+ case "architect":
64
+ return chalk.bgHex("#e7c7bd").black(` ${label} `);
65
+ case "planner":
66
+ return chalk.bgYellow.black(` ${label} `);
67
+ case "critic":
68
+ return chalk.bgMagenta.black(` ${label} `);
69
+ default:
70
+ return chalk.bgGray.black(` ${label} `);
71
+ }
72
+ }
73
+
74
+ function assignmentBadges(entry: PickEntry, opts: LiveModelPickerOptions): string[] {
75
+ return (opts.assignments ?? [])
76
+ .filter(a => modelMatchesAssignment(entry, a.model))
77
+ .map(a => {
78
+ const label = a.label.trim().toUpperCase();
79
+ const think = a.thinking ? ` (${a.thinking})` : "";
80
+ return opts.color === false ? `${label}${think}` : `${roleBadgeColor(label, a.color)}${chalk.dim(think)}`;
81
+ });
82
+ }
83
+
84
+
85
+ export function buildLiveModelChoices(entries: PickEntry[], opts: LiveModelPickerOptions = {}): SelectItem<PickEntry>[] {
86
+ const disabled = new Set(opts.disabledProviders ?? []);
87
+ return entries.map(entry => {
88
+ const blocked = disabled.has(entry.provider);
89
+ const badges = assignmentBadges(entry, opts);
90
+ const caps = blocked ? `${liveModelHint(entry.model, opts.current)} · ${opts.disabledHint ?? "provider not ready"}` : liveModelHint(entry.model, opts.current);
91
+ return {
92
+ value: entry,
93
+ label: `#${entry.index} ${qualifiedModelId(entry)}`,
94
+ group: blocked ? `${entry.provider} (not ready)` : entry.provider,
95
+ hint: badges.length ? `${badges.join(" ")} ${opts.color === false ? "· " : chalk.dim("· ")}${caps}` : caps,
96
+ hintRaw: badges.length > 0 && opts.color !== false,
97
+ disabled: blocked,
98
+ };
99
+ });
100
+ }
101
+
102
+ export function liveModelPicker(entries: PickEntry[], opts: LiveModelPickerOptions = {}): SelectList<PickEntry> {
103
+ return new SelectList(buildLiveModelChoices(entries, opts));
104
+ }
105
+
106
+ export function renderLiveModelPicker(list: SelectList<PickEntry>, opts: RenderSelectOptions = {}): string[] {
107
+ return renderSelectList(list, { title: "Select a live model", rows: 12, ...opts });
108
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Render GFM markdown tables in assistant text as box-drawn tables (consensus-seed
3
+ * P2.B8). Pure-TS, width-aware (uses ./width so CJK/emoji columns align). Non-table
4
+ * text passes through untouched; a block is only treated as a table when it has a
5
+ * header row, a `|---|---|` delimiter row, and at least one body row.
6
+ */
7
+ import { visibleWidth, truncateToWidth } from "./width";
8
+
9
+ export interface TableRenderOptions {
10
+ unicode?: boolean;
11
+ /** Hard cap on a column's display width before truncation; keeps wide tables on screen. */
12
+ maxColWidth?: number;
13
+ }
14
+
15
+ /** A line is a table row when it has at least one unescaped `|` and trims to start/contain cells. */
16
+ function isTableRow(line: string): boolean {
17
+ const t = line.trim();
18
+ return t.includes("|") && /\|/.test(t);
19
+ }
20
+
21
+ /** A line is the GFM delimiter row, e.g. `| --- | :--: | ---: |`. */
22
+ function isDelimiterRow(line: string): boolean {
23
+ const t = line.trim().replace(/^\||\|$/g, "");
24
+ if (!t.includes("-")) return false;
25
+ return t.split("|").every(cell => /^\s*:?-{1,}:?\s*$/.test(cell));
26
+ }
27
+
28
+ /** Split a markdown table row into trimmed cell strings (drop the outer pipes). */
29
+ function splitCells(line: string): string[] {
30
+ let t = line.trim();
31
+ if (t.startsWith("|")) t = t.slice(1);
32
+ if (t.endsWith("|")) t = t.slice(0, -1);
33
+ return t.split("|").map(c => c.trim());
34
+ }
35
+
36
+ type Align = "left" | "center" | "right";
37
+
38
+ function alignmentsFrom(delim: string): Align[] {
39
+ return splitCells(delim).map(cell => {
40
+ const c = cell.trim();
41
+ const left = c.startsWith(":");
42
+ const right = c.endsWith(":");
43
+ if (left && right) return "center";
44
+ if (right) return "right";
45
+ return "left";
46
+ });
47
+ }
48
+
49
+ function padCell(text: string, width: number, align: Align): string {
50
+ const w = visibleWidth(text);
51
+ const pad = Math.max(0, width - w);
52
+ if (align === "right") return " ".repeat(pad) + text;
53
+ if (align === "center") {
54
+ const l = Math.floor(pad / 2);
55
+ return " ".repeat(l) + text + " ".repeat(pad - l);
56
+ }
57
+ return text + " ".repeat(pad);
58
+ }
59
+
60
+ /** Full box glyph set incl. T-junctions for grid rules (layout.BoxGlyphs has corners only). */
61
+ interface TableGlyphs {
62
+ h: string; v: string;
63
+ tl: string; tt: string; tr: string;
64
+ ml: string; mm: string; mr: string;
65
+ bl: string; bt: string; br: string;
66
+ }
67
+ const TABLE_UNICODE: TableGlyphs = {
68
+ h: "─", v: "│",
69
+ tl: "┌", tt: "┬", tr: "┐",
70
+ ml: "├", mm: "┼", mr: "┤",
71
+ bl: "└", bt: "┴", br: "┘",
72
+ };
73
+ const TABLE_ASCII: TableGlyphs = {
74
+ h: "-", v: "|",
75
+ tl: "+", tt: "+", tr: "+",
76
+ ml: "+", mm: "+", mr: "+",
77
+ bl: "+", bt: "+", br: "+",
78
+ };
79
+
80
+ /** Render one parsed table (header + rows) to box-drawn lines. */
81
+ function drawTable(header: string[], rows: string[][], aligns: Align[], opts: TableRenderOptions): string[] {
82
+ const g = opts.unicode === false ? TABLE_ASCII : TABLE_UNICODE;
83
+ const cap = Math.max(4, opts.maxColWidth ?? 40);
84
+ const cols = Math.max(header.length, ...rows.map(r => r.length));
85
+ const norm = (r: string[]): string[] => Array.from({ length: cols }, (_, i) => truncateToWidth(r[i] ?? "", cap));
86
+ const h = norm(header);
87
+ const body = rows.map(norm);
88
+ const aligned = Array.from({ length: cols }, (_, i) => aligns[i] ?? "left");
89
+ // Column width = max display width of header + body cells in that column.
90
+ const widths = Array.from({ length: cols }, (_, i) =>
91
+ Math.max(visibleWidth(h[i]!), ...body.map(r => visibleWidth(r[i]!)), 1),
92
+ );
93
+ const rule = (l: string, mid: string, r: string): string =>
94
+ l + widths.map(w => g.h.repeat(w + 2)).join(mid) + r;
95
+ const rowLine = (cells: string[]): string =>
96
+ g.v + cells.map((c, i) => " " + padCell(c, widths[i]!, aligned[i]!) + " ").join(g.v) + g.v;
97
+ return [
98
+ rule(g.tl, g.tt, g.tr),
99
+ rowLine(h),
100
+ rule(g.ml, g.mm, g.mr),
101
+ ...body.map(rowLine),
102
+ rule(g.bl, g.bt, g.br),
103
+ ];
104
+ }
105
+
106
+ /**
107
+ * Replace every GFM table block in `text` with a box-drawn table. Returns the text
108
+ * with tables rendered; all non-table lines are preserved verbatim.
109
+ */
110
+ export function renderMarkdownTables(text: string, opts: TableRenderOptions = {}): string {
111
+ if (!text.includes("|")) return text;
112
+ const lines = text.split("\n");
113
+ const out: string[] = [];
114
+ let i = 0;
115
+ while (i < lines.length) {
116
+ // A table needs: header row, delimiter row, ≥1 body row.
117
+ if (
118
+ i + 1 < lines.length &&
119
+ isTableRow(lines[i]!) &&
120
+ isDelimiterRow(lines[i + 1]!) &&
121
+ i + 2 < lines.length &&
122
+ isTableRow(lines[i + 2]!)
123
+ ) {
124
+ const header = splitCells(lines[i]!);
125
+ const aligns = alignmentsFrom(lines[i + 1]!);
126
+ const rows: string[][] = [];
127
+ let j = i + 2;
128
+ while (j < lines.length && isTableRow(lines[j]!) && !isDelimiterRow(lines[j]!)) {
129
+ rows.push(splitCells(lines[j]!));
130
+ j++;
131
+ }
132
+ for (const line of drawTable(header, rows, aligns, opts)) out.push(line);
133
+ i = j;
134
+ continue;
135
+ }
136
+ out.push(lines[i]!);
137
+ i++;
138
+ }
139
+ return out.join("\n");
140
+ }
@@ -0,0 +1,97 @@
1
+ import chalk from "chalk";
2
+ /**
3
+ * Strip markdown formatting from text, returning plain text.
4
+ */
5
+ export function stripMarkdown(text: string): string {
6
+ if (!text) return "";
7
+
8
+ let out = text;
9
+
10
+ // 1. Remove code block fences (e.g. ```ts or ```)
11
+ out = out.replace(/^```[a-zA-Z0-9_-]*\s*$/gm, "");
12
+
13
+ // 2. Remove inline code backticks
14
+ out = out.replace(/`([^`]+)`/g, "$1");
15
+
16
+ // 3. Remove headers (e.g. # Header -> Header)
17
+ out = out.replace(/^#+\s+(.+)$/gm, "$1");
18
+
19
+ // 4. Remove bold/italic markers
20
+ out = out.replace(/\*\*\*([^\*]+)\*\*\*/g, "$1");
21
+ out = out.replace(/\*\*([^\*]+)\*\*/g, "$1");
22
+ out = out.replace(/\*([^\*]+)\*/g, "$1");
23
+ out = out.replace(/___([^\_]+)___/g, "$1");
24
+ out = out.replace(/__([^\_]+)__/g, "$1");
25
+ out = out.replace(/_([^\_]+)_/g, "$1");
26
+
27
+ // 5. Convert links [text](url) -> text (url)
28
+ out = out.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, "$1 ($2)");
29
+
30
+ // 6. Convert images ![alt](url) -> alt
31
+ out = out.replace(/!\[([^\]]*)\]\(([^\)]+)\)/g, "$1");
32
+
33
+ // 7. Remove blockquote markers (> text -> text)
34
+ out = out.replace(/^>\s+(.+)$/gm, "$1");
35
+
36
+ // 8. Remove horizontal rules
37
+ out = out.replace(/^[-\*_]{3,}\s*$/gm, "");
38
+
39
+ return out.trim();
40
+ }
41
+
42
+ export interface MarkdownAnsiOptions {
43
+ /** Heading painter (theme accent + bold); default chalk.bold. */
44
+ accent?: (s: string) => string;
45
+ }
46
+
47
+ /**
48
+ * Render markdown as styled ANSI text (jeo-ref final-report format): headings
49
+ * become painted section titles, **bold** / `inline code` keep visual weight,
50
+ * fences/links/quotes degrade exactly like stripMarkdown. Fenced code BODIES are
51
+ * passed through untouched (only the ``` fence rows are dropped) so code samples
52
+ * are never reformatted. Pure + stateless — safe for the one-shot finish path.
53
+ */
54
+ export function renderMarkdownAnsi(text: string, opts: MarkdownAnsiOptions = {}): string {
55
+ if (!text) return "";
56
+ const accent = opts.accent ?? ((s: string) => chalk.bold(s));
57
+
58
+ // Links/images FIRST: bold/code styling injects ANSI escapes whose `[` would
59
+ // otherwise be swallowed by the `[label](url)` matcher (corrupting both).
60
+ const styleInline = (line: string): string =>
61
+ line
62
+ .replace(/!\[([^\]]*)\]\(([^\)]+)\)/g, "$1")
63
+ .replace(/\[([^\]]+)\]\(([^\)]+)\)/g, (_m, label: string, url: string) => `${label} ${chalk.dim(`(${url})`)}`)
64
+ .replace(/`([^`]+)`/g, (_m, code: string) => chalk.cyan(code))
65
+ .replace(/\*\*\*([^\*]+)\*\*\*/g, (_m, t: string) => chalk.bold.italic(t))
66
+ .replace(/\*\*([^\*]+)\*\*/g, (_m, t: string) => chalk.bold(t))
67
+ .replace(/__([^\_]+)__/g, (_m, t: string) => chalk.bold(t));
68
+
69
+ const out: string[] = [];
70
+ let inFence = false;
71
+ for (const line of text.split("\n")) {
72
+ if (/^```/.test(line.trim())) {
73
+ inFence = !inFence;
74
+ continue; // drop the fence row itself (stripMarkdown parity)
75
+ }
76
+ if (inFence) {
77
+ out.push(line); // code bodies verbatim — never styled or reflowed
78
+ continue;
79
+ }
80
+ const heading = line.match(/^(#{1,6})\s+(.+)$/);
81
+ if (heading) {
82
+ out.push(accent(styleInline(heading[2]!)));
83
+ continue;
84
+ }
85
+ const quote = line.match(/^>\s+(.+)$/);
86
+ if (quote) {
87
+ out.push(chalk.dim(`▎ ${styleInline(quote[1]!)}`));
88
+ continue;
89
+ }
90
+ if (/^[-\*_]{3,}\s*$/.test(line)) {
91
+ out.push(chalk.dim("─".repeat(24)));
92
+ continue;
93
+ }
94
+ out.push(styleInline(line));
95
+ }
96
+ return out.join("\n").trim();
97
+ }
@@ -4,6 +4,8 @@ import { size } from "../terminal";
4
4
  export interface MeterOptions {
5
5
  /** Use ASCII-only glyphs for terminals without unicode (default true = unicode). */
6
6
  unicode?: boolean;
7
+ /** Whether to render color (default true). */
8
+ color?: boolean;
7
9
  }
8
10
 
9
11
  /** Horizontal evolutionary percent/progress meter for pipeline + doctor TUI views. */
@@ -19,7 +21,8 @@ export function meter(value: number, max = 1, width?: number, opts: MeterOptions
19
21
  const cells = width !== undefined ? Math.max(0, Math.trunc(width)) : Math.max(10, Math.min(40, size().cols - 30));
20
22
  const filledCount = Math.round(ratio * cells);
21
23
  const { fill, empty, color } = meterGlyphsFor(stageIndexForRatio(ratio), opts.unicode !== false);
22
- const bar = color(fill.repeat(filledCount)) + empty.repeat(cells - filledCount);
24
+ const paint = opts.color !== false ? color : (s: string) => s;
25
+ const bar = paint(fill.repeat(filledCount)) + empty.repeat(cells - filledCount);
23
26
  return `[${bar}] ${Math.round(ratio * 100)}%`;
24
27
  }
25
28
 
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { SelectList, renderSelectList, type SelectItem, type RenderSelectOptions } from "./select-list";
8
8
  import { catalogForProvider, type ModelCatalogEntry } from "../../ai/model-catalog-compat";
9
+ import { companyLabel } from "../../ai/model-catalog";
9
10
  import { PROVIDER_NAMES, type ProviderStatus } from "../../ai/provider-status";
10
11
  import type { ProviderName } from "../../ai/types";
11
12
 
@@ -58,11 +59,11 @@ export function buildModelChoices(statuses: ProviderStatus[], opts: ModelPickerO
58
59
  for (const provider of providers) {
59
60
  const ready = !!readyOf.get(provider);
60
61
  if (!ready && !includeUnready) continue;
61
- const group = `${provider}${ready ? "" : " (no credential)"}`;
62
+ const group = `${provider}${companyLabel(provider)}${ready ? "" : " (no credential)"}`;
62
63
  for (const entry of catalogForProvider(provider)) {
63
64
  items.push({
64
65
  value: entry.id,
65
- label: entry.id,
66
+ label: `${entry.id} (${companyLabel(provider, entry)})`,
66
67
  group,
67
68
  hint: modelHint(entry, ready, unicode),
68
69
  });