@leg3ndy/otto-bridge 1.1.2 → 1.1.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.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Para o estado atual da arquitetura, capacidades entregues, limitacoes e roadmap
15
15
 
16
16
  Para o corte de arquitetura do `0.9.0`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_0_9_0_RELEASE.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_0_9_0_RELEASE.md).
17
17
 
18
- Para o patch atual `1.1.2`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_2_PATCH.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_2_PATCH.md). Para o corte funcional da linha `1.1.0`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_0_RELEASE.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_0_RELEASE.md).
18
+ Para o patch atual `1.1.4`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_4_PATCH.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_4_PATCH.md). Para o corte funcional da linha `1.1.0`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_0_RELEASE.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_0_RELEASE.md).
19
19
 
20
20
  ## Distribuicao
21
21
 
@@ -38,14 +38,14 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
38
38
 
39
39
  ```bash
40
40
  npm pack
41
- npm install -g ./leg3ndy-otto-bridge-1.1.2.tgz
41
+ npm install -g ./leg3ndy-otto-bridge-1.1.4.tgz
42
42
  ```
43
43
 
44
- Na linha `1.1.2`, `playwright` segue como dependencia obrigatoria no `otto-bridge`. O primeiro `npm install -g @leg3ndy/otto-bridge` pode demorar mais porque instala o browser persistente usado pelo WhatsApp Web e pelos fluxos web em background do bridge.
44
+ Na linha `1.1.4`, `playwright` segue como dependencia obrigatoria no `otto-bridge`. O primeiro `npm install -g @leg3ndy/otto-bridge` pode demorar mais porque instala o browser persistente usado pelo WhatsApp Web e pelos fluxos web em background do bridge.
45
45
 
46
- No macOS, a linha `1.1.2` usa o provider `macos-helper`, um helper `WKWebView` sem Dock para o WhatsApp Web. O helper sobe com user-agent de Chrome moderno para evitar o bloqueio do WhatsApp ao detectar Safari/WebKit. O runtime antigo com Chromium/Playwright fica disponivel apenas como override explicito via `OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER=embedded-playwright`.
46
+ No macOS, a linha `1.1.4` usa o provider `macos-helper`, um helper `WKWebView` sem Dock para o WhatsApp Web. O helper sobe com user-agent de Chrome moderno para evitar o bloqueio do WhatsApp ao detectar Safari/WebKit. O runtime antigo com Chromium/Playwright fica disponivel apenas como override explicito via `OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER=embedded-playwright`.
47
47
 
48
- No nivel arquitetural, o `0.9.0` marcou a mudanca de papel do bridge: ele publica tools e resultados estruturados para o Otto, em vez de injetar resposta pronta como caminho principal do chat. O `1.0.0` oficializou isso como runtime agentico; o `1.1.2` mantem a camada workspace-first com rail de coding, trust/policy por workspace, source control first-class, working set persistido e grounding remoto por repositório, enquanto estabiliza o `Otto Console` com header fixo, intro card sem `model:` e redraw limpo no resize.
48
+ No nivel arquitetural, o `0.9.0` marcou a mudanca de papel do bridge: ele publica tools e resultados estruturados para o Otto, em vez de injetar resposta pronta como caminho principal do chat. O `1.0.0` oficializou isso como runtime agentico; o `1.1.4` mantem a camada workspace-first com rail de coding, trust/policy por workspace, source control first-class, working set persistido e grounding remoto por repositório, enquanto corrige a regressao do `Otto Console` e volta ao renderer fixo com header no topo, transcript acima do composer, menu com setas e palette de comandos ao digitar `/`.
49
49
 
50
50
  ## Publicacao
51
51
 
@@ -152,8 +152,9 @@ Dentro do console, use:
152
152
  - `/workspace attach <path>` para anexar uma pasta/repo novo pelo helper autenticado
153
153
  - `/workspace use <id|n>` para fixar um workspace anexado na sessão atual
154
154
  - `/workspace clear` para limpar o binding atual do chat/sessão
155
+ - `/new` para iniciar uma nova sessão e limpar o contexto local do console
155
156
 
156
- No TTY, o console agora usa buffer alternativo com header fixo no topo: banner, card azul com `cwd`/`bridge` e linha de comandos continuam visíveis enquanto o transcript cresce. O composer fica ancorado no rodapé com placeholder `Peça algo ao Otto`, quebra por largura real do terminal e mantém o transcript do Otto sempre acima sem duplicar a viewport no resize. O intro card não repete `model:`, o footer mostra o modelo ativo em cinza claro, e as mensagens renderizam o usuário com `>` e o assistente com bolinha azul, com espaçamento entre os blocos; `Shift+Tab` alterna o modo de aprovação sem sair do console.
157
+ No TTY, o console agora reaproveita a própria tela do bridge quando você entra no `Otto Console`, em vez de abrir uma segunda viewport logo abaixo do hub. O header é impresso uma vez no topo da sessão, o histórico completo fica salvo no scrollback real do terminal e o conteúdo vai empurrando os blocos para cima naturalmente. O composer continua com placeholder `Peça algo ao Otto`, o footer mantém modelo/tokens/aprovação, e ao digitar `/` o bridge abre uma palette navegável por setas, com `Enter` preenchendo comandos como `/new` para iniciar uma nova sessão local.
157
158
 
