palabre 0.9.1 → 0.10.1

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 (52) hide show
  1. package/README.md +6 -0
  2. package/dist/adapters/cli-pty.js +30 -10
  3. package/dist/adapters/cli-shared.js +73 -0
  4. package/dist/adapters/cli.js +40 -77
  5. package/dist/adapters/index.js +1 -0
  6. package/dist/adapters/ollama.js +32 -32
  7. package/dist/adapters/terminal.js +1 -0
  8. package/dist/args.js +1 -0
  9. package/dist/commands/agents.js +7 -1
  10. package/dist/commands/context.js +7 -1
  11. package/dist/commands/history.js +6 -1
  12. package/dist/commands/init.js +8 -3
  13. package/dist/commands/presets.js +6 -1
  14. package/dist/commands/shared.js +5 -1
  15. package/dist/commands/update.js +6 -1
  16. package/dist/config.js +17 -1
  17. package/dist/configWizard.js +5 -4
  18. package/dist/context.js +1 -0
  19. package/dist/contextScan.js +4 -3
  20. package/dist/discovery.js +1 -0
  21. package/dist/doctor.js +1 -0
  22. package/dist/errors.js +4 -0
  23. package/dist/exec.js +1 -0
  24. package/dist/history.js +10 -0
  25. package/dist/i18n.js +2 -0
  26. package/dist/index.js +170 -112
  27. package/dist/limits.js +4 -0
  28. package/dist/messages/adapter-errors.js +26 -2
  29. package/dist/messages/config.js +6 -0
  30. package/dist/messages/index.js +1 -0
  31. package/dist/messages/renderers.js +10 -2
  32. package/dist/messages/tui.js +8 -2
  33. package/dist/new.js +65 -11
  34. package/dist/ollamaUrl.js +20 -0
  35. package/dist/orchestrator.js +103 -150
  36. package/dist/output.js +1 -0
  37. package/dist/presets.js +1 -0
  38. package/dist/prompt.js +1 -0
  39. package/dist/renderers/console.js +1 -1
  40. package/dist/renderers/tui-prompts.js +343 -0
  41. package/dist/renderers/tui-renderer.js +228 -0
  42. package/dist/renderers/tui-screens.js +352 -0
  43. package/dist/renderers/tui-theme.js +356 -0
  44. package/dist/renderers/tui.js +7 -1086
  45. package/dist/runOptions.js +33 -2
  46. package/dist/session.js +1 -0
  47. package/dist/tuiController.js +61 -16
  48. package/dist/tuiState.js +4 -0
  49. package/dist/types.js +1 -0
  50. package/dist/update.js +1 -0
  51. package/dist/version.js +1 -0
  52. package/package.json +1 -1
