palabre 0.10.0 → 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.
package/README.md CHANGED
@@ -30,6 +30,9 @@ Requirements: Node.js 20 or newer, and at least two already installed/authentica
30
30
 
31
31
  ```bash
32
32
  npm install -g palabre
33
+ # or: pnpm add --global palabre
34
+ # or: yarn global add palabre
35
+ # or: bun add --global palabre
33
36
  palabre --version
34
37
  palabre --help
35
38
  ```
@@ -139,6 +142,9 @@ Prérequis : Node.js 20 ou plus, et au moins deux agents déjà installés/authe
139
142
 
140
143
  ```bash
141
144
  npm install -g palabre
145
+ # ou : pnpm add --global palabre
146
+ # ou : yarn global add palabre
147
+ # ou : bun add --global palabre
142
148
  palabre --version
143
149
  palabre --help
144
150
  ```
package/dist/index.js CHANGED
@@ -240,6 +240,25 @@ async function main() {
240
240
  if (parsed.command === "new") {
241
241
  const selection = await runNewWizard(config, messages);
242
242
  if (!selection) {
243
+ if (stayInTuiAfterSession) {
244
+ tuiNotice = messages.new.cancelled;
245
+ parsed.command = "";
246
+ parsed.commandExplicit = false;
247
+ for (;;) {
248
+ renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion, latestVersion: tuiLatestVersion });
249
+ const nextInput = await promptTuiHomeTopic(tuiMode, messages, { notice: tuiNotice });
250
+ tuiNotice = undefined;
251
+ const action = await handleTuiHomeInput(nextInput);
252
+ if (action === "quit")
253
+ return;
254
+ if (action === "continue" || action === "retry")
255
+ continue;
256
+ parsed.flags.mode = tuiMode;
257
+ parsed.flags.renderer = "tui";
258
+ break;
259
+ }
260
+ continue;
261
+ }
243
262
  console.log(messages.new.cancelled);
244
263
  return;
245
264
  }
package/dist/new.js CHANGED
@@ -5,6 +5,8 @@ import { isAgentDetected } from "./agentRegistry.js";
5
5
  import { discoverLocalTools } from "./discovery.js";
6
6
  import { findPresetNameForPair } from "./presets.js";
7
7
  import { MAX_TURNS, turnsOrDefault, validateTurns } from "./limits.js";
8
+ import { accent, bold, brandHeader, card, clearScreen, dim, padBlock, supportsInteractiveOutput, surfaceWidth } from "./renderers/tui-theme.js";
9
+ const interruptedAnswer = "\u0000palabre-interrupted";
8
10
  /**
9
11
  * Lance le wizard interactif `palabre new`.
10
12
  * Détecte les outils locaux, liste les agents de la config et guide la composition du débat.
@@ -18,10 +20,7 @@ export async function runNewWizard(config, messages) {
18
20
  }
19
21
  const rl = await createQuestioner();
20
22
  try {
21
- console.log(messages.new.title);
22
- console.log(messages.new.quitHint);
23
- console.log(messages.new.defaultHint);
24
- console.log("");
23
+ renderWizardIntro(messages);
25
24
  const mode = await askMode(rl, config.defaults?.mode ?? "debate", messages);
26
25
  if (!mode)
27
26
  return undefined;
@@ -176,11 +175,21 @@ async function askMode(rl, defaultMode, messages) {
176
175
  { value: "ask", label: messages.new.modeAsk }
177
176
  ];
178
177
  const fallback = choices.find((choice) => choice.value === defaultMode)?.value ?? "debate";
179
- console.log(messages.new.mode);
180
- choices.forEach((choice, index) => {
181
- const marker = choice.value === fallback ? "(*)" : " ";
182
- console.log(` ${index + 1}) ${marker} ${choice.label}`);
183
- });
178
+ if (supportsInteractiveOutput) {
179
+ const lines = choices.map((choice, index) => {
180
+ const marker = choice.value === fallback ? accent("(*)") : " ";
181
+ return `${bold(`${index + 1})`)} ${marker} ${choice.label}`;
182
+ });
183
+ console.log(padBlock(card(lines, surfaceWidth(), messages.new.mode)).join("\n"));
184
+ console.log("");
185
+ }
186
+ else {
187
+ console.log(messages.new.mode);
188
+ choices.forEach((choice, index) => {
189
+ const marker = choice.value === fallback ? "(*)" : " ";
190
+ console.log(` ${index + 1}) ${marker} ${choice.label}`);
191
+ });
192
+ }
184
193
  while (true) {
185
194
  const answer = await rl.question(`${messages.new.mode} [${fallback}]: `);
186
195
  const value = answer.trim().toLowerCase();
@@ -201,7 +210,33 @@ async function askMode(rl, defaultMode, messages) {
201
210
  }
202
211
  async function createQuestioner() {
203
212
  if (input.isTTY) {
204
- return createInterface({ input, output });
213
+ const rl = createInterface({ input, output });
214
+ return {
215
+ question(prompt) {
216
+ return new Promise((resolve, reject) => {
217
+ let settled = false;
218
+ const cleanup = () => rl.off("SIGINT", onSigint);
219
+ const settle = (value) => {
220
+ if (settled)
221
+ return;
222
+ settled = true;
223
+ cleanup();
224
+ resolve(value);
225
+ };
226
+ const onSigint = () => settle(interruptedAnswer);
227
+ rl.once("SIGINT", onSigint);
228
+ rl.question(prompt).then(settle, (error) => {
229
+ if (settled)
230
+ return;
231
+ cleanup();
232
+ reject(error);
233
+ });
234
+ });
235
+ },
236
+ close() {
237
+ rl.close();
238
+ }
239
+ };
205
240
  }
206
241
  const lines = await readPipedLines();
207
242
  let index = 0;
@@ -376,7 +411,25 @@ function uniqueNames(names) {
376
411
  return names.filter((name, index) => names.indexOf(name) === index);
377
412
  }
378
413
  function isQuit(value) {
379
- return ["q", "quit", "exit"].includes(value.toLowerCase());
414
+ return value === interruptedAnswer || ["q", "quit", "exit"].includes(value.toLowerCase());
415
+ }
416
+ function renderWizardIntro(messages) {
417
+ if (!supportsInteractiveOutput) {
418
+ console.log(messages.new.title);
419
+ console.log(messages.new.quitHint);
420
+ console.log(messages.new.defaultHint);
421
+ console.log("");
422
+ return;
423
+ }
424
+ clearScreen();
425
+ console.log("");
426
+ console.log(padBlock([brandHeader(messages.new.title)]).join("\n"));
427
+ console.log("");
428
+ console.log(padBlock([
429
+ dim(messages.new.quitHint),
430
+ dim(messages.new.defaultHint)
431
+ ]).join("\n"));
432
+ console.log("");
380
433
  }
381
434
  function printCommandPreview(selection, messages) {
382
435
  const explicitCommand = buildExplicitCommand(selection);
@@ -7,6 +7,10 @@ import { createInterface } from "node:readline/promises";
7
7
  import { stdin as input, stdout as output } from "node:process";
8
8
  import { renderTuiAgentsHelp, renderTuiRolesHelp } from "./tui-screens.js";
9
9
  import { accent, bold, composerCard, dim, glyphs, labeledRule, padBlock, surfacePadding, surfaceWidth, violet, wrapLine } from "./tui-theme.js";
10
+ /** Traduit une interruption du composer : premier Ctrl+C = accueil, second = fermeture. */
11
+ export function tuiHomeInterruptInput(kind) {
12
+ return kind === "back" ? { kind: "home" } : undefined;
13
+ }
10
14
  /** Parse `/ollama-url <url>` : renvoie `unknown` avec le message d'usage si l'argument est absent. */
