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
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Provider picker — turns live provider readiness into a `SelectList` for the
3
- * TUI `/provider` and `joc setup` flows. Ready providers are listed first and
3
+ * TUI `/provider` and `jeo setup` flows. Ready providers are listed first and
4
4
  * the recommended choice is the first ready provider. Pure builders.
5
5
  */
6
6
  import { SelectList, renderSelectList, type SelectItem, type RenderSelectOptions } from "./select-list";
7
7
  import type { ProviderStatus } from "../../ai/provider-status";
8
8
  import type { ProviderName } from "../../ai/types";
9
+ import { companyLabel } from "../../ai/model-catalog";
9
10
 
10
11
  /** Right-aligned hint for a provider row: credential kind + base URL + readiness. */
11
12
  export function providerHint(s: ProviderStatus, unicode = true): string {
@@ -20,7 +21,7 @@ export function buildProviderChoices(statuses: ProviderStatus[], unicode = true)
20
21
  const sorted = [...statuses].sort((a, b) => (a.ready === b.ready ? 0 : a.ready ? -1 : 1));
21
22
  return sorted.map(s => ({
22
23
  value: s.name,
23
- label: s.name,
24
+ label: `${s.name} (${companyLabel(s.name)})`,
24
25
  group: s.ready ? "ready" : "needs setup",
25
26
  hint: providerHint(s, unicode),
26
27
  }));
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Section layout primitives — a small shadcn/ui-inspired design-token layer for the
3
+ * terminal TUI. shadcn's discipline is *consistent vertical rhythm* (`space-y-*`) and
4
+ * *card grouping* with a muted header (`CardHeader` + `text-muted-foreground`). The
5
+ * gjc reference TUI does the same with `├─ Output ─┤` style dividers between a tool
6
+ * block and its output.
7
+ *
8
+ * These helpers give the live frame one shared spacing token (`SECTION_GAP`) and a
9
+ * muted card-header divider (`sectionLabel`) so the in-frame stages (plan / activity /
10
+ * output) read as distinct, evenly-spaced cards instead of one cramped wall of lines.
11
+ */
12
+ import chalk from "chalk";
13
+ import { visibleWidth } from "./color";
14
+
15
+ /** Vertical rhythm: blank lines inserted between adjacent sections (shadcn `space-y`). */
16
+ export const SECTION_GAP = 1;
17
+
18
+ export interface SectionLabelOpts {
19
+ color?: boolean;
20
+ unicode?: boolean;
21
+ }
22
+
23
+ /**
24
+ * A muted card-header divider spanning `width` columns: `── Plan ─────────────`.
25
+ * Mirrors shadcn's `CardHeader` (a muted title) and gjc's `├─ Output ─┤` separators,
26
+ * so each stage block is announced by a low-contrast label rather than running into
27
+ * the previous block.
28
+ */
29
+ export function sectionLabel(title: string, width: number, opts: SectionLabelOpts = {}): string {
30
+ const unicode = opts.unicode !== false;
31
+ const dash = unicode ? "─" : "-";
32
+ const head = `${dash.repeat(2)} ${title.trim()} `;
33
+ const headW = visibleWidth(head);
34
+ const fill = Math.max(0, Math.trunc(width) - headW);
35
+ const line = head + dash.repeat(fill);
36
+ return opts.color !== false ? chalk.dim(line) : line;
37
+ }
38
+
39
+ export interface Section {
40
+ /** Optional muted card header. Omit for self-headed blocks (plan/forge already label themselves). */
41
+ title?: string;
42
+ lines: string[];
43
+ }
44
+
45
+ export interface StackOptions {
46
+ width: number;
47
+ gap?: number;
48
+ color?: boolean;
49
+ unicode?: boolean;
50
+ }
51
+
52
+ /**
53
+ * Stack sections with a consistent blank-line rhythm. Empty sections are dropped, a
54
+ * titled section is prefixed with its muted `sectionLabel`, and exactly `gap` blank
55
+ * lines separate adjacent (non-empty) sections. Returns a flat line array ready to
56
+ * drop into the frame.
57
+ */
58
+ export function stackSections(sections: Section[], opts: StackOptions): string[] {
59
+ const gap = Math.max(0, opts.gap ?? SECTION_GAP);
60
+ const out: string[] = [];
61
+ for (const section of sections) {
62
+ if (!section.lines.length) continue;
63
+ if (out.length) for (let i = 0; i < gap; i++) out.push("");
64
+ if (section.title) {
65
+ out.push(sectionLabel(section.title, opts.width, { color: opts.color, unicode: opts.unicode }));
66
+ }
67
+ for (const line of section.lines) out.push(line);
68
+ }
69
+ return out;
70
+ }
@@ -10,6 +10,7 @@
10
10
  import chalk from "chalk";
11
11
  import { padLineTo } from "./layout";
12
12
  import { visibleWidth } from "./color";
13
+ import { truncate as truncateAnsi } from "../terminal";
13
14
 
14
15
  export interface SelectItem<T> {
15
16
  value: T;
@@ -20,6 +21,12 @@ export interface SelectItem<T> {
20
21
  disabled?: boolean;
21
22
  /** Optional right-aligned hint/badge (e.g. "✓ ready · 200k"). */
22
23
  hint?: string;
24
+ /** When true, `hint` is already styled and should not be wrapped in gray. */
25
+ hintRaw?: boolean;
26
+ /** Tree indentation depth for nested/sub-list choices. */
27
+ depth?: number;
28
+ /** Tree connector for nested rows; defaults to "mid" when depth > 0. */
29
+ branch?: "mid" | "last";
23
30
  }
24
31
 
25
32
  export class SelectList<T> {
@@ -145,11 +152,17 @@ export function renderSelectList<T>(list: SelectList<T>, opts: RenderSelectOptio
145
152
  const tint = (s: string, fn: (x: string) => string) => (color ? fn(s) : s);
146
153
 
147
154
  const out: string[] = [];
148
- if (opts.title) out.push(tint(opts.title, chalk.bold));
155
+ if (opts.title) {
156
+ for (const rawTitle of opts.title.split("\n")) {
157
+ const title = rawTitle ? tint(rawTitle, chalk.bold) : "";
158
+ out.push(cols ? clampToCols(title, cols) : title);
159
+ }
160
+ }
149
161
 
150
162
  const items = list.visible();
151
163
  if (items.length === 0) {
152
- out.push(tint(" (no matches)", chalk.gray));
164
+ const empty = tint(" (no matches)", chalk.gray);
165
+ out.push(cols ? clampToCols(empty, cols) : empty);
153
166
  } else {
154
167
  const cur = list.cursorIndex();
155
168
  // Scrolling window centered-ish on the cursor.
@@ -157,20 +170,25 @@ export function renderSelectList<T>(list: SelectList<T>, opts: RenderSelectOptio
157
170
  start = Math.min(start, Math.max(0, items.length - rows));
158
171
  const end = Math.min(items.length, start + rows);
159
172
 
160
- if (start > 0) out.push(tint(` \u2191 ${start} more`, chalk.gray));
173
+ if (start > 0) {
174
+ const more = tint(` \u2191 ${start} more`, chalk.gray);
175
+ out.push(cols ? clampToCols(more, cols) : more);
176
+ }
161
177
  let lastGroup: string | undefined;
162
178
  for (let i = start; i < end; i++) {
163
179
  const it = items[i]!;
164
180
  if (it.group && it.group !== lastGroup) {
165
- out.push(tint(` ${it.group}`, chalk.gray));
181
+ const group = tint(` ${it.group}`, chalk.gray);
182
+ out.push(cols ? clampToCols(group, cols) : group);
166
183
  lastGroup = it.group;
167
184
  }
168
185
  const isCur = i === cur;
169
186
  const marker = isCur ? tint(pointer, chalk.cyan) : " ";
170
- let label = it.disabled ? tint(it.label, chalk.gray) : isCur ? tint(it.label, chalk.cyan.bold) : it.label;
187
+ const prefix = treePrefix(it, { unicode });
188
+ let label = it.disabled ? tint(`${prefix}${it.label}`, chalk.gray) : isCur ? tint(`${prefix}${it.label}`, chalk.cyan.bold) : `${prefix}${it.label}`;
171
189
  let line = `${marker} ${label}`;
172
190
  if (it.hint) {
173
- const hint = tint(it.hint, chalk.gray);
191
+ const hint = it.hintRaw ? it.hint : tint(it.hint, chalk.gray);
174
192
  if (cols) {
175
193
  // right-align the hint within cols
176
194
  const used = visibleWidth(line) + visibleWidth(hint) + 1;
@@ -182,18 +200,30 @@ export function renderSelectList<T>(list: SelectList<T>, opts: RenderSelectOptio
182
200
  }
183
201
  out.push(cols ? clampToCols(line, cols) : line);
184
202
  }
185
- if (end < items.length) out.push(tint(` \u2193 ${items.length - end} more`, chalk.gray));
203
+ if (end < items.length) {
204
+ const more = tint(` \u2193 ${items.length - end} more`, chalk.gray);
205
+ out.push(cols ? clampToCols(more, cols) : more);
206
+ }
186
207
  }
187
208
 
188
209
  const q = list.filter();
189
210
  const filterPart = q ? `filter: ${q}` : "type to filter";
190
211
  const keys = unicode ? "\u2191/\u2193 move \u00b7 enter select \u00b7 esc cancel" : "up/down move . enter select . esc cancel";
191
- out.push(tint(` ${filterPart} \u2014 ${keys}`, chalk.gray));
212
+ const footer = tint(` ${filterPart} \u2014 ${keys}`, chalk.gray);
213
+ out.push(cols ? clampToCols(footer, cols) : footer);
192
214
  return out;
193
215
  }
194
216
 
195
- /** Fit a (possibly colored) line to cols by visible width, preserving the right hint. */
196
217
  function clampToCols(line: string, cols: number): string {
197
218
  if (visibleWidth(line) <= cols) return padLineTo(line, cols, "left");
198
- return line; // caller-built hint lines are already gap-fitted; leave longer plain lines to the renderer truncate
219
+ const cut = truncateAnsi(line, cols);
220
+ return visibleWidth(cut) <= cols ? padLineTo(cut, cols, "left") : cut;
221
+ }
222
+
223
+ function treePrefix<T>(item: SelectItem<T>, opts: { unicode: boolean }): string {
224
+ const depth = Math.max(0, item.depth ?? 0);
225
+ if (depth <= 0) return "";
226
+ const pad = " ".repeat(Math.max(0, depth - 1));
227
+ if (!opts.unicode) return `${pad}${item.branch === "last" ? "`-" : "|-"} `;
228
+ return `${pad}${item.branch === "last" ? "└─" : "├─"} `;
199
229
  }
@@ -0,0 +1,25 @@
1
+ import type { SkillDoc } from "../../skills/catalog";
2
+ import { skillSlashAliases } from "../../skills/catalog";
3
+ import { SelectList, renderSelectList, type RenderSelectOptions, type SelectItem } from "./select-list";
4
+
5
+ function skillHint(skill: SkillDoc): string {
6
+ const aliases = skillSlashAliases(skill);
7
+ return aliases.length ? aliases.slice(0, 3).join(" ") : skill.command;
8
+ }
9
+
10
+ export function buildSkillChoices(skills: SkillDoc[]): SelectItem<SkillDoc>[] {
11
+ return skills.map(skill => ({
12
+ value: skill,
13
+ label: skill.name,
14
+ group: "skills",
15
+ hint: skillHint(skill),
16
+ }));
17
+ }
18
+
19
+ export function skillPicker(skills: SkillDoc[]): SelectList<SkillDoc> {
20
+ return new SelectList(buildSkillChoices(skills));
21
+ }
22
+
23
+ export function renderSkillPicker(list: SelectList<SkillDoc>, opts: RenderSelectOptions = {}): string[] {
24
+ return renderSelectList(list, { title: "Select a skill", rows: 12, ...opts });
25
+ }
@@ -1,34 +1,257 @@
1
1
  /** Slash-command palette/autocomplete for the interactive REPL (TUI M3). */
2
+ import chalk from "chalk";
2
3
 
3
- export const SLASH_COMMANDS = [
4
- "/help",
5
- "/clear",
6
- "/compact",
7
- "/model",
8
- "/models",
9
- "/provider",
10
- "/agents",
11
- "/config",
12
- "/roles",
13
- "/thinking",
14
- "/view",
15
- "/diff",
16
- "/find",
17
- "/search",
18
- "/sessions",
19
- "/evolve",
20
- "/exit",
21
- "/quit",
4
+ /** Order-preserving subsequence test: every char of `needle` appears in `hay`
5
+ * left-to-right (gjc-style fuzzy match, e.g. "expt" ⊑ "export"). */
6
+ function subsequence(needle: string, hay: string): boolean {
7
+ if (needle === "") return true;
8
+ let i = 0;
9
+ for (let j = 0; j < hay.length && i < needle.length; j++) {
10
+ if (hay[j] === needle[i]) i++;
11
+ }
12
+ return i === needle.length;
13
+ }
14
+
15
+ export interface SlashCommandInfo {
16
+ command: string;
17
+ usage: string;
18
+ description: string;
19
+ group: "session" | "models" | "subagents" | "code" | "skills" | "system";
20
+ }
21
+
22
+ export const SLASH_COMMAND_DETAILS: readonly SlashCommandInfo[] = [
23
+ { command: "/help", usage: "/help", description: "Show this help message", group: "system" },
24
+ { command: "/clear", usage: "/clear", description: "Clear conversation history (keeps system prompt)", group: "session" },
25
+ { command: "/new", usage: "/new", description: "Start a new session (fresh history + new session id)", group: "session" },
26
+ { command: "/drop", usage: "/drop", description: "Delete the current session and start a new one", group: "session" },
27
+ { command: "/session", usage: "/session [info|delete]", description: "Show current session info, or delete it", group: "session" },
28
+ { command: "/rename", usage: "/rename <title>", description: "Rename the current session", group: "session" },
29
+ { command: "/resume", usage: "/resume [id]", description: "Resume a saved session in this REPL", group: "session" },
30
+ { command: "/retry", usage: "/retry", description: "Retry the last request", group: "session" },
31
+ { command: "/history", usage: "/history [n|all]", description: "Re-print the worked turn history (prompts, tool steps, replies) into scrollback", group: "session" },
32
+ { command: "/export", usage: "/export [path] [json]", description: "Export the session transcript to a file", group: "session" },
33
+ { command: "/dump", usage: "/dump", description: "Copy the session transcript to the clipboard", group: "session" },
34
+ { command: "/btw", usage: "/btw <question>", description: "Ask an ephemeral side question (history untouched)", group: "session" },
35
+ { command: "/compact", usage: "/compact", description: "Summarize older turns to free context", group: "session" },
36
+ { command: "/model", usage: "/model [id|#N|save|thinking <level>|subagent <role> <model|#N|thinking L>]", description: "Show/switch model; picker can apply to default or any subagent role and set thinking", group: "models" },
37
+ { command: "/fast", usage: "/fast [on|off|status]", description: "Toggle fast thinking mode when the active model supports it", group: "models" },
38
+ { command: "/provider", usage: "/provider [name] [model|#N]", description: "Credentials, switch provider, set model; `login <name>` starts OAuth", group: "models" },
39
+ { command: "/login", usage: "/login [provider]", description: "OAuth login (alias of /provider login)", group: "models" },
40
+ { command: "/logout", usage: "/logout <anthropic|openai|gemini|antigravity>", description: "Remove the stored OAuth token for a provider", group: "models" },
41
+ { command: "/roles", usage: "/roles [tier model]", description: "Show or set model role tiers (smol/slow/plan)", group: "models" },
42
+ { command: "/thinking", usage: "/thinking [level]", description: "Show or set thinking budget (minimal/low/medium/high/xhigh)", group: "models" },
43
+ { command: "/agents", usage: "/agents [edit|role] [model|#N|thinking L|maxSteps N|reset]", description: "List subagent roles; use /agents edit for the interactive picker or pin role model/settings", group: "subagents" },
44
+ { command: "/subagent", usage: "/subagent [role]", description: "Show the current subagent composition (per-role model · thinking · steps); alias of /agents", group: "subagents" },
45
+ { command: "/view", usage: "/view <file> [a-b]", description: "Render a file with line numbers + light highlight", group: "code" },
46
+ { command: "/diff", usage: "/diff [file]", description: "Render `git diff` with +/- coloring", group: "code" },
47
+ { command: "/find", usage: "/find <glob>", description: "List files matching a glob", group: "code" },
48
+ { command: "/search", usage: "/search <pat> [glob]", description: "Search the repo for a pattern", group: "code" },
49
+ { command: "/skill", usage: "/skill [name [intent]]", description: "List, show, or run a workflow skill", group: "skills" },
50
+ { command: "/skill:", usage: "/skill:<name> [intent]", description: "Run a workflow skill by GJC-style entrypoint", group: "skills" },
51
+ { command: "/sessions", usage: "/sessions", description: "List saved sessions", group: "session" },
52
+ { command: "/usage", usage: "/usage", description: "Show cumulative token usage for this session", group: "system" },
53
+ { command: "/context", usage: "/context", description: "Show context token usage breakdown", group: "system" },
54
+ { command: "/tools", usage: "/tools", description: "Show the tools currently visible to the agent", group: "system" },
55
+ { command: "/hotkeys", usage: "/hotkeys", description: "Show keyboard shortcuts", group: "system" },
56
+ { command: "/theme", usage: "/theme [name]", description: "Show or set the TUI theme (cosmic/matrix/solar/mono)", group: "system" },
57
+ { command: "/settings", usage: "/settings", description: "Show effective runtime configuration (alias of /config)", group: "system" },
58
+ { command: "/evolve", usage: "/evolve", description: "Simulate and view the agent's evolutionary gallery", group: "system" },
59
+ { command: "/config", usage: "/config", description: "Show the effective runtime configuration", group: "system" },
60
+ { command: "/exit", usage: "/exit", description: "Exit the agent", group: "system" },
61
+ { command: "/quit", usage: "/quit", description: "Exit the agent", group: "system" },
22
62
  ];
23
63
 
24
- /** Return the slash commands that prefix-match `input` (case-insensitive). Empty for non-slash input. */
64
+ export const SLASH_COMMANDS = SLASH_COMMAND_DETAILS.map(c => c.command);
65
+
66
+ export function mergeSlashCommandDetails(extra: readonly SlashCommandInfo[] = []): SlashCommandInfo[] {
67
+ const byCommand = new Map<string, SlashCommandInfo>();
68
+ for (const d of [...SLASH_COMMAND_DETAILS, ...extra]) byCommand.set(d.command, d);
69
+ return [...byCommand.values()];
70
+ }
71
+
72
+ /** Return the slash commands that match `input` (case-insensitive). Prefix matches
73
+ * come first; a fuzzy subsequence fallback (gjc-style, e.g. `/expt` → `/export`)
74
+ * follows so typos / abbreviations still surface a command. A bare `/` lists every
75
+ * command. Empty for non-slash input. */
25
76
  export function matchSlash(input: string, commands: string[] = SLASH_COMMANDS): string[] {
26
77
  if (!input.startsWith("/")) return [];
27
78
  const q = input.toLowerCase();
28
- return commands.filter(c => c.startsWith(q));
79
+ const body = q.slice(1);
80
+ if (body === "") return [...commands];
81
+ const starts = commands.filter(c => c.toLowerCase().startsWith(q));
82
+ const fuzzy = commands.filter(c => !starts.includes(c) && subsequence(body, c.slice(1).toLowerCase()));
83
+ return [...starts, ...fuzzy];
29
84
  }
30
85
 
31
86
  /** True when `input` looks like a slash command (starts with "/" and has no space). */
32
87
  export function isSlashAttempt(input: string): boolean {
33
88
  return input.startsWith("/") && !input.slice(1).includes(" ");
34
89
  }
90
+
91
+ const GROUP_LABELS: Record<SlashCommandInfo["group"], string> = {
92
+ session: "Session",
93
+ models: "Models / Providers",
94
+ subagents: "Subagents",
95
+ code: "Code tools",
96
+ skills: "Skills",
97
+ system: "System",
98
+ };
99
+
100
+ const GROUP_ORDER: readonly SlashCommandInfo["group"][] = ["models", "subagents", "code", "skills", "session", "system"];
101
+ /** Format a visible command palette for `/`, `/help`, or a partial slash prefix. */
102
+ export function formatSlashCommandList(input = "/", extra: readonly SlashCommandInfo[] = []): string[] {
103
+ const details = mergeSlashCommandDetails(extra);
104
+ const commands = details.map(c => c.command);
105
+ const query = input === "/?" ? "/" : input;
106
+ const matches = matchSlash(query, commands);
107
+ if (matches.length === 0) return [`Unknown command '${input}'. Try /help.`];
108
+ const wanted = new Set(matches);
109
+ const rows = details.filter(c => wanted.has(c.command));
110
+ const usageWidth = Math.max(...rows.map(c => c.usage.length), 6);
111
+ const title = query === "/" || query === "/help"
112
+ ? "Slash Commands:"
113
+ : `Slash Commands matching '${query}':`;
114
+ const lines = [title];
115
+ for (const group of GROUP_ORDER) {
116
+ const groupRows = rows.filter(c => c.group === group);
117
+ if (groupRows.length === 0) continue;
118
+ lines.push(` ${GROUP_LABELS[group]}:`);
119
+ for (const c of groupRows) lines.push(` ${c.usage.padEnd(usageWidth)} - ${c.description}`);
120
+ }
121
+ lines.push("Tip: type a slash prefix like '/mod' and press Tab to autocomplete.");
122
+ return lines;
123
+ }
124
+
125
+ export interface SkillPreviewItem {
126
+ name: string;
127
+ summary?: string;
128
+ }
129
+
130
+ function renderPreviewRows(
131
+ rows: { usage: string; description: string }[],
132
+ max: number,
133
+ selected: number,
134
+ ): string[] {
135
+ const fmt = (r: { usage: string; description: string }, on: boolean): string => {
136
+ const head = on ? chalk.cyan(`▸ ${r.usage}`) : ` ${r.usage}`;
137
+ return `${head} ${chalk.dim(r.description)}`;
138
+ };
139
+ const n = rows.length;
140
+ if (n === 0) return [];
141
+ // HARD row-budget contract: the returned lines NEVER exceed `max` — the caller
142
+ // reserves exactly that many footer rows, and one extra line shifts the input
143
+ // box / caret math (the "broken input box" corruption). When the list overflows,
144
+ // up to two slots are spent on ↑/↓ "more" markers INSIDE the budget.
145
+ const overflowing = n > max;
146
+ const slots = overflowing ? Math.max(1, max - 2) : n;
147
+ const sel = selected < 0 ? 0 : Math.min(selected, n - 1);
148
+ const start = Math.max(0, Math.min(sel - Math.floor(slots / 2), n - slots));
149
+ const lines: string[] = [];
150
+ // Position counter (1-based) of the selected row within the full match list,
151
+ // shown on a "more" marker so an overflowing list reads like gjc's `(3/33)`.
152
+ // When nothing is selected, the window starts at the top → `(1/total)`.
153
+ const counter = `(${sel + 1}/${n})`;
154
+ const below = n - (start + slots);
155
+ if (start > 0) lines.push(below > 0 ? ` ↑ ${start} more` : ` ↑ ${start} more ${counter}`);
156
+ for (let i = start; i < start + slots && i < n; i++) lines.push(fmt(rows[i]!, i === selected));
157
+ if (below > 0) lines.push(` ↓ ${below} more ${counter}`);
158
+ return lines;
159
+ }
160
+
161
+ /** Skills matching a `$keyword` probe (no space yet). Prefix matches come first,
162
+ * then a fuzzy subsequence fallback (gjc-style, e.g. `$dintv` → `$deep-interview`).
163
+ * A bare `$` lists every skill. */
164
+ function dollarMatches(trimmed: string, skills: readonly SkillPreviewItem[]): SkillPreviewItem[] {
165
+ const prefix = trimmed.slice(1).toLowerCase();
166
+ if (prefix === "") return [...skills];
167
+ const starts = skills.filter(s => s.name.toLowerCase().startsWith(prefix));
168
+ const fuzzy = skills.filter(s => !starts.includes(s) && subsequence(prefix, s.name.toLowerCase()));
169
+ return [...starts, ...fuzzy];
170
+ }
171
+
172
+ /** The `/command` or `$skill` token actively being typed, ANYWHERE in the line. */
173
+ export interface ActiveTrigger {
174
+ kind: "/" | "$";
175
+ /** The trigger token itself (`/mo`, `$te`, bare `/`, bare `$`, …). */
176
+ token: string;
177
+ /** Index of the token's first character in `line` — for in-place replacement. */
178
+ start: number;
179
+ }
180
+
181
+ /**
182
+ * Find the trigger token the user is typing, regardless of where it sits in the
183
+ * line (mention-style): the LAST whitespace-delimited word, when it starts with
184
+ * `/` or `$` and the caret is still on it (the word ends the line — no space
185
+ * after it yet). `"fix the bug /mo"` → `/mo`, `"explain $te"` → `$te`,
186
+ * `"/model"` → `/model`. No trigger once a space follows (`"/model gpt"` →
187
+ * the active word is `gpt`), and never for `/`/`$` glued inside a word
188
+ * (`"src/cli"`, `"FOO$BAR"` — paths/vars stay popup-free).
189
+ */
190
+ export function activeTriggerToken(line: string): ActiveTrigger | undefined {
191
+ const m = /(^|\s)([/$]\S*)$/.exec(line);
192
+ if (!m) return undefined;
193
+ const token = m[2]!;
194
+ return { kind: token[0] as "/" | "$", token, start: (m.index ?? 0) + m[1]!.length };
195
+ }
196
+
197
+ /**
198
+ * Compact live preview shown beneath the input box while a `/command` or
199
+ * `$skill` keyword is being typed — at any position in the line (mention-style,
200
+ * gjc/Codex parity): `"do X then /mo"` previews commands, `"…then $te"`
201
+ * previews skills. Returns matching usages + descriptions, capped, or [] when
202
+ * no trigger token is active (finished words stay popup-free). Match ORDER is
203
+ * preserved from matchSlash/dollarMatches so prefix hits sit above fuzzy hits.
204
+ */
205
+ export function formatSlashPreview(
206
+ line: string,
207
+ max = 6,
208
+ selected = -1,
209
+ extra: readonly SlashCommandInfo[] = [],
210
+ skills: readonly SkillPreviewItem[] = [],
211
+ ): string[] {
212
+ const trigger = activeTriggerToken(line);
213
+ if (!trigger) return [];
214
+ if (trigger.kind === "$") {
215
+ const rows = dollarMatches(trigger.token, skills).map(s => ({
216
+ usage: `$${s.name} [intent]`,
217
+ description: s.summary?.trim() || "run this skill directly",
218
+ }));
219
+ return renderPreviewRows(rows, max, selected);
220
+ }
221
+ const details = mergeSlashCommandDetails(extra);
222
+ const byCommand = new Map(details.map(c => [c.command, c] as const));
223
+ const rows = matchSlash(trigger.token, details.map(c => c.command))
224
+ .map(cmd => byCommand.get(cmd))
225
+ .filter((c): c is SlashCommandInfo => Boolean(c));
226
+ if (rows.length === 0) return [];
227
+ return renderPreviewRows(rows, max, selected);
228
+ }
229
+
230
+ /** The matching command names for an active `/cmd` or `$name` trigger token
231
+ * (anywhere in the line), in display order (prefix-first, then fuzzy). */
232
+ export function slashPreviewMatches(
233
+ line: string,
234
+ extra: readonly SlashCommandInfo[] = [],
235
+ skills: readonly SkillPreviewItem[] = [],
236
+ ): string[] {
237
+ const trigger = activeTriggerToken(line);
238
+ if (!trigger) return [];
239
+ if (trigger.kind === "$") return dollarMatches(trigger.token, skills).map(s => `$${s.name}`);
240
+ const details = mergeSlashCommandDetails(extra);
241
+ return matchSlash(trigger.token, details.map(c => c.command));
242
+ }
243
+
244
+ /**
245
+ * Tab-completion target for the live `/`·`$` popup: the highlighted row when the
246
+ * user arrowed to one, else the TOP match (prefix hits sort first). Returns the
247
+ * completed line WITH a trailing space — arguments follow, and the space closes
248
+ * the keyword popup (a space means a real invocation is being typed). Pure.
249
+ */
250
+ export function tabCompleteSelection(line: string, matches: readonly string[], selected: number): string | undefined {
251
+ if (matches.length === 0) return undefined;
252
+ const trigger = activeTriggerToken(line);
253
+ if (!trigger) return undefined;
254
+ const pick = matches[selected >= 0 && selected < matches.length ? selected : 0]!;
255
+ return line.slice(0, trigger.start) + `${pick} `;
256
+ }
257
+