158
159
  No modo `OttoAI Thinking`, o terminal agora marca explicitamente o trecho de raciocínio com `Pensando (OttoAI Thinking)` e separa esse bloco da resposta final do Otto.
159
160
 
@@ -169,7 +170,7 @@ Esse comando abre um shell local interativo para instalar extensoes, rodar coman
169
170
 
170
171
  ### WhatsApp Web em background
171
172
 
172
- Fluxo recomendado na linha `1.1.2`:
173
+ Fluxo recomendado na linha `1.1.4`:
173
174
 
174
175
  ```bash
175
176
  otto-bridge extensions --install whatsappweb
@@ -179,13 +180,13 @@ otto-bridge extensions --status whatsappweb
179
180
 
180
181
  O setup agora abre o login do WhatsApp Web no helper/background browser do proprio bridge. Depois do QR code, o Otto usa a sessao local em background, sem depender de aba visivel no Safari.
181
182
 
182
- Contrato da linha `1.1.2`:
183
+ Contrato da linha `1.1.4`:
183
184
 
184
185
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
185
186
  - `otto-bridge`: mantem o browser persistente do WhatsApp vivo em background enquanto o runtime do hub estiver ativo, sem depender de uma aba aberta no Safari
186
187
  - ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
187
188
 
188
- ## Handoff rapido da linha 1.1.2
189
+ ## Handoff rapido da linha 1.1.4
189
190
 
190
191
  Ja fechado no codigo:
191
192
 
@@ -56,11 +56,74 @@ const MAX_RENDERED_LIST_ENTRIES_COMPACT = 10;
56
56
  const MAX_RENDERED_FILE_CHARS = 6_000;
57
57
  const MAX_RENDERED_FILE_CHARS_COMPACT = 1_400;
58
58
  const CONSOLE_PLACEHOLDER = "Peça algo ao Otto";
59
- const CONSOLE_COMMAND_HINT = "/help, /model [fast|thinking], /approval [preview|confirm|trusted], /workspace, /status, /clear, /exit";
59
+ const CONSOLE_COMMAND_HINT = "/help, /model [fast|thinking], /approval [preview|confirm|trusted], /workspace, /status, /clear, /new, /exit";
60
60
  const CONSOLE_COMPOSER_PROMPT_WIDTH = 2;
61
61
  const CONSOLE_COMPOSER_CURSOR_COLUMN = 2;
62
62
  const CONSOLE_EST_CHARS_PER_TOKEN = 4;
63
63
  const CONSOLE_EST_MESSAGE_OVERHEAD_TOKENS = 8;
64
+ const CONSOLE_MENU_HINT = "↑ ↓ navegar • Enter selecionar";
65
+ const CONSOLE_SLASH_SUGGESTIONS = [
66
+ {
67
+ command: "/new",
68
+ insertText: "/new",
69
+ description: "inicia uma nova sessão e limpa o contexto local",
70
+ },
71
+ {
72
+ command: "/help",
73
+ insertText: "/help",
74
+ description: "mostra os comandos disponíveis no console",
75
+ },
76
+ {
77
+ command: "/model fast",
78
+ insertText: "/model fast",
79
+ description: "troca para OttoAI Fast",
80
+ },
81
+ {
82
+ command: "/model thinking",
83
+ insertText: "/model thinking",
84
+ description: "troca para OttoAI Thinking",
85
+ },
86
+ {
87
+ command: "/approval preview",
88
+ insertText: "/approval preview",
89
+ description: "usa permissões padrão no device",
90
+ },
91
+ {
92
+ command: "/approval confirm",
93
+ insertText: "/approval confirm",
94
+ description: "pede confirmação manual por passo",
95
+ },
96
+ {
97
+ command: "/approval trusted",
98
+ insertText: "/approval trusted",
99
+ description: "ativa bypass de permissões no device",
100
+ },
101
+ {
102
+ command: "/workspace",
103
+ insertText: "/workspace",
104
+ description: "mostra o workspace ativo e os anexados",
105
+ },
106
+ {
107
+ command: "/workspace list",
108
+ insertText: "/workspace list",
109
+ description: "lista os workspaces anexados ao device",
110
+ },
111
+ {
112
+ command: "/status",
113
+ insertText: "/status",
114
+ description: "mostra status técnico do bridge e do runtime",
115
+ },
116
+ {
117
+ command: "/clear",
118
+ insertText: "/clear",
119
+ description: "limpa a tela e o contexto local desta sessão",
120
+ },
121
+ {
122
+ command: "/exit",
123
+ insertText: "/exit",
124
+ description: "sai do Otto Console",
125
+ },
126
+ ];
64
127
  class CliRuntimeSession {
65
128
  config;
66
129
  runtime = null;
@@ -387,6 +450,16 @@ function getNextApprovalMode(mode) {
387
450
  }
388
451
  return "preview";
389
452
  }
453
+ export function resolveConsoleSlashSuggestions(value) {
454
+ const normalized = normalizeText(value).toLowerCase();
455
+ if (!normalized.startsWith("/") || normalized.includes("\n")) {
456
+ return [];
457
+ }
458
+ if (normalized === "/") {
459
+ return [...CONSOLE_SLASH_SUGGESTIONS];
460
+ }
461
+ return CONSOLE_SLASH_SUGGESTIONS.filter((item) => item.command.startsWith(normalized));
462
+ }
390
463
  function styleTranscriptLine(text, tone, enabled) {
391
464
  if (!enabled) {
392
465
  return text;
@@ -436,6 +509,21 @@ function buildConsoleFooterApprovalLine(mode, enabled, statusSuffix) {
436
509
  const hint = statusSuffix || "Shift+Tab para alternar";
437
510
  return style(`${state.label} (${hint})`, state.tone === "warning" ? ANSI.red : state.tone === "primary" ? ANSI.amber : ANSI.slate, enabled);
438
511
  }
512
+ function buildConsoleSlashSuggestionLines(suggestions, selectedSuggestionIndex, width, enabled) {
513
+ if (!suggestions.length) {
514
+ return [];
515
+ }
516
+ const suggestionWidth = Math.max(24, width - 4);
517
+ return suggestions.map((item, index) => {
518
+ const selected = index === selectedSuggestionIndex;
519
+ const prefix = selected
520
+ ? style("›", `${ANSI.bold}${ANSI.brandBlue}`, enabled)
521
+ : style(" ", ANSI.slate, enabled);
522
+ const command = style(item.command, selected ? `${ANSI.bold}${ANSI.white}` : ANSI.brandBlue, enabled);
523
+ const description = style(truncate(item.description, Math.max(12, suggestionWidth - item.command.length - 4)), selected ? ANSI.white : ANSI.slate, enabled);
524
+ return `${prefix} ${command} ${description}`;
525
+ });
526
+ }
439
527
  function styleTranscriptPrefix(text, tone, enabled) {
440
528
  if (!enabled || !text) {
441
529
  return text;
@@ -461,6 +549,8 @@ class ConsoleScreenRenderer {
461
549
  draftValue = "";
462
550
  conversationMessages = [];
463
551
  approvalStatusSuffix = null;
552
+ slashSuggestions = [];
553
+ selectedSuggestionIndex = 0;
464
554
  usingAlternateBuffer = false;
465
555
  constructor(modelMode, approvalMode, headerFactory = () => []) {
466
556
  this.modelMode = modelMode;
@@ -520,6 +610,23 @@ class ConsoleScreenRenderer {
520
610
  this.draftValue = value;
521
611
  this.render();
522
612
  }
613
+ setSlashSuggestions(suggestions, selectedSuggestionIndex = 0) {
614
+ this.slashSuggestions = [...suggestions];
615
+ this.selectedSuggestionIndex = Math.max(0, Math.min(selectedSuggestionIndex, Math.max(0, suggestions.length - 1)));
616
+ this.render();
617
+ }
618
+ setComposerState(value, suggestions, selectedSuggestionIndex = 0) {
619
+ this.draftValue = value;
620
+ this.slashSuggestions = [...suggestions];
621
+ this.selectedSuggestionIndex = Math.max(0, Math.min(selectedSuggestionIndex, Math.max(0, suggestions.length - 1)));
622
+ this.render();
623
+ }
624
+ resetComposer() {
625
+ this.draftValue = "";
626
+ this.slashSuggestions = [];
627
+ this.selectedSuggestionIndex = 0;
628
+ this.render();
629
+ }
523
630
  setConversationMessages(messages) {
524
631
  this.conversationMessages = [...messages];
525
632
  this.render();
@@ -568,10 +675,12 @@ class ConsoleScreenRenderer {
568
675
  const headerLines = this.headerFactory();
569
676
  const separator = style("─".repeat(width), ANSI.brandBlue, enabled);
570
677
  const composer = renderConsoleComposerLines(this.draftValue, width, enabled);
678
+ const suggestionLines = buildConsoleSlashSuggestionLines(this.slashSuggestions, this.selectedSuggestionIndex, width, enabled);
571
679
  const usageTokens = Math.min(estimateConsoleContextTokens(this.conversationMessages, this.draftValue), getCliModelContextWindowTokens(this.modelMode));
572
680
  const footerLines = [
573
681
  separator,
574
682
  ...composer.renderedLines,
683
+ ...suggestionLines,
575
684
  separator,
576
685
  buildConsoleFooterStatusLine(width, this.modelMode, usageTokens, enabled),
577
686
  buildConsoleFooterApprovalLine(this.approvalMode, enabled, this.approvalStatusSuffix),
@@ -591,6 +700,20 @@ class ConsoleScreenRenderer {
591
700
  output.write("\u001b[?25h");
592
701
  }
593
702
  }
703
+ export function createConsoleScreenRenderer(modelMode, approvalMode, runtimeSession, options) {
704
+ const canRender = (options?.ansiEnabled ?? supportsAnsi())
705
+ && (options?.inputIsTTY ?? Boolean(input.isTTY))
706
+ && (options?.outputIsTTY ?? Boolean(output.isTTY))
707
+ && (options?.hasRawMode ?? typeof input.setRawMode === "function");
708
+ if (!canRender) {
709
+ return null;
710
+ }
711
+ const renderer = new ConsoleScreenRenderer(modelMode, approvalMode, () => buildConsoleHeaderLines(runtimeSession));
712
+ if (options?.autoActivate ?? true) {
713
+ renderer.activate();
714
+ }
715
+ return renderer;
716
+ }
594
717
  export function resolveCliModelMode(value) {
595
718
  const normalized = normalizeText(value).toLowerCase();
596
719
  if (!normalized) {
@@ -1236,6 +1359,30 @@ export function tryConsumeControlSequence(buffer) {
1236
1359
  };
1237
1360
  }
1238
1361
  }
1362
+ const moveUpSequences = [
1363
+ "\u001b[A",
1364
+ "\u001bOA",
1365
+ ];
1366
+ for (const sequence of moveUpSequences) {
1367
+ if (buffer.startsWith(sequence)) {
1368
+ return {
1369
+ consumed: sequence.length,
1370
+ action: "move_up",
1371
+ };
1372
+ }
1373
+ }
1374
+ const moveDownSequences = [
1375
+ "\u001b[B",
1376
+ "\u001bOB",
1377
+ ];
1378
+ for (const sequence of moveDownSequences) {
1379
+ if (buffer.startsWith(sequence)) {
1380
+ return {
1381
+ consumed: sequence.length,
1382
+ action: "move_down",
1383
+ };
1384
+ }
1385
+ }
1239
1386
  if (buffer === "\u001b" || /^\u001b\[[0-9;?]*$/.test(buffer)) {
1240
1387
  return {
1241
1388
  consumed: 0,
@@ -1274,55 +1421,84 @@ async function askConsoleInput(rl, options) {
1274
1421
  return await new Promise((resolve, reject) => {
1275
1422
  const enabled = supportsAnsi();
1276
1423
  const ui = options?.ui && options.ui.isActive() ? options.ui : null;
1277
- const availableWidth = Number(output.columns || 96);
1278
- const innerWidth = Math.max(42, Math.min(availableWidth - 8, 116));
1279
- const sectionTopOffsetFromInputLine = 1;
1280
1424
  let value = "";
1281
1425
  let renderedOnce = false;
1426
+ let renderedCursorLineIndex = 0;
1282
1427
  let controlBuffer = "";
1428
+ let selectedSuggestionIndex = 0;
1429
+ const getPromptWidth = () => Math.max(48, Number(output.columns || 96));
1430
+ const getModelMode = () => options?.getModelMode?.() || "fast";
1431
+ const getApprovalMode = () => options?.getApprovalMode?.() || "preview";
1432
+ const getConversationMessages = () => options?.getConversationMessages?.() || [];
1433
+ const getVisibleSuggestions = () => {
1434
+ const suggestions = resolveConsoleSlashSuggestions(value).slice(0, 6);
1435
+ if (selectedSuggestionIndex >= suggestions.length) {
1436
+ selectedSuggestionIndex = Math.max(0, suggestions.length - 1);
1437
+ }
1438
+ return suggestions;
1439
+ };
1283
1440
  const cleanup = () => {
1284
1441
  input.removeListener("data", onData);
1442
+ output.off("resize", renderPromptBlock);
1443
+ if (!ui && renderedOnce) {
1444
+ cursorTo(output, 0);
1445
+ moveCursor(output, 0, -renderedCursorLineIndex);
1446
+ clearScreenDown(output);
1447
+ }
1285
1448
  input.setRawMode(false);
1286
1449
  input.pause();
1287
1450
  rl.resume();
1288
- ui?.setDraftValue("");
1289
- };
1290
- const renderInputContent = () => {
1291
- const promptPlain = "> ";
1292
- const promptStyled = style(">", `${ANSI.bold}${ANSI.white}`, enabled);
1293
- const maxValueLength = Math.max(0, innerWidth - promptPlain.length);
1294
- if (!value) {
1295
- const placeholder = truncate(CONSOLE_PLACEHOLDER, maxValueLength);
1296
- const padded = `${style(placeholder, ANSI.slate, enabled)}${" ".repeat(Math.max(0, maxValueLength - placeholder.length))}`;
1297
- return `${promptStyled} ${padded}`;
1298
- }
1299
- const visibleValue = value.length > maxValueLength
1300
- ? value.slice(value.length - maxValueLength)
1301
- : value;
1302
- return `${promptStyled} ${visibleValue}${" ".repeat(Math.max(0, maxValueLength - visibleValue.length))}`;
1451
+ ui?.resetComposer();
1303
1452
  };
1304
- const renderLegacyInput = () => {
1453
+ const renderPromptBlock = () => {
1305
1454
  if (ui) {
1306
- ui.setDraftValue(value);
1455
+ ui.setComposerState(value, getVisibleSuggestions(), selectedSuggestionIndex);
1307
1456
  return;
1308
1457
  }
1458
+ const width = getPromptWidth();
1459
+ const composer = renderConsoleComposerLines(value, width, enabled);
1460
+ const modelMode = getModelMode();
1461
+ const approvalMode = getApprovalMode();
1462
+ const usageTokens = Math.min(estimateConsoleContextTokens(getConversationMessages(), value), getCliModelContextWindowTokens(modelMode));
1463
+ const suggestionLines = buildConsoleSlashSuggestionLines(getVisibleSuggestions(), selectedSuggestionIndex, width, enabled);
1464
+ const blockLines = [
1465
+ style("─".repeat(width), ANSI.brandBlue, enabled),
1466
+ ...composer.renderedLines,
1467
+ ...suggestionLines,
1468
+ style("─".repeat(width), ANSI.brandBlue, enabled),
1469
+ buildConsoleFooterStatusLine(width, modelMode, usageTokens, enabled),
1470
+ buildConsoleFooterApprovalLine(approvalMode, enabled),
1471
+ ];
1309
1472
  if (renderedOnce) {
1310
1473
  cursorTo(output, 0);
1311
- moveCursor(output, 0, -sectionTopOffsetFromInputLine);
1474
+ moveCursor(output, 0, -renderedCursorLineIndex);
1475
+ clearScreenDown(output);
1312
1476
  }
1313
1477
  else {
1314
1478
  renderedOnce = true;
1315
1479
  }
1316
- const top = renderPromptFrameLine(innerWidth + 2, "", "┐");
1317
- const border = style("│", ANSI.brandBlue, enabled);
1318
- const middle = `${border} ${renderInputContent()} ${border}`;
1319
- const bottom = renderPromptFrameLine(innerWidth + 2, "└", "┘");
1320
- clearScreenDown(output);
1321
- output.write(`${top}\n${middle}\n${bottom}\n`);
1480
+ output.write(blockLines.join("\n"));
1481
+ renderedCursorLineIndex = 1 + composer.cursorLineIndex;
1482
+ const linesBelowCursor = blockLines.length - 1 - renderedCursorLineIndex;
1322
1483
  cursorTo(output, 0);
1323
- moveCursor(output, 0, -2);
1324
- const visibleValueLength = Math.min(value.length, Math.max(0, innerWidth - 2));
1325
- cursorTo(output, 4 + visibleValueLength);
1484
+ if (linesBelowCursor > 0) {
1485
+ moveCursor(output, 0, -linesBelowCursor);
1486
+ }
1487
+ cursorTo(output, Math.min(width - 1, CONSOLE_COMPOSER_CURSOR_COLUMN + composer.cursorColumn));
1488
+ };
1489
+ const applySelectedSuggestion = () => {
1490
+ const suggestions = getVisibleSuggestions();
1491
+ const selected = suggestions[selectedSuggestionIndex];
1492
+ if (!selected) {
1493
+ return false;
1494
+ }
1495
+ if (normalizeText(value) === selected.insertText) {
1496
+ return false;
1497
+ }
1498
+ value = selected.insertText;
1499
+ selectedSuggestionIndex = 0;
1500
+ renderPromptBlock();
1501
+ return true;
1326
1502
  };
1327
1503
  const consumeControlBuffer = () => {
1328
1504
  while (controlBuffer.length > 0) {
@@ -1339,12 +1515,31 @@ async function askConsoleInput(rl, options) {
1339
1515
  continue;
1340
1516
  }
1341
1517
  if (parsed.action === "cycle_approval") {
1342
- void options?.onCycleApprovalMode?.();
1518
+ void Promise.resolve(options?.onCycleApprovalMode?.()).finally(() => {
1519
+ renderPromptBlock();
1520
+ });
1521
+ continue;
1522
+ }
1523
+ if (parsed.action === "move_up") {
1524
+ const suggestions = getVisibleSuggestions();
1525
+ if (suggestions.length > 0) {
1526
+ selectedSuggestionIndex = selectedSuggestionIndex > 0 ? selectedSuggestionIndex - 1 : suggestions.length - 1;
1527
+ renderPromptBlock();
1528
+ }
1529
+ continue;
1530
+ }
1531
+ if (parsed.action === "move_down") {
1532
+ const suggestions = getVisibleSuggestions();
1533
+ if (suggestions.length > 0) {
1534
+ selectedSuggestionIndex = selectedSuggestionIndex < suggestions.length - 1 ? selectedSuggestionIndex + 1 : 0;
1535
+ renderPromptBlock();
1536
+ }
1343
1537
  continue;
1344
1538
  }
1345
1539
  if (parsed.action === "newline") {
1346
1540
  value += "\n";
1347
- renderLegacyInput();
1541
+ selectedSuggestionIndex = 0;
1542
+ renderPromptBlock();
1348
1543
  }
1349
1544
  }
1350
1545
  };
@@ -1362,6 +1557,9 @@ async function askConsoleInput(rl, options) {
1362
1557
  return;
1363
1558
  }
1364
1559
  if (char === "\r" || char === "\n") {
1560
+ if (applySelectedSuggestion()) {
1561
+ continue;
1562
+ }
1365
1563
  cleanup();
1366
1564
  if (!ui) {
1367
1565
  output.write("\n");
@@ -1371,17 +1569,22 @@ async function askConsoleInput(rl, options) {
1371
1569
  }
1372
1570
  if (char === "\u007f" || char === "\b") {
1373
1571
  value = value.slice(0, -1);
1374
- renderLegacyInput();
1572
+ selectedSuggestionIndex = 0;
1573
+ renderPromptBlock();
1375
1574
  continue;
1376
1575
  }
1377
1576
  if (char === "\t") {
1577
+ applySelectedSuggestion();
1378
1578
  continue;
1379
1579
  }
1380
1580
  value += char;
1381
- renderLegacyInput();
1581
+ renderPromptBlock();
1382
1582
  }
1383
1583
  };
1384
- renderLegacyInput();
1584
+ if (!ui) {
1585
+ output.on("resize", renderPromptBlock);
1586
+ }
1587
+ renderPromptBlock();
1385
1588
  input.on("data", onData);
1386
1589
  });
1387
1590
  }
@@ -1683,12 +1886,9 @@ async function followConsoleJob(rl, config, jobId) {
1683
1886
  }
1684
1887
  async function runOttoConsole(rl, config, runtimeSession, options) {
1685
1888
  let activeModel = "fast";
1686
- const sessionId = randomUUID();
1889
+ let sessionId = randomUUID();
1687
1890
  const conversation = [];
1688
- const ui = supportsAnsi() && typeof input.setRawMode === "function" && input.isTTY && output.isTTY
1689
- ? new ConsoleScreenRenderer(activeModel, config.approvalMode, () => buildConsoleHeaderLines(runtimeSession))
1690
- : null;
1691
- ui?.activate();
1891
+ const ui = createConsoleScreenRenderer(activeModel, config.approvalMode, runtimeSession);
1692
1892
  if (!ui) {
1693
1893
  printConsoleScreen(runtimeSession);
1694
1894
  }
@@ -1766,6 +1966,9 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1766
1966
  return await askConsoleInput(rl, {
1767
1967
  ui,
1768
1968
  onCycleApprovalMode: cycleApprovalMode,
1969
+ getConversationMessages: () => conversation,
1970
+ getModelMode: () => activeModel,
1971
+ getApprovalMode: () => config.approvalMode,
1769
1972
  });
1770
1973
  };
1771
1974
  const askConsoleDecision = async (promptText, defaultValue = true) => {
@@ -1779,7 +1982,7 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1779
1982
  };
1780
1983
  const printConsoleHelp = () => {
1781
1984
  emitConsoleEntry("Console", "headline");
1782
- emitConsoleEntry("Comandos: /help, /model [fast|thinking], /approval [preview|confirm|trusted], /workspace, /status, /clear, /exit", "muted");
1985
+ emitConsoleEntry("Comandos: /help, /model [fast|thinking], /approval [preview|confirm|trusted], /workspace, /status, /clear, /new, /exit", "muted");
1783
1986
  emitConsoleEntry("Composer: Enter envia, Shift+Enter adiciona linha e Shift+Tab alterna o modo de aprovação.", "muted");
1784
1987
  emitConsoleEntry("Workspace: /workspace list, /workspace attach <path>, /workspace use <id|n>, /workspace clear", "muted");
1785
1988
  };
@@ -1963,6 +2166,17 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1963
2166
  emitConsoleEntry("Contexto local do console limpo.", "muted");
1964
2167
  return;
1965
2168
  }
2169
+ if (normalizedPrompt === "/new") {
2170
+ sessionId = randomUUID();
2171
+ conversation.splice(0, conversation.length);
2172
+ ui?.clearTranscript();
2173
+ renderConversationState();
2174
+ if (!ui) {
2175
+ printConsoleScreen(runtimeSession);
2176
+ }
2177
+ emitConsoleEntry("Nova sessão iniciada. Contexto local reiniciado.", "muted");
2178
+ return;
2179
+ }
1966
2180
  if (normalizedPrompt === "/model") {
1967
2181
  emitConsoleEntry(`Modelo ativo: ${getCliModelLabel(activeModel)}.`, "muted");
1968
2182
  emitConsoleEntry("Use /model fast ou /model thinking para trocar.", "muted");
@@ -2028,9 +2242,6 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
2028
2242
  emitUserPrompt(normalizedPrompt);
2029
2243
  emitConsoleEntry("", "muted");
2030
2244
  conversation.push({ role: "user", content: normalizedPrompt });
2031
- while (conversation.length > 18) {
2032
- conversation.shift();
2033
- }
2034
2245
  renderConversationState();
2035
2246
  let streamedAssistant = "";
2036
2247
  let assistantEntryId = null;
@@ -2134,9 +2345,6 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
2134
2345
  }
2135
2346
  if (finalAssistantSummary) {
2136
2347
  conversation.push({ role: "assistant", content: finalAssistantSummary });
2137
- while (conversation.length > 18) {
2138
- conversation.shift();
2139
- }
2140
2348
  renderConversationState();
2141
2349
  }
2142
2350
  };
@@ -2192,52 +2400,112 @@ async function printHelpView(rl) {
2192
2400
  { text: "otto-bridge version | otto-bridge unpair", tone: "primary" },
2193
2401
  { text: "Mostra a versao instalada ou remove o pairing local.", tone: "muted" },
2194
2402
  { text: "", tone: "muted" },
2195
- { text: "Dentro do console: /help, /model fast|thinking, /approval preview|confirm|trusted, /status, /clear, /exit", tone: "muted" },
2403
+ { text: "Dentro do console: /help, /model fast|thinking, /approval preview|confirm|trusted, /status, /clear, /new, /exit", tone: "muted" },
2196
2404
  ]));
2197
2405
  await pauseForEnter(rl);
2198
2406
  }
2199
- async function pickHomeChoice(rl, paired) {
2200
- printSection("Home");
2407
+ function renderHomeOptionLine(label, selected) {
2408
+ if (!selected) {
2409
+ return ` ${label}`;
2410
+ }
2411
+ return `${style("▸", ANSI.brandBlue, supportsAnsi())} ${style(label, `${ANSI.bold}${ANSI.white}`, supportsAnsi())}`;
2412
+ }
2413
+ async function pickHomeChoice(rl, paired, renderBaseScreen) {
2201
2414
  const options = paired
2202
2415
  ? [
2203
- `${style("1.", ANSI.brandBlue, supportsAnsi())} Otto Console`,
2204
- `${style("2.", ANSI.brandBlue, supportsAnsi())} Terminal`,
2205
- `${style("3.", ANSI.brandBlue, supportsAnsi())} Setup / parear novamente`,
2206
- `${style("4.", ANSI.brandBlue, supportsAnsi())} Status detalhado`,
2207
- `${style("5.", ANSI.brandBlue, supportsAnsi())} Extensões`,
2208
- `${style("6.", ANSI.brandBlue, supportsAnsi())} Ajuda`,
2209
- `${style("7.", ANSI.brandBlue, supportsAnsi())} Sair`,
2416
+ { value: "console", label: "Otto Console" },
2417
+ { value: "terminal", label: "Terminal" },
2418
+ { value: "setup", label: "Setup / parear novamente" },
2419
+ { value: "status", label: "Status detalhado" },
2420
+ { value: "extensions", label: "Extensões" },
2421
+ { value: "help", label: "Ajuda" },
2422
+ { value: "exit", label: "Sair" },
2210
2423
  ]
2211
2424
  : [
2212
- `${style("1.", ANSI.brandBlue, supportsAnsi())} Pairing setup`,
2213
- `${style("2.", ANSI.brandBlue, supportsAnsi())} Terminal`,
2214
- `${style("3.", ANSI.brandBlue, supportsAnsi())} Ajuda`,
2215
- `${style("4.", ANSI.brandBlue, supportsAnsi())} Sair`,
2425
+ { value: "setup", label: "Pairing setup" },
2426
+ { value: "terminal", label: "Terminal" },
2427
+ { value: "help", label: "Ajuda" },
2428
+ { value: "exit", label: "Sair" },
2216
2429
  ];
2217
- console.log(options.join("\n"));
2218
- const answer = await ask(rl, "Escolha");
2219
- if (!paired) {
2220
- if (answer === "1")
2221
- return "setup";
2222
- if (answer === "2")
2223
- return "terminal";
2224
- if (answer === "3")
2225
- return "help";
2430
+ if (!supportsAnsi() || typeof input.setRawMode !== "function" || !input.isTTY) {
2431
+ printSection("Home");
2432
+ console.log(options.map((option, index) => `${style(`${index + 1}.`, ANSI.brandBlue, supportsAnsi())} ${option.label}`).join("\n"));
2433
+ const answer = await ask(rl, "Escolha");
2434
+ const numericIndex = Number(answer);
2435
+ if (Number.isInteger(numericIndex) && numericIndex >= 1 && numericIndex <= options.length) {
2436
+ return options[numericIndex - 1].value;
2437
+ }
2226
2438
  return "exit";
2227
2439
  }
2228
- if (answer === "1")
2229
- return "console";
2230
- if (answer === "2")
2231
- return "terminal";
2232
- if (answer === "3")
2233
- return "setup";
2234
- if (answer === "4")
2235
- return "status";
2236
- if (answer === "5")
2237
- return "extensions";
2238
- if (answer === "6")
2239
- return "help";
2240
- return "exit";
2440
+ rl.pause();
2441
+ input.setRawMode(true);
2442
+ input.resume();
2443
+ return await new Promise((resolve, reject) => {
2444
+ let selectedIndex = 0;
2445
+ let controlBuffer = "";
2446
+ const cleanup = () => {
2447
+ input.removeListener("data", onData);
2448
+ input.setRawMode(false);
2449
+ input.pause();
2450
+ rl.resume();
2451
+ };
2452
+ const render = () => {
2453
+ renderBaseScreen();
2454
+ printSection("Home");
2455
+ console.log(options.map((option, index) => renderHomeOptionLine(option.label, index === selectedIndex)).join("\n"));
2456
+ console.log("");
2457
+ printSoft(CONSOLE_MENU_HINT);
2458
+ };
2459
+ const consumeControlBuffer = () => {
2460
+ while (controlBuffer.length > 0) {
2461
+ const parsed = tryConsumeControlSequence(controlBuffer);
2462
+ if (!parsed) {
2463
+ controlBuffer = "";
2464
+ return;
2465
+ }
2466
+ if (parsed.action === "incomplete") {
2467
+ return;
2468
+ }
2469
+ controlBuffer = controlBuffer.slice(parsed.consumed);
2470
+ if (parsed.action === "move_up") {
2471
+ selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1;
2472
+ render();
2473
+ continue;
2474
+ }
2475
+ if (parsed.action === "move_down") {
2476
+ selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0;
2477
+ render();
2478
+ continue;
2479
+ }
2480
+ if (parsed.action === "ignore" || parsed.action === "cycle_approval" || parsed.action === "newline") {
2481
+ continue;
2482
+ }
2483
+ }
2484
+ };
2485
+ const onData = (chunk) => {
2486
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
2487
+ for (const char of Array.from(text)) {
2488
+ if (controlBuffer || char === "\u001b") {
2489
+ controlBuffer += char;
2490
+ consumeControlBuffer();
2491
+ continue;
2492
+ }
2493
+ if (char === "\u0003") {
2494
+ cleanup();
2495
+ reject(createCliExitError());
2496
+ return;
2497
+ }
2498
+ if (char === "\r" || char === "\n") {
2499
+ cleanup();
2500
+ output.write("\n");
2501
+ resolve(options[selectedIndex].value);
2502
+ return;
2503
+ }
2504
+ }
2505
+ };
2506
+ render();
2507
+ input.on("data", onData);
2508
+ });
2241
2509
  }
2242
2510
  export async function launchInteractiveCli(options) {
2243
2511
  let rl = await createPromptInterface();
@@ -2264,7 +2532,7 @@ export async function launchInteractiveCli(options) {
2264
2532
  await runtimeSession.waitForReady();
2265
2533
  for (;;) {
2266
2534
  printHubScreen(runtimeSession);
2267
- const choice = await pickHomeChoice(rl, true);
2535
+ const choice = await pickHomeChoice(rl, true, () => printHubScreen(runtimeSession));
2268
2536
  if (choice === "exit") {
2269
2537
  break;
2270
2538
  }
package/dist/main.js CHANGED
@@ -129,6 +129,7 @@ Console:
129
129
  /workspace clear
130
130
  /status
131
131
  /clear
132
+ /new
132
133
  /exit
133
134
 
134
135
  Examples:
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "1.1.2";
2
+ export const BRIDGE_VERSION = "1.1.4";
3
3
  export const BRIDGE_PACKAGE_NAME = "@leg3ndy/otto-bridge";
4
4
  export const DEFAULT_API_BASE_URL = "http://localhost:8000";
5
5
  export const DEFAULT_POLL_INTERVAL_MS = 3000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leg3ndy/otto-bridge",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",
@@ -24,7 +24,7 @@ if (!existsSync(mainPath)) {
24
24
  process.exit(0);
25
25
  }
26
26
 
27
- console.log("\n[otto-bridge] Welcome to OTTOAI 1.1.2");
27
+ console.log("\n[otto-bridge] Welcome to OTTOAI 1.1.4");
28
28
  console.log("[otto-bridge] Vamos iniciar o setup interativo do bridge.\n");
29
29
 
30
30
  const result = spawnSync(process.execPath, [mainPath, "setup", "--postinstall"], {