11
15
  export function parseTuiOllamaUrlCommand(parts, messages) {
12
16
  const value = parts[1];
@@ -85,7 +89,7 @@ export async function promptTuiHomeTopic(mode = "debate", messages, options = {}
85
89
  try {
86
90
  const result = await questionWithInterrupt(rl, tuiPrompt(mode, messages.tui.subject, messages, options.notice));
87
91
  if (result.kind !== "answer") {
88
- return undefined;
92
+ return tuiHomeInterruptInput(result.kind);
89
93
  }
90
94
  const answer = result.value;
91
95
  const value = answer.trim();
@@ -24,6 +24,7 @@ class TuiRenderer {
24
24
  spinnerFrame = 0;
25
25
  currentSection = "debate";
26
26
  currentAgent;
27
+ sessionMode = "debate";
27
28
  /** Titre du tour en attente, rendu en tête du prochain bloc `message`. */
28
29
  pendingHeader;
29
30
  constructor(messages, color, interactive) {
@@ -32,6 +33,7 @@ class TuiRenderer {
32
33
  this.interactive = interactive;
33
34
  }
34
35
  start(options, agents = []) {
36
+ this.sessionMode = options.mode;
35
37
  if (this.interactive) {
36
38
  clearScreen();
37
39
  }
@@ -109,13 +111,15 @@ class TuiRenderer {
109
111
  const width = this.width();
110
112
  const folderPath = path.dirname(outputPath);
111
113
  const fileName = path.basename(outputPath);
114
+ const modeCommand = this.sessionMode === "ask" ? "/debat" : "/ask";
112
115
  process.stdout.write(`\n${padBlock(panel([
113
116
  `${success(glyphs().check)} ${bold(this.messages.tui.sessionDone)}`,
114
117
  "",
115
118
  row(this.messages.tui.exportedFile, terminalLink(outputPath, compactFileName(fileName, width - 24))),
116
119
  row(this.messages.tui.exportedFolder, terminalLink(folderPath, compactPath(folderPath, width - 24))),
117
120
  "",
118
- dim(this.messages.tui.sessionHistoryHint)
121
+ `${accent("/retry")} ${dim(this.messages.tui.helpRetry)} ${accent("/new")} ${dim(this.messages.tui.helpNew)} ${accent(modeCommand)} ${dim(this.messages.tui.changeMode)}`,
122
+ `${accent("/history")} ${dim(this.messages.tui.helpHistory)} ${accent("/config")} ${dim(this.messages.tui.helpConfig)} ${accent("/help")} ${dim(this.messages.tui.helpHelp)}`
119
123
  ], width)).join("\n")}\n\n`);
120
124
  }
121
125
  renderSessionHeader(options, agents) {
@@ -344,7 +344,7 @@ export const codes = {
344
344
  reset: "\u001b[0m",
345
345
  bright: "\u001b[1m",
346
346
  dim: "\u001b[2m",
347
- logoViolet: "\u001b[38;2;167;139;250m",
347
+ logoViolet: "\u001b[38;2;139;105;218m",
348
348
  violet: "\u001b[38;5;141m",
349
349
  cyan: "\u001b[36m",
350
350
  gray: "\u001b[38;5;244m",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palabre",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
5
5
  "license": "MIT",
6
6
  "type": "module",