@@ -0,0 +1,352 @@
1
+ /**
2
+ * @file Écrans plein terminal du TUI : accueil, aide, agents, rôles, historique,
3
+ * configuration et instructions de mise à jour. Chaque écran efface l'écran en TTY
4
+ * puis compose des blocs du thème (`tui-theme`) ; aucune lecture d'entrée ici.
5
+ */
6
+ import path from "node:path";
7
+ import { isRetiredAgentName } from "../agentRegistry.js";
8
+ import { DEFAULT_OLLAMA_BASE_URL, resolveOllamaBaseUrl } from "../ollamaUrl.js";
9
+ import { accent, agentLabel, bold, brandHeader, card, clearScreen, compactFileName, compactPath, composerCard, dim, dirnamePortable, logoBlock, padBlock, panel, row, rows, supportsInteractiveOutput, surfaceWidth, terminalLink, viewportWidth } from "./tui-theme.js";
10
+ /** Affiche l'ecran d'accueil TUI lance par `palabre` sans sujet. */
11
+ export function renderTuiHome(config, _configPath, messages, state = {}) {
12
+ if (supportsInteractiveOutput) {
13
+ clearScreen();
14
+ }
15
+ const viewport = viewportWidth();
16
+ const width = surfaceWidth();
17
+ const defaults = config.defaults ?? {};
18
+ const mode = state.mode ?? defaults.mode ?? "debate";
19
+ const debateAgents = defaults.agentA && defaults.agentB
20
+ ? `${defaults.agentA} <-> ${defaults.agentB}`
21
+ : messages.tui.noValue;
22
+ const askAgents = defaults.askAgents && defaults.askAgents.length > 0
23
+ ? defaults.askAgents.join(", ")
24
+ : debateAgents.replace(" <-> ", ", ");
25
+ const debateRoles = defaults.agentA && defaults.agentB
26
+ ? `${roleFor(config, defaults.agentA, messages)} <-> ${roleFor(config, defaults.agentB, messages)}`
27
+ : messages.tui.noValue;
28
+ const askAgentNames = defaults.askAgents && defaults.askAgents.length > 0
29
+ ? defaults.askAgents
30
+ : [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent));
31
+ const askRoles = askAgentNames.length > 0
32
+ ? askAgentNames.map((agent) => roleFor(config, agent, messages)).join(", ")
33
+ : debateRoles.replace(" <-> ", ", ");
34
+ const summary = mode === "ask"
35
+ ? defaults.askSummaryAgent ?? defaults.summaryAgent ?? messages.tui.lastAskAgent
36
+ : defaults.summaryAgent ?? defaults.agentB ?? "agent B";
37
+ const version = state.version ?? "0.0.0";
38
+ const versionLines = state.latestVersion
39
+ ? [dim(`v${version}`), accent(messages.tui.updateAvailable(version, state.latestVersion))]
40
+ : [dim(`v${version}`)];
41
+ const lines = [
42
+ "",
43
+ ...padBlock(logoBlock(messages)),
44
+ ...padBlock(versionLines),
45
+ "",
46
+ ...padBlock(composerCard([
47
+ `${accent(messages.tui.modeValue(mode))} ${dim("·")} ${mode === "ask" ? askAgents : debateAgents}`,
48
+ `${accent(messages.tui.roles)} ${dim("·")} ${mode === "ask" ? askRoles : debateRoles}`,
49
+ `${accent(messages.tui.summary)} ${dim("·")} ${summary}${mode === "debate" ? ` ${dim("·")} ${accent(messages.tui.responses)} ${String(defaults.turns ?? "?")}` : ""}`,
50
+ `${accent(messages.tui.folder)} ${dim("·")} ${compactPath(process.cwd(), Math.min(width - 4, viewport - 12))}`,
51
+ `${accent(messages.tui.docs)} ${dim("·")} ${documentationUrl(config)}`,
52
+ "",
53
+ `${accent("/help")} ${dim(messages.tui.commands)} ${accent("/roles")} ${dim(messages.tui.roles.toLowerCase())} ${accent("/config")} ${dim(messages.tui.settings)} ${accent(mode === "ask" ? "/debat" : "/ask")} ${dim(messages.tui.changeMode)}`
54
+ ], width)),
55
+ "",
56
+ ...padBlock([
57
+ dim(messages.tui.tipContext)
58
+ ])
59
+ ];
60
+ process.stdout.write(lines.join("\n") + "\n");
61
+ }
62
+ /** Affiche les instructions de mise a jour sans quitter le TUI. */
63
+ export function renderTuiUpdate(instructions, _messages) {
64
+ if (supportsInteractiveOutput) {
65
+ clearScreen();
66
+ }
67
+ const width = surfaceWidth();
68
+ process.stdout.write([
69
+ "",
70
+ ...padBlock([brandHeader()]),
71
+ "",
72
+ ...padBlock(card(instructions.split(/\r?\n/), width)),
73
+ ""
74
+ ].join("\n"));
75
+ }
76
+ /** Affiche l'aide interne du composer TUI. */
77
+ export function renderTuiHelp(messages) {
78
+ if (supportsInteractiveOutput) {
79
+ clearScreen();
80
+ }
81
+ const width = surfaceWidth();
82
+ process.stdout.write([
83
+ "",
84
+ ...padBlock([brandHeader(messages.tui.helpTitle)]),
85
+ "",
86
+ ...padBlock(card([
87
+ row("/ask", messages.tui.helpAsk),
88
+ row("/debat", messages.tui.helpDebate),
89
+ "",
90
+ row("/agents", messages.tui.helpAgents),
91
+ row("/roles", messages.tui.helpRoles),
92
+ row("/config", messages.tui.helpConfig),
93
+ "",
94
+ row("/new", messages.tui.helpNew),
95
+ row("/retry", messages.tui.helpRetry),
96
+ row("/history", messages.tui.helpHistory),
97
+ row("/update", messages.tui.helpUpdate),
98
+ row("/home", messages.tui.backCommand),
99
+ row("/help", messages.tui.helpHelp),
100
+ row("/quit", messages.tui.helpQuit),
101
+ "",
102
+ dim(messages.tui.helpFallback)
103
+ ], width)),
104
+ ""
105
+ ].join("\n"));
106
+ }
107
+ /** Affiche l'aide rapide des agents configures. */
108
+ export function renderTuiAgentsHelp(config, mode, messages) {
109
+ if (supportsInteractiveOutput) {
110
+ clearScreen();
111
+ }
112
+ const width = surfaceWidth();
113
+ const activeAgents = activeAgentNamesForMode(config, mode);
114
+ const separator = mode === "ask" ? ", " : " <-> ";
115
+ const exampleAgents = exampleAgentsForMode(config, mode);
116
+ process.stdout.write([
117
+ "",
118
+ ...padBlock([brandHeader(messages.tui.agentsTitle)]),
119
+ "",
120
+ ...padBlock(card([
121
+ row(messages.tui.activeMode, messages.tui.modeValue(mode)),
122
+ row(messages.tui.activeAgents, activeAgents.length > 0 ? activeAgents.join(separator) : messages.tui.noValue),
123
+ "",
124
+ bold(messages.tui.availableAgents),
125
+ "",
126
+ ...agentInventoryRows(config, messages),
127
+ "",
128
+ dim(`${messages.tui.example}: ${messages.tui.modeLabel(mode)} > ${messages.tui.agentsPrompt} > ${exampleAgents.join(" ")}`)
129
+ ], width)),
130
+ ""
131
+ ].join("\n"));
132
+ }
133
+ /** Affiche l'aide rapide des roles disponibles. */
134
+ export function renderTuiRolesHelp(mode, messages, config) {
135
+ if (supportsInteractiveOutput) {
136
+ clearScreen();
137
+ }
138
+ const width = surfaceWidth();
139
+ const currentRoles = config ? roleLineForMode(config, mode, messages) : undefined;
140
+ const activeAgents = config ? activeAgentNamesForMode(config, mode) : [];
141
+ const expectedCount = activeAgents.length || (mode === "ask" ? 3 : 2);
142
+ const exampleRoles = exampleRolesForMode(mode, expectedCount);
143
+ process.stdout.write([
144
+ "",
145
+ ...padBlock([brandHeader(messages.tui.rolesTitle)]),
146
+ "",
147
+ ...padBlock(card([
148
+ ...(activeAgents.length > 0 ? [row(messages.tui.activeAgents, activeAgents.join(mode === "ask" ? ", " : " <-> "))] : []),
149
+ ...(currentRoles ? [row(messages.tui.currentConfig, currentRoles), ""] : []),
150
+ bold(messages.tui.availableRoles),
151
+ "",
152
+ row("implementer", messages.tui.roleImplementer),
153
+ row("critic", messages.tui.roleCritic),
154
+ row("architect", messages.tui.roleArchitect),
155
+ row("scout", messages.tui.roleScout),
156
+ row("reviewer", messages.tui.roleReviewer),
157
+ row("summarizer", messages.tui.roleSummarizer),
158
+ "",
159
+ dim(`${messages.tui.example}: ${messages.tui.modeLabel(mode)} > ${messages.tui.rolesPrompt} > ${exampleRoles.join(" ")}`)
160
+ ], Math.min(width, 82))),
161
+ ""
162
+ ].join("\n"));
163
+ }
164
+ /** Affiche les derniers exports Palabre disponibles. */
165
+ export function renderTuiHistory(entries, messages) {
166
+ if (supportsInteractiveOutput) {
167
+ clearScreen();
168
+ }
169
+ const width = surfaceWidth();
170
+ const entryRows = entries.length === 0
171
+ ? [dim(messages.tui.historyEmpty)]
172
+ : entries.flatMap((entry) => {
173
+ const folderPath = path.dirname(entry.path);
174
+ const folderLabel = folderPath === "." ? dirnamePortable(entry.path) : folderPath;
175
+ return [
176
+ row(messages.tui.historyMode(entry.mode), entry.topic),
177
+ row(messages.tui.activeAgents, entry.agents || messages.tui.noValue),
178
+ ...(entry.count ? [row(messages.tui.historyCount(entry.mode), entry.count)] : []),
179
+ row(messages.tui.historyFile, terminalLink(entry.path, compactFileName(entry.fileName, width - 24))),
180
+ row(messages.tui.folder, terminalLink(folderPath, compactPath(folderLabel, width - 24))),
181
+ ...(entry.date ? [row("Date", entry.date)] : []),
182
+ ""
183
+ ];
184
+ }).slice(0, -1);
185
+ process.stdout.write([
186
+ "",
187
+ ...padBlock([brandHeader(messages.tui.historyTitle)]),
188
+ "",
189
+ ...padBlock(panel([
190
+ ...entryRows,
191
+ "",
192
+ dim(messages.tui.historyOpenHint)
193
+ ], width)),
194
+ ""
195
+ ].join("\n"));
196
+ }
197
+ /** Affiche l'ecran de config natif TUI, adapte au mode courant. */
198
+ export function renderTuiConfig(config, configPath, mode, messages, state = {}) {
199
+ if (supportsInteractiveOutput) {
200
+ clearScreen();
201
+ }
202
+ const width = surfaceWidth();
203
+ const defaults = config.defaults ?? {};
204
+ const debateAgents = defaults.agentA && defaults.agentB ? `${defaults.agentA} <-> ${defaults.agentB}` : messages.tui.noValue;
205
+ const askAgents = defaults.askAgents && defaults.askAgents.length > 0
206
+ ? defaults.askAgents.join(", ")
207
+ : debateAgents.replace(" <-> ", ", ");
208
+ const debateRoles = defaults.agentA && defaults.agentB ? `${roleFor(config, defaults.agentA, messages)} <-> ${roleFor(config, defaults.agentB, messages)}` : messages.tui.noValue;
209
+ const askRoles = roleLineForMode(config, "ask", messages);
210
+ const summary = mode === "ask"
211
+ ? defaults.askSummaryAgent ?? defaults.summaryAgent ?? messages.tui.lastAskAgent
212
+ : defaults.summaryAgent ?? defaults.agentB ?? messages.tui.noValue;
213
+ const ollamaAgent = config.agents["ollama-local"];
214
+ const ollamaModel = ollamaAgent?.type === "ollama" ? ollamaAgent.model : undefined;
215
+ const ollamaUrl = ollamaAgent?.type === "ollama" ? ollamaAgent.baseUrl ?? DEFAULT_OLLAMA_BASE_URL : undefined;
216
+ const ollamaEffectiveUrl = ollamaUrl ? safeEffectiveOllamaUrl(ollamaUrl) : undefined;
217
+ const generalBox = card(rows([
218
+ [messages.tui.activeMode, messages.tui.modeValue(mode)],
219
+ [messages.tui.configFile, configPath],
220
+ [messages.tui.interface, defaults.interface ?? "tui"],
221
+ [messages.tui.language, config.language ?? "fr"],
222
+ [messages.tui.availableAgentsShort, agentInventoryLine(config, messages)]
223
+ ]), width, messages.tui.configSectionGeneral);
224
+ const ollamaBox = ollamaModel
225
+ ? card(rows([
226
+ [messages.tui.ollamaModel, ollamaModel],
227
+ ...(ollamaUrl ? [[messages.tui.ollamaUrl, ollamaUrl]] : []),
228
+ ...(ollamaEffectiveUrl && ollamaEffectiveUrl !== ollamaUrl ? [[messages.tui.ollamaUrlEffective, ollamaEffectiveUrl]] : [])
229
+ ]), width, "Ollama")
230
+ : [];
231
+ const sessionEntries = mode === "ask"
232
+ ? [
233
+ [messages.tui.activeAgents, askAgents],
234
+ [messages.tui.roles, askRoles],
235
+ [messages.tui.summary, summary]
236
+ ]
237
+ : [
238
+ [messages.tui.activeAgents, debateAgents],
239
+ [messages.tui.roles, debateRoles],
240
+ [messages.tui.summary, summary],
241
+ [messages.tui.responses, String(defaults.turns ?? "?")]
242
+ ];
243
+ const sessionBox = card(rows(sessionEntries), width, messages.tui.modeValue(mode));
244
+ const commandRows = [
245
+ bold(messages.tui.availableCommands),
246
+ "",
247
+ ...(mode === "ask"
248
+ ? [
249
+ row("/agents", messages.tui.askAgentsUsage),
250
+ row("/roles", messages.tui.rolesUsage),
251
+ row("/summary", messages.tui.summaryUsage)
252
+ ]
253
+ : [
254
+ row("/agents", messages.tui.debateAgentsUsage),
255
+ row("/roles", messages.tui.rolesUsage),
256
+ row("/turns", messages.tui.turnsUsage),
257
+ row("/summary", messages.tui.summaryUsage)
258
+ ]),
259
+ row("/mode", messages.tui.modeConfigCommand),
260
+ ...(ollamaModel ? [
261
+ row("/ollama", messages.tui.ollamaInfoCommand),
262
+ row("/ollama-model", messages.tui.ollamaModelUsage),
263
+ row("/ollama-url", messages.tui.ollamaUrlCommand),
264
+ row("/ollama-sync", messages.tui.ollamaSyncCommand)
265
+ ] : []),
266
+ row("/interface", messages.tui.interfaceUsage),
267
+ row("/language", messages.tui.languageUsage),
268
+ "",
269
+ row("/home", messages.tui.backCommand),
270
+ row("/quit", messages.tui.quitCommand)
271
+ ];
272
+ const lines = [
273
+ "",
274
+ ...padBlock([brandHeader(messages.tui.configTitle)]),
275
+ "",
276
+ ...padBlock(generalBox),
277
+ "",
278
+ ...padBlock(sessionBox),
279
+ ...(ollamaBox.length > 0 ? ["", ...padBlock(ollamaBox)] : []),
280
+ "",
281
+ ...padBlock(commandRows),
282
+ ...(state.message ? ["", ...padBlock([state.message])] : [])
283
+ ];
284
+ process.stdout.write(lines.join("\n") + "\n");
285
+ }
286
+ /** Résout l'URL Ollama effective sans lever : retombe sur `OLLAMA_HOST` ou l'URL config brute. */
287
+ function safeEffectiveOllamaUrl(configUrl) {
288
+ try {
289
+ return resolveOllamaBaseUrl({ configUrl });
290
+ }
291
+ catch {
292
+ return process.env.OLLAMA_HOST?.trim() || configUrl;
293
+ }
294
+ }
295
+ function roleFor(config, agent, messages) {
296
+ return config.agents[agent]?.role ?? messages.tui.noValue;
297
+ }
298
+ function roleLineForMode(config, mode, messages) {
299
+ const agents = activeAgentNamesForMode(config, mode);
300
+ if (mode === "ask") {
301
+ return agents.length > 0 ? agents.map((agent) => roleFor(config, agent, messages)).join(", ") : messages.tui.noValue;
302
+ }
303
+ return agents.length === 2
304
+ ? `${roleFor(config, agents[0], messages)} <-> ${roleFor(config, agents[1], messages)}`
305
+ : messages.tui.noValue;
306
+ }
307
+ function activeAgentNamesForMode(config, mode) {
308
+ const defaults = config.defaults ?? {};
309
+ if (mode === "ask") {
310
+ const agents = defaults.askAgents && defaults.askAgents.length > 0
311
+ ? defaults.askAgents
312
+ : [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent));
313
+ return agents.filter((agent) => Boolean(config.agents[agent]) && !isRetiredAgentName(agent));
314
+ }
315
+ return [defaults.agentA, defaults.agentB].filter((agent) => typeof agent === "string" && Boolean(config.agents[agent]) && !isRetiredAgentName(agent));
316
+ }
317
+ function agentInventoryLine(config, messages) {
318
+ const agents = Object.entries(config.agents)
319
+ .filter(([name]) => !isRetiredAgentName(name))
320
+ .map(([name]) => name)
321
+ .sort();
322
+ return agents.length > 0 ? agents.map(agentLabel).join(", ") : messages.tui.noValue;
323
+ }
324
+ function agentInventoryRows(config, messages) {
325
+ const entries = Object.entries(config.agents)
326
+ .filter(([name]) => !isRetiredAgentName(name))
327
+ .sort(([agentA], [agentB]) => agentA.localeCompare(agentB));
328
+ if (entries.length === 0) {
329
+ return [dim(messages.tui.noConfiguredAgents)];
330
+ }
331
+ return entries.map(([name, agent]) => row(name, `${agent.type} ${dim("·")} ${agent.role}`));
332
+ }
333
+ function exampleAgentsForMode(config, mode) {
334
+ const activeAgents = activeAgentNamesForMode(config, mode);
335
+ if (activeAgents.length > 0) {
336
+ return activeAgents;
337
+ }
338
+ const available = Object.keys(config.agents).filter((agent) => !isRetiredAgentName(agent)).sort();
339
+ return mode === "ask" ? available.slice(0, 3) : available.slice(0, 2);
340
+ }
341
+ function documentationUrl(config) {
342
+ return `https://palab.re/${config.language === "en" ? "en" : "fr"}`;
343
+ }
344
+ function exampleRolesForMode(mode, count) {
345
+ const roles = mode === "ask"
346
+ ? ["critic", "implementer", "scout", "architect"]
347
+ : ["implementer", "critic"];
348
+ while (roles.length < count) {
349
+ roles.push("reviewer");
350
+ }
351
+ return roles.slice(0, count);
352
+ }