palabre 0.9.1 → 0.10.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.
Files changed (51) hide show
  1. package/dist/adapters/cli-pty.js +30 -10
  2. package/dist/adapters/cli-shared.js +73 -0
  3. package/dist/adapters/cli.js +40 -77
  4. package/dist/adapters/index.js +1 -0
  5. package/dist/adapters/ollama.js +32 -32
  6. package/dist/adapters/terminal.js +1 -0
  7. package/dist/args.js +1 -0
  8. package/dist/commands/agents.js +7 -1
  9. package/dist/commands/context.js +7 -1
  10. package/dist/commands/history.js +6 -1
  11. package/dist/commands/init.js +8 -3
  12. package/dist/commands/presets.js +6 -1
  13. package/dist/commands/shared.js +5 -1
  14. package/dist/commands/update.js +6 -1
  15. package/dist/config.js +17 -1
  16. package/dist/configWizard.js +5 -4
  17. package/dist/context.js +1 -0
  18. package/dist/contextScan.js +4 -3
  19. package/dist/discovery.js +1 -0
  20. package/dist/doctor.js +1 -0
  21. package/dist/errors.js +4 -0
  22. package/dist/exec.js +1 -0
  23. package/dist/history.js +10 -0
  24. package/dist/i18n.js +2 -0
  25. package/dist/index.js +151 -112
  26. package/dist/limits.js +4 -0
  27. package/dist/messages/adapter-errors.js +26 -2
  28. package/dist/messages/config.js +6 -0
  29. package/dist/messages/index.js +1 -0
  30. package/dist/messages/renderers.js +10 -2
  31. package/dist/messages/tui.js +8 -2
  32. package/dist/new.js +1 -0
  33. package/dist/ollamaUrl.js +20 -0
  34. package/dist/orchestrator.js +103 -150
  35. package/dist/output.js +1 -0
  36. package/dist/presets.js +1 -0
  37. package/dist/prompt.js +1 -0
  38. package/dist/renderers/console.js +1 -1
  39. package/dist/renderers/tui-prompts.js +339 -0
  40. package/dist/renderers/tui-renderer.js +224 -0
  41. package/dist/renderers/tui-screens.js +352 -0
  42. package/dist/renderers/tui-theme.js +356 -0
  43. package/dist/renderers/tui.js +7 -1086
  44. package/dist/runOptions.js +33 -2
  45. package/dist/session.js +1 -0
  46. package/dist/tuiController.js +61 -16
  47. package/dist/tuiState.js +4 -0
  48. package/dist/types.js +1 -0
  49. package/dist/update.js +1 -0
  50. package/dist/version.js +1 -0
  51. package/package.json +1 -1
