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,356 @@
1
+ /**
2
+ * @file Primitives visuelles du TUI : détection couleur/TTY/Unicode, glyphes avec repli
3
+ * ASCII (`PALABRE_ASCII=1`), cadre de boîte unifié, gouttière d'ancrage gauche, en-tête
4
+ * de marque compact, colonnes label/valeur adaptatives, tokens sémantiques et palette agents.
5
+ * Aucune logique produit ici : les écrans, prompts et le renderer consomment ces briques.
6
+ */
7
+ import path from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+ /** `true` si stdout supporte les couleurs ANSI (TTY sans `NO_COLOR`). */
10
+ export const supportsColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
11
+ /** `true` si stdout permet un rendu interactif (clear screen, spinner, liens OSC 8). */
12
+ export const supportsInteractiveOutput = Boolean(process.stdout.isTTY);
13
+ const unicodeGlyphs = {
14
+ h: "─",
15
+ v: "│",
16
+ tl: "┌",
17
+ tr: "┐",
18
+ bl: "└",
19
+ br: "┘",
20
+ pointer: "›",
21
+ prompt: "❯",
22
+ check: "✓",
23
+ cross: "✗",
24
+ spinner: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
25
+ };
26
+ const asciiGlyphs = {
27
+ h: "-",
28
+ v: "|",
29
+ tl: "+",
30
+ tr: "+",
31
+ bl: "+",
32
+ br: "+",
33
+ pointer: ">",
34
+ prompt: ">",
35
+ check: "v",
36
+ cross: "x",
37
+ spinner: ["-", "\\", "|", "/"]
38
+ };
39
+ /**
40
+ * Retourne le jeu de glyphes actif. Résolu paresseusement à chaque appel pour que
41
+ * `PALABRE_ASCII=1` (et les tests) puissent forcer le repli ASCII sans réimporter le module.
42
+ */
43
+ export function glyphs() {
44
+ return unicodeSupported() ? unicodeGlyphs : asciiGlyphs;
45
+ }
46
+ /**
47
+ * Heuristique de support Unicode inspirée de `figures`/`ora` : hors Windows, seul le
48
+ * TTY console `TERM=linux` pose problème ; sur Windows, Windows Terminal, ConEmu,
49
+ * VS Code et les shells posant `TERM` gèrent le tracé Unicode. `PALABRE_ASCII` force le repli.
50
+ */
51
+ function unicodeSupported() {
52
+ const env = process.env;
53
+ if (env.PALABRE_ASCII) {
54
+ return false;
55
+ }
56
+ if (process.platform !== "win32") {
57
+ return env.TERM !== "linux";
58
+ }
59
+ return Boolean(env.WT_SESSION)
60
+ || env.ConEmuANSI === "ON"
61
+ || env.TERM_PROGRAM === "vscode"
62
+ || Boolean(env.TERM);
63
+ }
64
+ /** Efface l'écran et restaure le curseur. À n'appeler que si `supportsInteractiveOutput`. */
65
+ export function clearScreen() {
66
+ process.stdout.write("\u001bc\u001b[?25h");
67
+ }
68
+ /** Largeur de la surface de contenu centrée : 72 à 96 colonnes selon le terminal. */
69
+ export function surfaceWidth() {
70
+ return Math.max(72, Math.min(viewportWidth() - 8, 96));
71
+ }
72
+ /** Largeur totale exploitable du terminal, bornée entre 72 et 180 colonnes. */
73
+ export function viewportWidth() {
74
+ return Math.max(72, Math.min(process.stdout.columns || 100, 180));
75
+ }
76
+ /**
77
+ * Gouttière fixe à gauche de toute la sortie TUI. L'ancrage gauche remplace
78
+ * l'ancien centrage : le rendu reste stable quelle que soit la largeur du terminal.
79
+ */
80
+ export function surfacePadding() {
81
+ return " ";
82
+ }
83
+ /** Compacte un chemin trop long en gardant la fin (`...suffixe`). */
84
+ export function compactPath(value, maxLength) {
85
+ if (value.length <= maxLength) {
86
+ return value;
87
+ }
88
+ const marker = "...";
89
+ const tailLength = Math.max(12, maxLength - marker.length);
90
+ return `${marker}${value.slice(-tailLength)}`;
91
+ }
92
+ /** `path.dirname` tolérant aux séparateurs mixtes `/` et `\` des exports historiques. */
93
+ export function dirnamePortable(value) {
94
+ const separatorIndex = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\"));
95
+ return separatorIndex > 0 ? value.slice(0, separatorIndex) : path.dirname(value);
96
+ }
97
+ /** Compacte un nom de fichier export en préservant l'extension `.debate.md`/`.ask.md`. */
98
+ export function compactFileName(value, maxLength) {
99
+ if (value.length <= maxLength) {
100
+ return value;
101
+ }
102
+ const extension = value.match(/\.(debate|ask)\.md$/i)?.[0] ?? "";
103
+ const marker = "...";
104
+ const headLength = Math.max(12, maxLength - marker.length - extension.length);
105
+ return `${value.slice(0, headLength)}${marker}${extension}`;
106
+ }
107
+ /** Rend un lien cliquable OSC 8 vers un fichier local, ou le label brut hors TTY. */
108
+ export function terminalLink(filePath, label) {
109
+ if (!supportsInteractiveOutput) {
110
+ return label;
111
+ }
112
+ return `\u001b]8;;${pathToFileURL(filePath).href}\u001b\\${label}\u001b]8;;\u001b\\`;
113
+ }
114
+ /** Logo Palabre + tagline, réservés à l'écran d'accueil. */
115
+ export function logoBlock(messages) {
116
+ return [
117
+ ...logo(),
118
+ "",
119
+ bold(messages.tui.tagline)
120
+ ];
121
+ }
122
+ /**
123
+ * Ligne de marque compacte des écrans hors accueil : titre accentué suivi d'une
124
+ * règle horizontale jusqu'à la largeur de surface. Remplace le logo 5 lignes.
125
+ */
126
+ export function brandHeader(title = "PALABRE") {
127
+ const rule = Math.max(0, surfaceWidth() - visibleLength(title) - 1);
128
+ return `${accent(bold(title))} ${dim(glyphs().h.repeat(rule))}`;
129
+ }
130
+ /** Logo "ANSI Shadow" (56 colonnes) en Unicode ; repli sur le logo ASCII historique sinon. */
131
+ const unicodeLogoLines = [
132
+ "██████╗ █████╗ ██╗ █████╗ ██████╗ ██████╗ ███████╗",
133
+ "██╔══██╗██╔══██╗██║ ██╔══██╗██╔══██╗██╔══██╗██╔════╝",
134
+ "██████╔╝███████║██║ ███████║██████╔╝██████╔╝█████╗ ",
135
+ "██╔═══╝ ██╔══██║██║ ██╔══██║██╔══██╗██╔══██╗██╔══╝ ",
136
+ "██║ ██║ ██║██████╗██║ ██║██████╔╝██║ ██║███████╗",
137
+ "╚═╝ ╚═╝ ╚═╝╚═════╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝"
138
+ ];
139
+ const asciiLogoLines = [
140
+ " ___ ___ _ ___ ___ ___ ___ ",
141
+ "| _ \\| _ || | | _ || _ )| _ \\| __|",
142
+ "| _/| || |_ | || _ \\| /| _| ",
143
+ "|_| |_|_||___||_|_||___/|_|_\\|___|"
144
+ ];
145
+ function logo() {
146
+ const lines = unicodeSupported() ? unicodeLogoLines : asciiLogoLines;
147
+ return lines.map((line) => supportsColor ? `${codes.logoViolet}${line}${codes.reset}` : line);
148
+ }
149
+ /** Ancre un bloc de lignes à la gouttière gauche fixe. */
150
+ export function padBlock(lines) {
151
+ const gutter = surfacePadding();
152
+ return lines.map((line) => `${gutter}${line}`);
153
+ }
154
+ /**
155
+ * Cadre fermé unique de tout le TUI (`┌─┐ │ └─┘`, repli ASCII `+-+ | +-+`).
156
+ * Toutes les boîtes (`card`, `panel`, `composerCard`, `textSurface`) en dérivent
157
+ * pour garantir un style homogène.
158
+ */
159
+ function frame(lines, width, options = {}) {
160
+ const g = glyphs();
161
+ const paint = options.border ?? dim;
162
+ const contentWidth = Math.max(18, width - 4);
163
+ const wrapped = lines.flatMap((line) => wrapLine(line, contentWidth));
164
+ const body = wrapped.length > 0 ? wrapped : [""];
165
+ const inner = options.padY ? ["", ...body, ""] : body;
166
+ const titleLength = options.title ? visibleLength(options.title) + 2 : 0;
167
+ const top = options.title
168
+ ? `${paint(`${g.tl}${g.h}`)} ${bold(options.title)} ${paint(`${g.h.repeat(Math.max(0, contentWidth - titleLength + 1))}${g.tr}`)}`
169
+ : paint(`${g.tl}${g.h.repeat(contentWidth + 2)}${g.tr}`);
170
+ const bottom = paint(`${g.bl}${g.h.repeat(contentWidth + 2)}${g.br}`);
171
+ return [
172
+ top,
173
+ ...inner.map((line) => `${paint(g.v)} ${padRight(line, contentWidth)} ${paint(g.v)}`),
174
+ bottom
175
+ ];
176
+ }
177
+ /** Boîte de contenu standard, avec titre optionnel intégré à la bordure. */
178
+ export function card(lines, width, title) {
179
+ return frame(lines, width, { title });
180
+ }
181
+ /** Boîte "héro" de l'accueil : même cadre que `card` avec respirations verticales. */
182
+ export function composerCard(lines, width) {
183
+ return frame(lines, width, { padY: true });
184
+ }
185
+ /** Panneau de session et d'historique ; même cadre que `card`, titre optionnel. */
186
+ export function panel(lines, width, title) {
187
+ return frame(lines, width, { title });
188
+ }
189
+ /**
190
+ * Bloc à barre latérale gauche seule, à la couleur de l'agent : utilisé pour les
191
+ * messages de débat/ask, moins chargé qu'un cadre fermé quand les blocs s'enchaînent.
192
+ */
193
+ export function accentBar(lines, width, agent) {
194
+ const g = glyphs();
195
+ const paint = agent ? (value) => agentColor(agent, value) : violet;
196
+ const contentWidth = Math.max(24, width - 2);
197
+ const body = lines.flatMap((line) => line ? wrapLine(line, contentWidth) : [""]);
198
+ return (body.length > 0 ? body : [""]).map((line) => `${paint(g.v)} ${line}`);
199
+ }
200
+ /**
201
+ * Règle horizontale portant un label : `── Label ──────`.
202
+ * Sert de séparateur de zone de saisie avec le fil d'Ariane intégré.
203
+ */
204
+ export function labeledRule(label, paint = dim) {
205
+ const g = glyphs();
206
+ const fill = Math.max(0, surfaceWidth() - visibleLength(label) - 4);
207
+ return `${paint(g.h.repeat(2))} ${label} ${paint(g.h.repeat(fill))}`;
208
+ }
209
+ /** Trait de soulignement d'un titre, à la couleur de l'agent quand elle est connue. */
210
+ export function underlineFor(title, width, agent) {
211
+ const length = Math.max(8, Math.min(48, visibleLength(title), width - 4));
212
+ const line = glyphs().h.repeat(length);
213
+ return agent ? agentColor(agent, line) : accent(line);
214
+ }
215
+ /** Ligne `label valeur` avec label en gras aligné sur `labelWidth` colonnes. */
216
+ export function row(label, value, labelWidth = 16) {
217
+ return `${bold(label.padEnd(labelWidth))} ${value}`;
218
+ }
219
+ /**
220
+ * Rend un bloc de lignes label/valeur avec une colonne de labels adaptative :
221
+ * la largeur s'aligne sur le label le plus long du bloc au lieu d'une constante,
222
+ * ce qui évite les colonnes cassées quand un label dépasse 16 caractères.
223
+ */
224
+ export function rows(entries) {
225
+ const labels = entries.filter((entry) => typeof entry !== "string");
226
+ const labelWidth = Math.max(...labels.map(([label]) => label.length), 0) + 2;
227
+ return entries.map((entry) => typeof entry === "string" ? entry : row(entry[0], entry[1], labelWidth));
228
+ }
229
+ /** Coupe une ligne aux mots pour tenir dans `width` colonnes visibles (ANSI ignoré). */
230
+ export function wrapLine(value, width) {
231
+ const clean = value.replace(/\r?\n/g, " ");
232
+ if (visibleLength(clean) <= width) {
233
+ return [clean];
234
+ }
235
+ const words = clean.split(/\s+/);
236
+ const lines = [];
237
+ let current = "";
238
+ for (const word of words) {
239
+ const next = current ? `${current} ${word}` : word;
240
+ if (visibleLength(next) <= width) {
241
+ current = next;
242
+ }
243
+ else {
244
+ if (current)
245
+ lines.push(current);
246
+ current = word;
247
+ }
248
+ }
249
+ if (current)
250
+ lines.push(current);
251
+ return lines;
252
+ }
253
+ function padRight(value, width) {
254
+ const missing = Math.max(0, width - visibleLength(value));
255
+ return `${value}${" ".repeat(missing)}`;
256
+ }
257
+ function visibleLength(value) {
258
+ return stripAnsi(value).length;
259
+ }
260
+ function stripAnsi(value) {
261
+ return value
262
+ .replace(/\u001b\][^\u001b]*(?:\u0007|\u001b\\)/g, "")
263
+ .replace(/\u001b\[[0-9;?]*[A-Za-z]/g, "");
264
+ }
265
+ /** Met en gras si les couleurs sont supportées, sinon retourne la valeur brute. */
266
+ export function bold(value) {
267
+ return supportsColor ? `${codes.bright}${value}${codes.reset}` : value;
268
+ }
269
+ /** Atténue si les couleurs sont supportées, sinon retourne la valeur brute. */
270
+ export function dim(value) {
271
+ return supportsColor ? `${codes.dim}${value}${codes.reset}` : value;
272
+ }
273
+ /** Couleur d'accent Palabre (violet). */
274
+ export function accent(value) {
275
+ return supportsColor ? `${codes.violet}${value}${codes.reset}` : value;
276
+ }
277
+ /** Violet des règles horizontales et bordures neutres. */
278
+ export function violet(value) {
279
+ return supportsColor ? `${codes.violet}${value}${codes.reset}` : value;
280
+ }
281
+ /** Token sémantique : succès (fin de session, confirmation). */
282
+ export function success(value) {
283
+ return supportsColor ? `${codes.green}${value}${codes.reset}` : value;
284
+ }
285
+ /** Token sémantique : avertissement non bloquant. */
286
+ export function warning(value) {
287
+ return supportsColor ? `${codes.yellow}${value}${codes.reset}` : value;
288
+ }
289
+ /** Token sémantique : erreur. */
290
+ export function danger(value) {
291
+ return supportsColor ? `${codes.red}${value}${codes.reset}` : value;
292
+ }
293
+ /** Nom d'agent rendu dans sa couleur stable. */
294
+ export function agentLabel(agent) {
295
+ return supportsColor ? `${agentAnsi(agent)}${agent}${codes.reset}` : agent;
296
+ }
297
+ /** Applique la couleur stable d'un agent à une valeur arbitraire. */
298
+ export function agentColor(agent, value) {
299
+ return supportsColor ? `${agentAnsi(agent)}${value}${codes.reset}` : value;
300
+ }
301
+ function agentAnsi(agent) {
302
+ const normalized = agent.toLowerCase();
303
+ const color = agentColors[normalized] ?? hashedAgentColor(normalized);
304
+ return `\u001b[38;2;${color[0]};${color[1]};${color[2]}m`;
305
+ }
306
+ function hashedAgentColor(agent) {
307
+ let hash = 0;
308
+ for (let index = 0; index < agent.length; index += 1) {
309
+ hash = (hash * 31 + agent.charCodeAt(index)) | 0;
310
+ }
311
+ const hue = Math.abs(hash) % 360;
312
+ return hslToRgb(hue, 55, 55);
313
+ }
314
+ function hslToRgb(hue, saturation, lightness) {
315
+ const s = saturation / 100;
316
+ const l = lightness / 100;
317
+ const c = (1 - Math.abs(2 * l - 1)) * s;
318
+ const x = c * (1 - Math.abs((hue / 60) % 2 - 1));
319
+ const m = l - c / 2;
320
+ const [r, g, b] = hue < 60 ? [c, x, 0]
321
+ : hue < 120 ? [x, c, 0]
322
+ : hue < 180 ? [0, c, x]
323
+ : hue < 240 ? [0, x, c]
324
+ : hue < 300 ? [x, 0, c]
325
+ : [c, 0, x];
326
+ return [
327
+ Math.round((r + m) * 255),
328
+ Math.round((g + m) * 255),
329
+ Math.round((b + m) * 255)
330
+ ];
331
+ }
332
+ /** Couleurs fixes des agents connus ; les agents custom reçoivent une couleur hashée stable. */
333
+ const agentColors = {
334
+ antigravity: [76, 141, 255],
335
+ claude: [222, 115, 86],
336
+ codex: [136, 82, 197],
337
+ opencode: [80, 168, 103],
338
+ vibe: [234, 92, 126],
339
+ "ollama-local": [179, 176, 31],
340
+ ollama: [179, 176, 31]
341
+ };
342
+ /** Codes ANSI bruts utilisés par le thème et le renderer de débat. */
343
+ export const codes = {
344
+ reset: "\u001b[0m",
345
+ bright: "\u001b[1m",
346
+ dim: "\u001b[2m",
347
+ logoViolet: "\u001b[38;2;139;105;218m",
348
+ violet: "\u001b[38;5;141m",
349
+ cyan: "\u001b[36m",
350
+ gray: "\u001b[38;5;244m",
351
+ green: "\u001b[32m",
352
+ magenta: "\u001b[35m",
353
+ orange: "\u001b[38;5;214m",
354
+ red: "\u001b[31m",
355
+ yellow: "\u001b[33m"
356
+ };