@leg3ndy/otto-bridge 1.1.3 → 1.1.5

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.3`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_3_PATCH.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_3_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.5`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_5_PATCH.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_5_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.3.tgz
41
+ npm install -g ./leg3ndy-otto-bridge-1.1.5.tgz
42
42
  ```
43
43
 
44
- Na linha `1.1.3`, `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.5`, `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.3` 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.5` 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.3` 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 troca o `Otto Console` para scrollback real, menu com setas e palette de comandos ao digitar `/`.
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.5` 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 o `Otto Console` para voltar ao scrollback real, aceitar digitação enquanto o Otto streama e reaproveitar a tela do hub sem duplicar o banner.
49
49
 
50
50
  ## Publicacao
51
51
 
@@ -170,7 +170,7 @@ Esse comando abre um shell local interativo para instalar extensoes, rodar coman
170
170
 
171
171
  ### WhatsApp Web em background
172
172
 
173
- Fluxo recomendado na linha `1.1.3`:
173
+ Fluxo recomendado na linha `1.1.5`:
174
174
 
175
175
  ```bash
176
176
  otto-bridge extensions --install whatsappweb
@@ -180,13 +180,13 @@ otto-bridge extensions --status whatsappweb
180
180
 
181
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.
182
182
 
183
- Contrato da linha `1.1.3`:
183
+ Contrato da linha `1.1.5`:
184
184
 
185
185
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
186
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
187
187
  - ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
188
188
 
189
- ## Handoff rapido da linha 1.1.3
189
+ ## Handoff rapido da linha 1.1.5
190
190
 
191
191
  Ja fechado no codigo:
192
192
 
@@ -509,6 +509,21 @@ function buildConsoleFooterApprovalLine(mode, enabled, statusSuffix) {
509
509
  const hint = statusSuffix || "Shift+Tab para alternar";
510
510
  return style(`${state.label} (${hint})`, state.tone === "warning" ? ANSI.red : state.tone === "primary" ? ANSI.amber : ANSI.slate, enabled);
511
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
+ }
512
527
  function styleTranscriptPrefix(text, tone, enabled) {
513
528
  if (!enabled || !text) {
514
529
  return text;
@@ -525,7 +540,7 @@ class ConsoleScreenRenderer {
525
540
  modelMode;
526
541
  approvalMode;
527
542
  headerFactory;
528
- transcript = [];
543
+ liveEntries = [];
529
544
  onResize = () => {
530
545
  this.render();
531
546
  };
@@ -534,7 +549,10 @@ class ConsoleScreenRenderer {
534
549
  draftValue = "";
535
550
  conversationMessages = [];
536
551
  approvalStatusSuffix = null;
537
- usingAlternateBuffer = false;
552
+ slashSuggestions = [];
553
+ selectedSuggestionIndex = 0;
554
+ renderedOnce = false;
555
+ renderedCursorLineIndex = 0;
538
556
  constructor(modelMode, approvalMode, headerFactory = () => []) {
539
557
  this.modelMode = modelMode;
540
558
  this.approvalMode = approvalMode;
@@ -545,8 +563,6 @@ class ConsoleScreenRenderer {
545
563
  return;
546
564
  }
547
565
  this.active = true;
548
- output.write("\u001b[?1049h");
549
- this.usingAlternateBuffer = true;
550
566
  output.on("resize", this.onResize);
551
567
  this.render();
552
568
  }
@@ -555,34 +571,40 @@ class ConsoleScreenRenderer {
555
571
  return;
556
572
  }
557
573
  output.off("resize", this.onResize);
574
+ this.clearRenderBlock();
558
575
  this.active = false;
559
576
  output.write("\u001b[?25h");
560
- if (this.usingAlternateBuffer) {
561
- output.write("\u001b[?1049l");
562
- this.usingAlternateBuffer = false;
563
- }
564
577
  }
565
578
  isActive() {
566
579
  return this.active;
567
580
  }
568
581
  clearTranscript() {
569
- this.transcript.splice(0, this.transcript.length);
582
+ this.liveEntries.splice(0, this.liveEntries.length);
583
+ this.renderedOnce = false;
584
+ clearScreen();
585
+ output.write(this.headerFactory().join("\n"));
586
+ output.write("\n\n");
570
587
  this.render();
571
588
  }
572
589
  pushEntry(entry) {
573
590
  const id = this.nextEntryId++;
574
- this.transcript.push({
591
+ const nextEntry = {
575
592
  id,
576
593
  ...entry,
577
- });
578
- this.render();
594
+ };
595
+ if (nextEntry.text.length === 0 && (Boolean(nextEntry.prefix) || nextEntry.tone === "reasoning")) {
596
+ this.liveEntries.push(nextEntry);
597
+ this.render();
598
+ return id;
599
+ }
600
+ this.printCommittedEntry(nextEntry);
579
601
  return id;
580
602
  }
581
603
  appendToEntry(id, text) {
582
604
  if (!id || !text) {
583
605
  return;
584
606
  }
585
- const entry = this.transcript.find((item) => item.id === id);
607
+ const entry = this.liveEntries.find((item) => item.id === id);
586
608
  if (!entry) {
587
609
  return;
588
610
  }
@@ -593,6 +615,32 @@ class ConsoleScreenRenderer {
593
615
  this.draftValue = value;
594
616
  this.render();
595
617
  }
618
+ setSlashSuggestions(suggestions, selectedSuggestionIndex = 0) {
619
+ this.slashSuggestions = [...suggestions];
620
+ this.selectedSuggestionIndex = Math.max(0, Math.min(selectedSuggestionIndex, Math.max(0, suggestions.length - 1)));
621
+ this.render();
622
+ }
623
+ setComposerState(value, suggestions, selectedSuggestionIndex = 0) {
624
+ this.draftValue = value;
625
+ this.slashSuggestions = [...suggestions];
626
+ this.selectedSuggestionIndex = Math.max(0, Math.min(selectedSuggestionIndex, Math.max(0, suggestions.length - 1)));
627
+ this.render();
628
+ }
629
+ resetComposer() {
630
+ this.draftValue = "";
631
+ this.slashSuggestions = [];
632
+ this.selectedSuggestionIndex = 0;
633
+ this.render();
634
+ }
635
+ commitLiveEntries() {
636
+ if (this.liveEntries.length === 0) {
637
+ return;
638
+ }
639
+ const width = this.getRenderWidth();
640
+ const lines = this.liveEntries.flatMap((entry) => this.buildEntryLines(entry, width));
641
+ this.liveEntries.splice(0, this.liveEntries.length);
642
+ this.printLinesAbove(lines);
643
+ }
596
644
  setConversationMessages(messages) {
597
645
  this.conversationMessages = [...messages];
598
646
  this.render();
@@ -631,41 +679,84 @@ class ConsoleScreenRenderer {
631
679
  }
632
680
  return rendered;
633
681
  }
682
+ getRenderWidth() {
683
+ return Math.max(48, Number(output.columns || 96));
684
+ }
685
+ clearRenderBlock() {
686
+ if (!this.renderedOnce) {
687
+ return;
688
+ }
689
+ output.write("\u001b[?25l");
690
+ cursorTo(output, 0);
691
+ if (this.renderedCursorLineIndex > 0) {
692
+ moveCursor(output, 0, -this.renderedCursorLineIndex);
693
+ }
694
+ clearScreenDown(output);
695
+ output.write("\u001b[?25h");
696
+ this.renderedOnce = false;
697
+ this.renderedCursorLineIndex = 0;
698
+ }
699
+ printLinesAbove(lines) {
700
+ if (!this.active) {
701
+ return;
702
+ }
703
+ this.clearRenderBlock();
704
+ if (lines.length > 0) {
705
+ output.write(lines.join("\n"));
706
+ output.write("\n");
707
+ }
708
+ this.render();
709
+ }
710
+ printCommittedEntry(entry) {
711
+ this.printLinesAbove(this.buildEntryLines(entry, this.getRenderWidth()));
712
+ }
634
713
  render() {
635
714
  if (!this.active) {
636
715
  return;
637
716
  }
638
717
  const enabled = supportsAnsi();
639
- const width = Math.max(48, Number(output.columns || 96));
640
- const height = Math.max(12, Number(output.rows || 24));
641
- const headerLines = this.headerFactory();
718
+ const width = this.getRenderWidth();
642
719
  const separator = style("─".repeat(width), ANSI.brandBlue, enabled);
643
720
  const composer = renderConsoleComposerLines(this.draftValue, width, enabled);
721
+ const suggestionLines = buildConsoleSlashSuggestionLines(this.slashSuggestions, this.selectedSuggestionIndex, width, enabled);
644
722
  const usageTokens = Math.min(estimateConsoleContextTokens(this.conversationMessages, this.draftValue), getCliModelContextWindowTokens(this.modelMode));
645
- const footerLines = [
723
+ const liveLines = this.liveEntries.flatMap((entry) => this.buildEntryLines(entry, width));
724
+ const blockLines = [
725
+ ...liveLines,
646
726
  separator,
647
727
  ...composer.renderedLines,
728
+ ...suggestionLines,
648
729
  separator,
649
730
  buildConsoleFooterStatusLine(width, this.modelMode, usageTokens, enabled),
650
731
  buildConsoleFooterApprovalLine(this.approvalMode, enabled, this.approvalStatusSuffix),
651
732
  ];
652
- const visibleHeader = headerLines.slice(0, Math.max(0, height - footerLines.length));
653
- const transcriptHeight = Math.max(0, height - visibleHeader.length - footerLines.length);
654
- const transcriptLines = this.transcript.flatMap((entry) => this.buildEntryLines(entry, width));
655
- const visibleTranscript = transcriptLines.slice(-transcriptHeight);
656
- const paddedTranscript = [
657
- ...Array.from({ length: Math.max(0, transcriptHeight - visibleTranscript.length) }, () => ""),
658
- ...visibleTranscript,
659
- ];
733
+ this.clearRenderBlock();
660
734
  output.write("\u001b[?25l");
661
- output.write("\u001b[H\u001b[2J");
662
- output.write([...visibleHeader, ...paddedTranscript, ...footerLines].join("\n"));
663
- cursorTo(output, Math.min(width - 1, CONSOLE_COMPOSER_CURSOR_COLUMN + composer.cursorColumn), visibleHeader.length + transcriptHeight + 1 + composer.cursorLineIndex);
735
+ output.write(blockLines.join("\n"));
736
+ this.renderedOnce = true;
737
+ this.renderedCursorLineIndex = liveLines.length + 1 + composer.cursorLineIndex;
738
+ const linesBelowCursor = blockLines.length - 1 - this.renderedCursorLineIndex;
739
+ cursorTo(output, Math.min(width - 1, CONSOLE_COMPOSER_CURSOR_COLUMN + composer.cursorColumn));
740
+ if (linesBelowCursor > 0) {
741
+ moveCursor(output, 0, -linesBelowCursor);
742
+ }
743
+ cursorTo(output, Math.min(width - 1, CONSOLE_COMPOSER_CURSOR_COLUMN + composer.cursorColumn));
664
744
  output.write("\u001b[?25h");
665
745
  }
666
746
  }
667
- function createConsoleScreenRenderer(_modelMode, _approvalMode, _runtimeSession) {
668
- return null;
747
+ export function createConsoleScreenRenderer(modelMode, approvalMode, runtimeSession, options) {
748
+ const canRender = (options?.ansiEnabled ?? supportsAnsi())
749
+ && (options?.inputIsTTY ?? Boolean(input.isTTY))
750
+ && (options?.outputIsTTY ?? Boolean(output.isTTY))
751
+ && (options?.hasRawMode ?? typeof input.setRawMode === "function");
752
+ if (!canRender) {
753
+ return null;
754
+ }
755
+ const renderer = new ConsoleScreenRenderer(modelMode, approvalMode, () => buildConsoleHeaderLines(runtimeSession));
756
+ if (options?.autoActivate ?? false) {
757
+ renderer.activate();
758
+ }
759
+ return renderer;
669
760
  }
670
761
  export function resolveCliModelMode(value) {
671
762
  const normalized = normalizeText(value).toLowerCase();
@@ -1364,6 +1455,183 @@ export function tryConsumeControlSequence(buffer) {
1364
1455
  }
1365
1456
  return null;
1366
1457
  }
1458
+ class ConsoleInputController {
1459
+ rl;
1460
+ ui;
1461
+ options;
1462
+ active = false;
1463
+ value = "";
1464
+ controlBuffer = "";
1465
+ selectedSuggestionIndex = 0;
1466
+ queuedValues = [];
1467
+ pendingResolvers = [];
1468
+ terminalError = null;
1469
+ constructor(rl, ui, options) {
1470
+ this.rl = rl;
1471
+ this.ui = ui;
1472
+ this.options = options;
1473
+ }
1474
+ activate() {
1475
+ if (this.active || typeof input.setRawMode !== "function" || !input.isTTY) {
1476
+ return;
1477
+ }
1478
+ this.active = true;
1479
+ this.rl.pause();
1480
+ input.setRawMode(true);
1481
+ input.resume();
1482
+ input.on("data", this.onData);
1483
+ this.render();
1484
+ }
1485
+ dispose() {
1486
+ if (!this.active) {
1487
+ return;
1488
+ }
1489
+ input.removeListener("data", this.onData);
1490
+ input.setRawMode(false);
1491
+ input.pause();
1492
+ this.rl.resume();
1493
+ this.active = false;
1494
+ this.ui.resetComposer();
1495
+ }
1496
+ nextValue() {
1497
+ if (this.queuedValues.length > 0) {
1498
+ return Promise.resolve(this.queuedValues.shift() || "");
1499
+ }
1500
+ if (this.terminalError) {
1501
+ return Promise.reject(this.terminalError);
1502
+ }
1503
+ return new Promise((resolve, reject) => {
1504
+ this.pendingResolvers.push({ resolve, reject });
1505
+ });
1506
+ }
1507
+ getVisibleSuggestions() {
1508
+ const suggestions = resolveConsoleSlashSuggestions(this.value).slice(0, 6);
1509
+ if (this.selectedSuggestionIndex >= suggestions.length) {
1510
+ this.selectedSuggestionIndex = Math.max(0, suggestions.length - 1);
1511
+ }
1512
+ return suggestions;
1513
+ }
1514
+ render() {
1515
+ this.ui.setComposerState(this.value, this.getVisibleSuggestions(), this.selectedSuggestionIndex);
1516
+ }
1517
+ enqueueValue(value) {
1518
+ const next = this.pendingResolvers.shift();
1519
+ if (next) {
1520
+ next.resolve(value);
1521
+ return;
1522
+ }
1523
+ this.queuedValues.push(value);
1524
+ }
1525
+ rejectPending(error) {
1526
+ while (this.pendingResolvers.length > 0) {
1527
+ const next = this.pendingResolvers.shift();
1528
+ next?.reject(error);
1529
+ }
1530
+ }
1531
+ closeWithError(error) {
1532
+ this.terminalError = error;
1533
+ this.rejectPending(error);
1534
+ this.dispose();
1535
+ }
1536
+ applySelectedSuggestion() {
1537
+ const suggestions = this.getVisibleSuggestions();
1538
+ const selected = suggestions[this.selectedSuggestionIndex];
1539
+ if (!selected) {
1540
+ return false;
1541
+ }
1542
+ if (normalizeText(this.value) === selected.insertText) {
1543
+ return false;
1544
+ }
1545
+ this.value = selected.insertText;
1546
+ this.selectedSuggestionIndex = 0;
1547
+ this.render();
1548
+ return true;
1549
+ }
1550
+ consumeControlBuffer() {
1551
+ while (this.controlBuffer.length > 0) {
1552
+ const parsed = tryConsumeControlSequence(this.controlBuffer);
1553
+ if (!parsed) {
1554
+ this.controlBuffer = "";
1555
+ return;
1556
+ }
1557
+ if (parsed.action === "incomplete") {
1558
+ return;
1559
+ }
1560
+ this.controlBuffer = this.controlBuffer.slice(parsed.consumed);
1561
+ if (parsed.action === "ignore") {
1562
+ continue;
1563
+ }
1564
+ if (parsed.action === "cycle_approval") {
1565
+ void Promise.resolve(this.options?.onCycleApprovalMode?.()).finally(() => {
1566
+ this.render();
1567
+ });
1568
+ continue;
1569
+ }
1570
+ if (parsed.action === "move_up") {
1571
+ const suggestions = this.getVisibleSuggestions();
1572
+ if (suggestions.length > 0) {
1573
+ this.selectedSuggestionIndex = this.selectedSuggestionIndex > 0
1574
+ ? this.selectedSuggestionIndex - 1
1575
+ : suggestions.length - 1;
1576
+ this.render();
1577
+ }
1578
+ continue;
1579
+ }
1580
+ if (parsed.action === "move_down") {
1581
+ const suggestions = this.getVisibleSuggestions();
1582
+ if (suggestions.length > 0) {
1583
+ this.selectedSuggestionIndex = this.selectedSuggestionIndex < suggestions.length - 1
1584
+ ? this.selectedSuggestionIndex + 1
1585
+ : 0;
1586
+ this.render();
1587
+ }
1588
+ continue;
1589
+ }
1590
+ if (parsed.action === "newline") {
1591
+ this.value += "\n";
1592
+ this.selectedSuggestionIndex = 0;
1593
+ this.render();
1594
+ }
1595
+ }
1596
+ }
1597
+ onData = (chunk) => {
1598
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
1599
+ for (const char of Array.from(text)) {
1600
+ if (this.controlBuffer || char === "\u001b") {
1601
+ this.controlBuffer += char;
1602
+ this.consumeControlBuffer();
1603
+ continue;
1604
+ }
1605
+ if (char === "\u0003") {
1606
+ this.closeWithError(createCliExitError());
1607
+ return;
1608
+ }
1609
+ if (char === "\r" || char === "\n") {
1610
+ if (this.applySelectedSuggestion()) {
1611
+ continue;
1612
+ }
1613
+ const submittedValue = normalizeText(this.value);
1614
+ this.value = "";
1615
+ this.selectedSuggestionIndex = 0;
1616
+ this.render();
1617
+ this.enqueueValue(submittedValue);
1618
+ continue;
1619
+ }
1620
+ if (char === "\u007f" || char === "\b") {
1621
+ this.value = this.value.slice(0, -1);
1622
+ this.selectedSuggestionIndex = 0;
1623
+ this.render();
1624
+ continue;
1625
+ }
1626
+ if (char === "\t") {
1627
+ this.applySelectedSuggestion();
1628
+ continue;
1629
+ }
1630
+ this.value += char;
1631
+ this.render();
1632
+ }
1633
+ };
1634
+ }
1367
1635
  async function askConsoleInput(rl, options) {
1368
1636
  if (!supportsAnsi() || typeof input.setRawMode !== "function" || !input.isTTY) {
1369
1637
  return normalizeText(await question(rl, "> "));
@@ -1401,27 +1669,11 @@ async function askConsoleInput(rl, options) {
1401
1669
  input.setRawMode(false);
1402
1670
  input.pause();
1403
1671
  rl.resume();
1404
- ui?.setDraftValue("");
1405
- };
1406
- const renderSuggestionLines = (width) => {
1407
- const suggestions = getVisibleSuggestions();
1408
- if (!suggestions.length) {
1409
- return [];
1410
- }
1411
- const suggestionWidth = Math.max(24, width - 4);
1412
- return suggestions.map((item, index) => {
1413
- const selected = index === selectedSuggestionIndex;
1414
- const prefix = selected
1415
- ? style("›", `${ANSI.bold}${ANSI.brandBlue}`, enabled)
1416
- : style(" ", ANSI.slate, enabled);
1417
- const command = style(item.command, selected ? `${ANSI.bold}${ANSI.white}` : ANSI.brandBlue, enabled);
1418
- const description = style(truncate(item.description, Math.max(12, suggestionWidth - item.command.length - 4)), selected ? ANSI.white : ANSI.slate, enabled);
1419
- return `${prefix} ${command} ${description}`;
1420
- });
1672
+ ui?.resetComposer();
1421
1673
  };
1422
1674
  const renderPromptBlock = () => {
1423
1675
  if (ui) {
1424
- ui.setDraftValue(value);
1676
+ ui.setComposerState(value, getVisibleSuggestions(), selectedSuggestionIndex);
1425
1677
  return;
1426
1678
  }
1427
1679
  const width = getPromptWidth();
@@ -1429,7 +1681,7 @@ async function askConsoleInput(rl, options) {
1429
1681
  const modelMode = getModelMode();
1430
1682
  const approvalMode = getApprovalMode();
1431
1683
  const usageTokens = Math.min(estimateConsoleContextTokens(getConversationMessages(), value), getCliModelContextWindowTokens(modelMode));
1432
- const suggestionLines = renderSuggestionLines(width);
1684
+ const suggestionLines = buildConsoleSlashSuggestionLines(getVisibleSuggestions(), selectedSuggestionIndex, width, enabled);
1433
1685
  const blockLines = [
1434
1686
  style("─".repeat(width), ANSI.brandBlue, enabled),
1435
1687
  ...composer.renderedLines,
@@ -1530,7 +1782,9 @@ async function askConsoleInput(rl, options) {
1530
1782
  continue;
1531
1783
  }
1532
1784
  cleanup();
1533
- output.write("\n");
1785
+ if (!ui) {
1786
+ output.write("\n");
1787
+ }
1534
1788
  resolve(normalizeText(value));
1535
1789
  return;
1536
1790
  }
@@ -1548,7 +1802,9 @@ async function askConsoleInput(rl, options) {
1548
1802
  renderPromptBlock();
1549
1803
  }
1550
1804
  };
1551
- output.on("resize", renderPromptBlock);
1805
+ if (!ui) {
1806
+ output.on("resize", renderPromptBlock);
1807
+ }
1552
1808
  renderPromptBlock();
1553
1809
  input.on("data", onData);
1554
1810
  });
@@ -1853,10 +2109,20 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1853
2109
  let activeModel = "fast";
1854
2110
  let sessionId = randomUUID();
1855
2111
  const conversation = [];
1856
- const ui = createConsoleScreenRenderer(activeModel, config.approvalMode, runtimeSession);
2112
+ const ui = createConsoleScreenRenderer(activeModel, config.approvalMode, runtimeSession, {
2113
+ autoActivate: false,
2114
+ });
1857
2115
  if (!ui) {
1858
2116
  printConsoleScreen(runtimeSession);
1859
2117
  }
2118
+ else if (options?.reuseHubHeader) {
2119
+ output.write(`${style(`Comandos: ${CONSOLE_COMMAND_HINT}`, ANSI.slateItalic, supportsAnsi())}\n\n`);
2120
+ ui.activate();
2121
+ }
2122
+ else {
2123
+ printConsoleScreen(runtimeSession);
2124
+ ui.activate();
2125
+ }
1860
2126
  const renderConversationState = () => {
1861
2127
  ui?.setConversationMessages(conversation);
1862
2128
  ui?.setModelMode(activeModel);
@@ -1927,7 +2193,16 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1927
2193
  const cycleApprovalMode = () => {
1928
2194
  void setApprovalMode(getNextApprovalMode(config.approvalMode));
1929
2195
  };
2196
+ const inputController = ui
2197
+ ? new ConsoleInputController(rl, ui, {
2198
+ onCycleApprovalMode: cycleApprovalMode,
2199
+ })
2200
+ : null;
2201
+ inputController?.activate();
1930
2202
  const readConsoleInput = async () => {
2203
+ if (inputController) {
2204
+ return await inputController.nextValue();
2205
+ }
1931
2206
  return await askConsoleInput(rl, {
1932
2207
  ui,
1933
2208
  onCycleApprovalMode: cycleApprovalMode,
@@ -2287,6 +2562,7 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
2287
2562
  }
2288
2563
  streamedAssistant += contentChunk;
2289
2564
  });
2565
+ ui?.commitLiveEntries();
2290
2566
  if (assistantPrefixPrinted && !ui) {
2291
2567
  output.write("\n");
2292
2568
  }
@@ -2331,6 +2607,7 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
2331
2607
  }
2332
2608
  }
2333
2609
  finally {
2610
+ inputController?.dispose();
2334
2611
  ui?.dispose();
2335
2612
  }
2336
2613
  }
@@ -2375,6 +2652,14 @@ function renderHomeOptionLine(label, selected) {
2375
2652
  }
2376
2653
  return `${style("▸", ANSI.brandBlue, supportsAnsi())} ${style(label, `${ANSI.bold}${ANSI.white}`, supportsAnsi())}`;
2377
2654
  }
2655
+ function clearHomeMenuBlock(optionCount) {
2656
+ if (!supportsAnsi() || !output.isTTY) {
2657
+ return;
2658
+ }
2659
+ cursorTo(output, 0);
2660
+ moveCursor(output, 0, -(optionCount + 4));
2661
+ clearScreenDown(output);
2662
+ }
2378
2663
  async function pickHomeChoice(rl, paired, renderBaseScreen) {
2379
2664
  const options = paired
2380
2665
  ? [
@@ -2461,9 +2746,15 @@ async function pickHomeChoice(rl, paired, renderBaseScreen) {
2461
2746
  return;
2462
2747
  }
2463
2748
  if (char === "\r" || char === "\n") {
2749
+ const selectedOption = options[selectedIndex];
2464
2750
  cleanup();
2465
- output.write("\n");
2466
- resolve(options[selectedIndex].value);
2751
+ if (selectedOption?.value === "console") {
2752
+ clearHomeMenuBlock(options.length);
2753
+ }
2754
+ else {
2755
+ output.write("\n");
2756
+ }
2757
+ resolve(selectedOption?.value || "exit");
2467
2758
  return;
2468
2759
  }
2469
2760
  }
@@ -2566,7 +2857,7 @@ export async function launchInteractiveCli(options) {
2566
2857
  continue;
2567
2858
  }
2568
2859
  if (choice === "console" && runtimeSession) {
2569
- await runOttoConsole(rl, config, runtimeSession);
2860
+ await runOttoConsole(rl, config, runtimeSession, { reuseHubHeader: true });
2570
2861
  continue;
2571
2862
  }
2572
2863
  if (choice === "status" && runtimeSession) {
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "1.1.3";
2
+ export const BRIDGE_VERSION = "1.1.5";
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.3",
3
+ "version": "1.1.5",
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.3");
27
+ console.log("\n[otto-bridge] Welcome to OTTOAI 1.1.5");
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"], {