@@ -0,0 +1,339 @@
1
+ /**
2
+ * @file Entrées interactives du TUI : prompt d'accueil, commandes `/config`,
3
+ * assistants agents/rôles et composer visuel. Toute la lecture readline vit ici,
4
+ * avec la gestion du double Ctrl+C (retour puis quit) partagée par tous les prompts.
5
+ */
6
+ import { createInterface } from "node:readline/promises";
7
+ import { stdin as input, stdout as output } from "node:process";
8
+ import { renderTuiAgentsHelp, renderTuiRolesHelp } from "./tui-screens.js";
9
+ import { accent, bold, composerCard, dim, glyphs, labeledRule, padBlock, surfacePadding, surfaceWidth, violet, wrapLine } from "./tui-theme.js";
10
+ /** Parse `/ollama-url <url>` : renvoie `unknown` avec le message d'usage si l'argument est absent. */
11
+ export function parseTuiOllamaUrlCommand(parts, messages) {
12
+ const value = parts[1];
13
+ return value ? { kind: "ollama-url", url: value } : { kind: "unknown", message: messages.tui.ollamaUrlUsage };
14
+ }
15
+ /**
16
+ * Extrait les options de contexte inline (`--context <chemins...>`, `--files <chemins...>`)
17
+ * d'un sujet tapé dans le composer, comme le suggère le tip de l'accueil. Les chemins
18
+ * suivent leur flag jusqu'au prochain token commençant par `--`. Limite assumée du MVP :
19
+ * les chemins contenant des espaces ne sont pas supportés dans cette syntaxe inline.
20
+ */
21
+ export function parseComposerTopic(value) {
22
+ const topicTokens = [];
23
+ const files = [];
24
+ const context = [];
25
+ let collector;
26
+ for (const token of value.split(/\s+/).filter(Boolean)) {
27
+ if (token === "--context") {
28
+ collector = context;
29
+ continue;
30
+ }
31
+ if (token === "--files" || token === "--file") {
32
+ collector = files;
33
+ continue;
34
+ }
35
+ if (token.startsWith("--")) {
36
+ // Flag inconnu : rendu au sujet tel quel pour rester visible et sans surprise.
37
+ collector = undefined;
38
+ topicTokens.push(token);
39
+ continue;
40
+ }
41
+ (collector ?? topicTokens).push(token);
42
+ }
43
+ return { topic: topicTokens.join(" "), files, context };
44
+ }
45
+ let lastTuiInterruptAt = 0;
46
+ const doubleInterruptMs = 1200;
47
+ function nextInterruptKind() {
48
+ const now = Date.now();
49
+ const kind = now - lastTuiInterruptAt <= doubleInterruptMs ? "quit" : "back";
50
+ lastTuiInterruptAt = now;
51
+ return kind;
52
+ }
53
+ function questionWithInterrupt(rl, prompt) {
54
+ return new Promise((resolve, reject) => {
55
+ let settled = false;
56
+ const cleanup = () => rl.off("SIGINT", onSigint);
57
+ const settle = (result) => {
58
+ if (settled)
59
+ return;
60
+ settled = true;
61
+ cleanup();
62
+ resolve(result);
63
+ };
64
+ const onSigint = () => {
65
+ const kind = nextInterruptKind();
66
+ rl.close();
67
+ settle({ kind });
68
+ };
69
+ rl.once("SIGINT", onSigint);
70
+ rl.question(prompt).then((value) => settle({ kind: "answer", value }), (error) => {
71
+ if (settled)
72
+ return;
73
+ settled = true;
74
+ cleanup();
75
+ reject(error);
76
+ });
77
+ });
78
+ }
79
+ /** Lit une demande depuis l'accueil TUI. Retourne undefined si l'utilisateur quitte. */
80
+ export async function promptTuiHomeTopic(mode = "debate", messages, options = {}) {
81
+ if (!input.isTTY) {
82
+ return undefined;
83
+ }
84
+ const rl = createInterface({ input, output });
85
+ try {
86
+ const result = await questionWithInterrupt(rl, tuiPrompt(mode, messages.tui.subject, messages, options.notice));
87
+ if (result.kind !== "answer") {
88
+ return undefined;
89
+ }
90
+ const answer = result.value;
91
+ const value = answer.trim();
92
+ const parts = value.split(/\s+/).filter(Boolean);
93
+ const command = parts[0]?.toLowerCase() ?? "";
94
+ if (!value || command === "/quit" || command === "/q" || command === "/exit") {
95
+ return undefined;
96
+ }
97
+ if (command === "/new") {
98
+ return { kind: "new" };
99
+ }
100
+ if (command === "/retry") {
101
+ return { kind: "retry" };
102
+ }
103
+ if (command === "/update") {
104
+ return { kind: "update" };
105
+ }
106
+ if (command === "/historique" || command === "/history") {
107
+ return { kind: "history" };
108
+ }
109
+ if (command === "/home" || command === "/back" || command === "/b") {
110
+ return { kind: "home" };
111
+ }
112
+ if (command === "/config") {
113
+ return { kind: "config" };
114
+ }
115
+ if (command === "/agents") {
116
+ return { kind: "agents", agents: parts.slice(1) };
117
+ }
118
+ if (command === "/ask") {
119
+ return { kind: "mode", mode: "ask" };
120
+ }
121
+ if (command === "/debat" || command === "/débat" || command === "/debate") {
122
+ return { kind: "mode", mode: "debate" };
123
+ }
124
+ if (command === "/help" || command === "/h" || command === "/?") {
125
+ return { kind: "help" };
126
+ }
127
+ if (command === "/roles" || command === "/role") {
128
+ return { kind: "roles", roles: parts.slice(1) };
129
+ }
130
+ if (value.startsWith("/")) {
131
+ return { kind: "help" };
132
+ }
133
+ const composerInput = parseComposerTopic(value);
134
+ return {
135
+ kind: "topic",
136
+ topic: composerInput.topic,
137
+ ...(composerInput.files.length > 0 ? { files: composerInput.files } : {}),
138
+ ...(composerInput.context.length > 0 ? { context: composerInput.context } : {})
139
+ };
140
+ }
141
+ finally {
142
+ rl.close();
143
+ }
144
+ }
145
+ /** Lit une commande depuis l'ecran de config TUI. */
146
+ export async function promptTuiConfigCommand(mode, messages) {
147
+ if (!input.isTTY) {
148
+ return { kind: "back" };
149
+ }
150
+ const rl = createInterface({ input, output });
151
+ try {
152
+ const result = await questionWithInterrupt(rl, tuiPrompt(mode, messages.tui.configPrompt, messages));
153
+ if (result.kind === "quit") {
154
+ return { kind: "quit" };
155
+ }
156
+ if (result.kind === "back") {
157
+ return { kind: "back" };
158
+ }
159
+ const answer = result.value;
160
+ const parts = answer.trim().split(/\s+/).filter(Boolean);
161
+ const command = parts[0]?.toLowerCase();
162
+ if (!command || command === "/home" || command === "/back" || command === "/b") {
163
+ return { kind: "back" };
164
+ }
165
+ if (command === "/quit" || command === "/q" || command === "/exit") {
166
+ return { kind: "quit" };
167
+ }
168
+ if (command === "/mode") {
169
+ return { kind: "mode" };
170
+ }
171
+ if (command === "/default") {
172
+ return { kind: "default-mode" };
173
+ }
174
+ if (command === "/interface") {
175
+ const value = parts[1];
176
+ if (value === "tui" || value === "terminal") {
177
+ return { kind: "interface", interfaceName: value };
178
+ }
179
+ return { kind: "unknown", message: "Usage: /interface <tui|terminal>" };
180
+ }
181
+ if (command === "/language" || command === "/langue" || command === "/lang") {
182
+ const value = parts[1];
183
+ if (value === "fr" || value === "en") {
184
+ return { kind: "language", language: value };
185
+ }
186
+ return { kind: "unknown", message: "Usage: /language <fr|en>" };
187
+ }
188
+ if (command === "/agents") {
189
+ return parts.length > 1
190
+ ? { kind: "agents", agents: parts.slice(1) }
191
+ : { kind: "agents", agents: [] };
192
+ }
193
+ if (command === "/roles" || command === "/role") {
194
+ return { kind: "roles", roles: parts.slice(1) };
195
+ }
196
+ if (command === "/turns") {
197
+ const turns = Number(parts[1]);
198
+ return Number.isInteger(turns)
199
+ ? { kind: "turns", turns }
200
+ : { kind: "unknown", message: messages.tui.turnsUsage };
201
+ }
202
+ if (command === "/summary") {
203
+ const value = parts[1];
204
+ if (!value) {
205
+ return { kind: "unknown", message: messages.tui.summaryUsage };
206
+ }
207
+ return { kind: "summary", agent: isNoneValue(value) ? undefined : value };
208
+ }
209
+ if (command === "/ollama") {
210
+ const value = parts[1];
211
+ return value ? { kind: "ollama-model", model: value } : { kind: "ollama-info" };
212
+ }
213
+ if (command === "/ollama-url" || command === "/ollama-host") {
214
+ return parseTuiOllamaUrlCommand(parts, messages);
215
+ }
216
+ if (command === "/ollama-model") {
217
+ const value = parts[1];
218
+ return value ? { kind: "ollama-model", model: value } : { kind: "unknown", message: messages.tui.ollamaModelUsage };
219
+ }
220
+ if (command === "/model") {
221
+ const [first, second] = parts.slice(1);
222
+ const value = first === "ollama-local" ? second : first;
223
+ return value ? { kind: "ollama-model", model: value } : { kind: "unknown", message: messages.tui.ollamaModelUsage };
224
+ }
225
+ if (command === "/ollama-sync") {
226
+ return { kind: "ollama-sync" };
227
+ }
228
+ return { kind: "unknown", message: messages.tui.unknownCommand };
229
+ }
230
+ finally {
231
+ rl.close();
232
+ }
233
+ }
234
+ /** Assistant minimal pour modifier les agents du mode courant. */
235
+ export async function promptTuiAgentsWizard(config, mode, messages) {
236
+ if (!input.isTTY) {
237
+ return { kind: "back" };
238
+ }
239
+ renderTuiAgentsHelp(config, mode, messages);
240
+ const rl = createInterface({ input, output });
241
+ try {
242
+ const result = await questionWithInterrupt(rl, tuiPrompt(mode, messages.tui.agentsPrompt, messages));
243
+ if (result.kind === "quit") {
244
+ return { kind: "quit" };
245
+ }
246
+ if (result.kind === "back") {
247
+ return { kind: "back" };
248
+ }
249
+ const value = result.value.trim();
250
+ if (!value || value === "/home" || value === "/back") {
251
+ return { kind: "back" };
252
+ }
253
+ if (value === "/quit" || value === "/q") {
254
+ return { kind: "quit" };
255
+ }
256
+ return { kind: "agents", agents: value.split(/\s+/).filter(Boolean) };
257
+ }
258
+ finally {
259
+ rl.close();
260
+ }
261
+ }
262
+ /** Assistant minimal pour modifier les roles du mode courant. */
263
+ export async function promptTuiRolesWizard(config, mode, messages) {
264
+ if (!input.isTTY) {
265
+ return { kind: "back" };
266
+ }
267
+ renderTuiRolesHelp(mode, messages, config);
268
+ const rl = createInterface({ input, output });
269
+ try {
270
+ const result = await questionWithInterrupt(rl, tuiPrompt(mode, messages.tui.rolesPrompt, messages));
271
+ if (result.kind === "quit") {
272
+ return { kind: "quit" };
273
+ }
274
+ if (result.kind === "back") {
275
+ return { kind: "back" };
276
+ }
277
+ const answer = result.value;
278
+ const value = answer.trim();
279
+ if (!value || value === "/home" || value === "/back") {
280
+ return { kind: "back" };
281
+ }
282
+ if (value === "/quit" || value === "/q") {
283
+ return { kind: "quit" };
284
+ }
285
+ return { kind: "roles", roles: value.split(/\s+/).filter(Boolean) };
286
+ }
287
+ finally {
288
+ rl.close();
289
+ }
290
+ }
291
+ /** Affiche un composer visuel juste avant la vraie ligne readline. */
292
+ export function renderTuiComposer(mode, messages, labelPrefix = messages.tui.subject, options = {}) {
293
+ if (!options.force && !input.isTTY) {
294
+ return;
295
+ }
296
+ const width = surfaceWidth();
297
+ process.stdout.write([
298
+ "",
299
+ ...padBlock(composerInputBox(mode, labelPrefix, width, messages)),
300
+ ""
301
+ ].join("\n"));
302
+ }
303
+ /**
304
+ * Zone de saisie : fil d'Ariane intégré à la règle violette, puis ligne de saisie
305
+ * réduite au marqueur `❯` — le contexte vit dans la règle, pas devant le curseur.
306
+ */
307
+ function tuiPrompt(mode, labelPrefix, messages, notice) {
308
+ const padding = surfacePadding();
309
+ const promptLine = `${padding}${accent(glyphs().prompt)} `;
310
+ return [
311
+ "",
312
+ `${padding}${labeledRule(promptTrail(mode, labelPrefix, messages), violet)}`,
313
+ ...(notice ? promptNoticeLines(notice) : []),
314
+ promptLine,
315
+ ].join("\n");
316
+ }
317
+ function promptNoticeLines(notice) {
318
+ const padding = surfacePadding();
319
+ const contentWidth = surfaceWidth();
320
+ return wrapLine(notice, contentWidth).map((line) => `${padding}${line}`);
321
+ }
322
+ function promptModeLabel(mode, messages) {
323
+ return `Mode ${messages.tui.modeValue(mode).toLowerCase()}`;
324
+ }
325
+ function promptTrail(mode, labelPrefix, messages) {
326
+ const parts = [bold("Palabre"), accent(promptModeLabel(mode, messages))];
327
+ if (labelPrefix !== messages.tui.subject) {
328
+ parts.push(bold(labelPrefix));
329
+ }
330
+ return parts.join(` ${dim(glyphs().pointer)} `);
331
+ }
332
+ function composerInputBox(mode, labelPrefix, width, messages) {
333
+ return composerCard([
334
+ `${promptTrail(mode, labelPrefix, messages)} ${accent(glyphs().prompt)}`
335
+ ], width);
336
+ }
337
+ function isNoneValue(value) {
338
+ return ["none", "aucun", "off", "non", "0", "disabled"].includes(value.toLowerCase());
339
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * @file Renderer TUI des événements de débat/ask : en-tête de session, statut
3
+ * "agent en cours" avec spinner, messages encadrés à la couleur de l'agent,
4
+ * synthèse structurée et panneau de fin avec liens vers l'export.
5
+ */
6
+ import path from "node:path";
7
+ import { accent, accentBar, agentColor, agentLabel, bold, brandHeader, card, clearScreen, codes, compactFileName, compactPath, danger, dim, glyphs, padBlock, panel, row, success, supportsColor, supportsInteractiveOutput, surfacePadding, surfaceWidth, terminalLink, underlineFor, warning } from "./tui-theme.js";
8
+ /** Cree le premier renderer TUI leger, sans dependance UI externe. */
9
+ export function createTuiRenderer(messages) {
10
+ return new TuiRenderer(messages, supportsColor, supportsInteractiveOutput);
11
+ }
12
+ /**
13
+ * Renderer TUI V0.
14
+ *
15
+ * Il ne tente pas encore de faire du scrolling controle ou des panes interactives :
16
+ * il fournit un tableau de bord plein terminal, un statut d'agent en cours et des
17
+ * sections lisibles tout en conservant la sortie complete dans le flux terminal.
18
+ */
19
+ class TuiRenderer {
20
+ messages;
21
+ color;
22
+ interactive;
23
+ spinner;
24
+ spinnerFrame = 0;
25
+ currentSection = "debate";
26
+ currentAgent;
27
+ /** Titre du tour en attente, rendu en tête du prochain bloc `message`. */
28
+ pendingHeader;
29
+ constructor(messages, color, interactive) {
30
+ this.messages = messages;
31
+ this.color = color;
32
+ this.interactive = interactive;
33
+ }
34
+ start(options, agents = []) {
35
+ if (this.interactive) {
36
+ clearScreen();
37
+ }
38
+ process.stdout.write(this.renderSessionHeader(options, agents).join("\n") + "\n");
39
+ }
40
+ notice(message) {
41
+ const width = this.width();
42
+ process.stdout.write(`\n${padBlock(card([
43
+ `${success(this.messages.renderers.infoPrefix)} ${message}`
44
+ ], width)).join("\n")}\n`);
45
+ }
46
+ warning(message) {
47
+ process.stderr.write(`${warning(this.messages.renderers.warningPrefix)} ${message}\n`);
48
+ }
49
+ turnStart(turn, totalTurns, agent, role) {
50
+ this.currentSection = "debate";
51
+ this.currentAgent = agent;
52
+ this.pendingHeader = `${agentLabel(agent)} (${role}) - ${this.messages.renderers.turn(turn, totalTurns)}`;
53
+ }
54
+ askResponseStart(response, totalResponses, agent, role) {
55
+ this.currentSection = "ask";
56
+ this.currentAgent = agent;
57
+ this.pendingHeader = `${agentLabel(agent)} (${role}) - ${this.messages.tui.askResponse(response, totalResponses)}`;
58
+ }
59
+ thinkingStart(agent, role) {
60
+ this.thinkingEnd();
61
+ const text = this.messages.renderers.thinking(agent, role);
62
+ if (!this.interactive) {
63
+ process.stdout.write(`${text}...\n`);
64
+ return;
65
+ }
66
+ process.stdout.write("\u001b[?25l");
67
+ const frames = glyphs().spinner;
68
+ const render = () => {
69
+ const frame = frames[this.spinnerFrame % frames.length];
70
+ this.spinnerFrame += 1;
71
+ process.stdout.write(`\r\u001b[2K${surfacePadding()}${agentColor(agent, frame)} ${text}...`);
72
+ };
73
+ render();
74
+ this.spinner = setInterval(render, 120);
75
+ }
76
+ thinkingEnd() {
77
+ if (this.spinner) {
78
+ clearInterval(this.spinner);
79
+ this.spinner = undefined;
80
+ }
81
+ if (this.interactive) {
82
+ process.stdout.write("\r\u001b[2K\u001b[?25h");
83
+ }
84
+ }
85
+ message(content) {
86
+ const trimmed = content.trim();
87
+ process.stdout.write(`${this.formatMessage(this.currentSection === "summary" ? this.formatSummary(trimmed) : trimmed)}\n`);
88
+ }
89
+ askResponseMessage(content) {
90
+ this.message(content);
91
+ }
92
+ summaryStart(agent, role) {
93
+ this.currentSection = "summary";
94
+ this.currentAgent = agent;
95
+ this.pendingHeader = `${this.messages.renderers.summaryTitle} - ${agentLabel(agent)} (${role})`;
96
+ }
97
+ error(failure) {
98
+ this.thinkingEnd();
99
+ const width = this.width();
100
+ const hint = this.messages.adapterErrors.hint(failure.kind);
101
+ process.stderr.write(`\n${padBlock(card([
102
+ danger(`${glyphs().cross} ${this.messages.common.errorPrefix}`),
103
+ `${formatFailureLocation(failure, this.messages)}: ${failure.message}`,
104
+ ...(hint ? ["", dim(`${this.messages.adapterErrors.suggestionPrefix}: ${hint}`)] : [])
105
+ ], width)).join("\n")}\n`);
106
+ }
107
+ done(outputPath) {
108
+ this.thinkingEnd();
109
+ const width = this.width();
110
+ const folderPath = path.dirname(outputPath);
111
+ const fileName = path.basename(outputPath);
112
+ process.stdout.write(`\n${padBlock(panel([
113
+ `${success(glyphs().check)} ${bold(this.messages.tui.sessionDone)}`,
114
+ "",
115
+ row(this.messages.tui.exportedFile, terminalLink(outputPath, compactFileName(fileName, width - 24))),
116
+ row(this.messages.tui.exportedFolder, terminalLink(folderPath, compactPath(folderPath, width - 24))),
117
+ "",
118
+ dim(this.messages.tui.sessionHistoryHint)
119
+ ], width)).join("\n")}\n\n`);
120
+ }
121
+ renderSessionHeader(options, agents) {
122
+ const width = this.width();
123
+ const mode = messagesModeLabel(this.messages, options.mode).toUpperCase();
124
+ const main = panel([
125
+ accent(mode),
126
+ this.messages.renderers.subject(options.topic),
127
+ this.messages.renderers.agents(formatAgents(options, agents)),
128
+ formatSessionProgress(options, this.messages),
129
+ this.messages.renderers.context(formatContext(options, this.messages)),
130
+ this.messages.renderers.workingFolder(compactPath(options.session.cwd, Math.max(24, width - 14)))
131
+ ], width);
132
+ return [
133
+ "",
134
+ ...padBlock([brandHeader()]),
135
+ "",
136
+ ...padBlock(main),
137
+ ""
138
+ ];
139
+ }
140
+ /**
141
+ * Bloc de message unique : en-tête du tour (titre souligné) et contenu dans le même
142
+ * bloc, délimité par la seule barre latérale gauche à la couleur de l'agent.
143
+ */
144
+ formatMessage(content) {
145
+ const width = this.width();
146
+ const header = this.pendingHeader
147
+ ? [bold(this.pendingHeader), underlineFor(this.pendingHeader, width, this.currentAgent), ""]
148
+ : [];
149
+ this.pendingHeader = undefined;
150
+ const body = content.split(/\r?\n/);
151
+ return `\n${padBlock(accentBar([...header, ...body], width, this.currentAgent)).join("\n")}\n`;
152
+ }
153
+ formatSummary(content) {
154
+ return content
155
+ .split(/\r?\n/)
156
+ .map((line) => {
157
+ const heading = line.match(/^###\s+(.+)$/);
158
+ if (!heading) {
159
+ return line;
160
+ }
161
+ return [
162
+ "",
163
+ this.c("magenta", heading[1] ?? line),
164
+ this.dim("-".repeat(Math.min(48, this.width())))
165
+ ].join("\n");
166
+ })
167
+ .join("\n")
168
+ .trimStart();
169
+ }
170
+ width() {
171
+ return surfaceWidth();
172
+ }
173
+ c(color, value) {
174
+ if (!this.color)
175
+ return value;
176
+ return `${codes[color]}${value}${codes.reset}`;
177
+ }
178
+ dim(value) {
179
+ if (!this.color)
180
+ return value;
181
+ return `${codes.dim}${value}${codes.reset}`;
182
+ }
183
+ }
184
+ function formatAgents(options, agents) {
185
+ if (options.mode === "ask") {
186
+ return agents.length > 0
187
+ ? agents.map(formatAgent).join(", ")
188
+ : (options.askAgents ?? [options.agentA, options.agentB]).join(", ");
189
+ }
190
+ if (agents.length >= 2) {
191
+ return `${formatAgent(agents[0])} <-> ${formatAgent(agents[1])}`;
192
+ }
193
+ return `${options.agentA} <-> ${options.agentB}`;
194
+ }
195
+ function formatAgent(agent) {
196
+ return agent ? `${agentLabel(agent.name)} (${agent.role})` : "?";
197
+ }
198
+ function formatSummary(options, messages) {
199
+ if (!options.summaryEnabled) {
200
+ return messages.renderers.disabled;
201
+ }
202
+ return options.summaryAgent;
203
+ }
204
+ function formatResponseCount(options) {
205
+ return options.mode === "ask" ? options.askAgents?.length ?? 2 : options.turns;
206
+ }
207
+ function formatSessionProgress(options, messages) {
208
+ return `${messages.tui.historyCount(options.mode)}: ${formatResponseCount(options)} | ${messages.tui.summary}: ${formatSummary(options, messages)}`;
209
+ }
210
+ function formatContext(options, messages) {
211
+ return options.files.length === 0
212
+ ? messages.renderers.noInjectedFiles
213
+ : messages.renderers.injectedFiles(options.files.length, options.files.map((file) => file.path));
214
+ }
215
+ function formatFailureLocation(failure, messages) {
216
+ if (failure.phase === "summary") {
217
+ return messages.renderers.summaryTitle;
218
+ }
219
+ const turn = failure.turn === undefined ? "" : `, ${messages.tui.turnLabel(failure.turn)}`;
220
+ return `${failure.agent ?? "?"} (${failure.role ?? "?"}${turn})`;
221
+ }
222
+ function messagesModeLabel(messages, mode) {
223
+ return messages.tui.modeValue(mode);
224
+ }