@leg3ndy/otto-bridge 1.1.2 → 1.1.3

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.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).
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.3.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.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.
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.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`.
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.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 `/`.
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.3`:
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.3`:
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.3
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;
@@ -591,6 +664,9 @@ class ConsoleScreenRenderer {
591
664
  output.write("\u001b[?25h");
592
665
  }
593
666
  }
667
+ function createConsoleScreenRenderer(_modelMode, _approvalMode, _runtimeSession) {
668
+ return null;
669
+ }
594
670
  export function resolveCliModelMode(value) {
595
671
  const normalized = normalizeText(value).toLowerCase();
596
672
  if (!normalized) {
@@ -1236,6 +1312,30 @@ export function tryConsumeControlSequence(buffer) {
1236
1312
  };
1237
1313
  }
1238
1314
  }
1315
+ const moveUpSequences = [
1316
+ "\u001b[A",
1317
+ "\u001bOA",
1318
+ ];
1319
+ for (const sequence of moveUpSequences) {
1320
+ if (buffer.startsWith(sequence)) {
1321
+ return {
1322
+ consumed: sequence.length,
1323
+ action: "move_up",
1324
+ };
1325
+ }
1326
+ }
1327
+ const moveDownSequences = [
1328
+ "\u001b[B",
1329
+ "\u001bOB",
1330
+ ];
1331
+ for (const sequence of moveDownSequences) {
1332
+ if (buffer.startsWith(sequence)) {
1333
+ return {
1334
+ consumed: sequence.length,
1335
+ action: "move_down",
1336
+ };
1337
+ }
1338
+ }
1239
1339
  if (buffer === "\u001b" || /^\u001b\[[0-9;?]*$/.test(buffer)) {
1240
1340
  return {
1241
1341
  consumed: 0,
@@ -1274,55 +1374,100 @@ async function askConsoleInput(rl, options) {
1274
1374
  return await new Promise((resolve, reject) => {
1275
1375
  const enabled = supportsAnsi();
1276
1376
  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
1377
  let value = "";
1281
1378
  let renderedOnce = false;
1379
+ let renderedCursorLineIndex = 0;
1282
1380
  let controlBuffer = "";
1381
+ let selectedSuggestionIndex = 0;
1382
+ const getPromptWidth = () => Math.max(48, Number(output.columns || 96));
1383
+ const getModelMode = () => options?.getModelMode?.() || "fast";
1384
+ const getApprovalMode = () => options?.getApprovalMode?.() || "preview";
1385
+ const getConversationMessages = () => options?.getConversationMessages?.() || [];
1386
+ const getVisibleSuggestions = () => {
1387
+ const suggestions = resolveConsoleSlashSuggestions(value).slice(0, 6);
1388
+ if (selectedSuggestionIndex >= suggestions.length) {
1389
+ selectedSuggestionIndex = Math.max(0, suggestions.length - 1);
1390
+ }
1391
+ return suggestions;
1392
+ };
1283
1393
  const cleanup = () => {
1284
1394
  input.removeListener("data", onData);
1395
+ output.off("resize", renderPromptBlock);
1396
+ if (!ui && renderedOnce) {
1397
+ cursorTo(output, 0);
1398
+ moveCursor(output, 0, -renderedCursorLineIndex);
1399
+ clearScreenDown(output);
1400
+ }
1285
1401
  input.setRawMode(false);
1286
1402
  input.pause();
1287
1403
  rl.resume();
1288
1404
  ui?.setDraftValue("");
1289
1405
  };
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}`;
1406
+ const renderSuggestionLines = (width) => {
1407
+ const suggestions = getVisibleSuggestions();
1408
+ if (!suggestions.length) {
1409
+ return [];
1298
1410
  }
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))}`;
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
+ });
1303
1421
  };
1304
- const renderLegacyInput = () => {
1422
+ const renderPromptBlock = () => {
1305
1423
  if (ui) {
1306
1424
  ui.setDraftValue(value);
1307
1425
  return;
1308
1426
  }
1427
+ const width = getPromptWidth();
1428
+ const composer = renderConsoleComposerLines(value, width, enabled);
1429
+ const modelMode = getModelMode();
1430
+ const approvalMode = getApprovalMode();
1431
+ const usageTokens = Math.min(estimateConsoleContextTokens(getConversationMessages(), value), getCliModelContextWindowTokens(modelMode));
1432
+ const suggestionLines = renderSuggestionLines(width);
1433
+ const blockLines = [
1434
+ style("─".repeat(width), ANSI.brandBlue, enabled),
1435
+ ...composer.renderedLines,
1436
+ ...suggestionLines,
1437
+ style("─".repeat(width), ANSI.brandBlue, enabled),
1438
+ buildConsoleFooterStatusLine(width, modelMode, usageTokens, enabled),
1439
+ buildConsoleFooterApprovalLine(approvalMode, enabled),
1440
+ ];
1309
1441
  if (renderedOnce) {
1310
1442
  cursorTo(output, 0);
1311
- moveCursor(output, 0, -sectionTopOffsetFromInputLine);
1443
+ moveCursor(output, 0, -renderedCursorLineIndex);
1444
+ clearScreenDown(output);
1312
1445
  }
1313
1446
  else {
1314
1447
  renderedOnce = true;
1315
1448
  }
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`);
1449
+ output.write(blockLines.join("\n"));
1450
+ renderedCursorLineIndex = 1 + composer.cursorLineIndex;
1451
+ const linesBelowCursor = blockLines.length - 1 - renderedCursorLineIndex;
1322
1452
  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);
1453
+ if (linesBelowCursor > 0) {
1454
+ moveCursor(output, 0, -linesBelowCursor);
1455
+ }
1456
+ cursorTo(output, Math.min(width - 1, CONSOLE_COMPOSER_CURSOR_COLUMN + composer.cursorColumn));
1457
+ };
1458
+ const applySelectedSuggestion = () => {
1459
+ const suggestions = getVisibleSuggestions();
1460
+ const selected = suggestions[selectedSuggestionIndex];
1461
+ if (!selected) {
1462
+ return false;
1463
+ }
1464
+ if (normalizeText(value) === selected.insertText) {
1465
+ return false;
1466
+ }
1467
+ value = selected.insertText;
1468
+ selectedSuggestionIndex = 0;
1469
+ renderPromptBlock();
1470
+ return true;
1326
1471
  };
1327
1472
  const consumeControlBuffer = () => {
1328
1473
  while (controlBuffer.length > 0) {
@@ -1339,12 +1484,31 @@ async function askConsoleInput(rl, options) {
1339
1484
  continue;
1340
1485
  }
1341
1486
  if (parsed.action === "cycle_approval") {
1342
- void options?.onCycleApprovalMode?.();
1487
+ void Promise.resolve(options?.onCycleApprovalMode?.()).finally(() => {
1488
+ renderPromptBlock();
1489
+ });
1490
+ continue;
1491
+ }
1492
+ if (parsed.action === "move_up") {
1493
+ const suggestions = getVisibleSuggestions();
1494
+ if (suggestions.length > 0) {
1495
+ selectedSuggestionIndex = selectedSuggestionIndex > 0 ? selectedSuggestionIndex - 1 : suggestions.length - 1;
1496
+ renderPromptBlock();
1497
+ }
1498
+ continue;
1499
+ }
1500
+ if (parsed.action === "move_down") {
1501
+ const suggestions = getVisibleSuggestions();
1502
+ if (suggestions.length > 0) {
1503
+ selectedSuggestionIndex = selectedSuggestionIndex < suggestions.length - 1 ? selectedSuggestionIndex + 1 : 0;
1504
+ renderPromptBlock();
1505
+ }
1343
1506
  continue;
1344
1507
  }
1345
1508
  if (parsed.action === "newline") {
1346
1509
  value += "\n";
1347
- renderLegacyInput();
1510
+ selectedSuggestionIndex = 0;
1511
+ renderPromptBlock();
1348
1512
  }
1349
1513
  }
1350
1514
  };
@@ -1362,26 +1526,30 @@ async function askConsoleInput(rl, options) {
1362
1526
  return;
1363
1527
  }
1364
1528
  if (char === "\r" || char === "\n") {
1365
- cleanup();
1366
- if (!ui) {
1367
- output.write("\n");
1529
+ if (applySelectedSuggestion()) {
1530
+ continue;
1368
1531
  }
1532
+ cleanup();
1533
+ output.write("\n");
1369
1534
  resolve(normalizeText(value));
1370
1535
  return;
1371
1536
  }
1372
1537
  if (char === "\u007f" || char === "\b") {
1373
1538
  value = value.slice(0, -1);
1374
- renderLegacyInput();
1539
+ selectedSuggestionIndex = 0;
1540
+ renderPromptBlock();
1375
1541
  continue;
1376
1542
  }
1377
1543
  if (char === "\t") {
1544
+ applySelectedSuggestion();
1378
1545
  continue;
1379
1546
  }
1380
1547
  value += char;
1381
- renderLegacyInput();
1548
+ renderPromptBlock();
1382
1549
  }
1383
1550
  };
1384
- renderLegacyInput();
1551
+ output.on("resize", renderPromptBlock);
1552
+ renderPromptBlock();
1385
1553
  input.on("data", onData);
1386
1554
  });
1387
1555
  }
@@ -1683,12 +1851,9 @@ async function followConsoleJob(rl, config, jobId) {
1683
1851
  }
1684
1852
  async function runOttoConsole(rl, config, runtimeSession, options) {
1685
1853
  let activeModel = "fast";
1686
- const sessionId = randomUUID();
1854
+ let sessionId = randomUUID();
1687
1855
  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();
1856
+ const ui = createConsoleScreenRenderer(activeModel, config.approvalMode, runtimeSession);
1692
1857
  if (!ui) {
1693
1858
  printConsoleScreen(runtimeSession);
1694
1859
  }
@@ -1766,6 +1931,9 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1766
1931
  return await askConsoleInput(rl, {
1767
1932
  ui,
1768
1933
  onCycleApprovalMode: cycleApprovalMode,
1934
+ getConversationMessages: () => conversation,
1935
+ getModelMode: () => activeModel,
1936
+ getApprovalMode: () => config.approvalMode,
1769
1937
  });
1770
1938
  };
1771
1939
  const askConsoleDecision = async (promptText, defaultValue = true) => {
@@ -1779,7 +1947,7 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1779
1947
  };
1780
1948
  const printConsoleHelp = () => {
1781
1949
  emitConsoleEntry("Console", "headline");
1782
- emitConsoleEntry("Comandos: /help, /model [fast|thinking], /approval [preview|confirm|trusted], /workspace, /status, /clear, /exit", "muted");
1950
+ emitConsoleEntry("Comandos: /help, /model [fast|thinking], /approval [preview|confirm|trusted], /workspace, /status, /clear, /new, /exit", "muted");
1783
1951
  emitConsoleEntry("Composer: Enter envia, Shift+Enter adiciona linha e Shift+Tab alterna o modo de aprovação.", "muted");
1784
1952
  emitConsoleEntry("Workspace: /workspace list, /workspace attach <path>, /workspace use <id|n>, /workspace clear", "muted");
1785
1953
  };
@@ -1963,6 +2131,17 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1963
2131
  emitConsoleEntry("Contexto local do console limpo.", "muted");
1964
2132
  return;
1965
2133
  }
2134
+ if (normalizedPrompt === "/new") {
2135
+ sessionId = randomUUID();
2136
+ conversation.splice(0, conversation.length);
2137
+ ui?.clearTranscript();
2138
+ renderConversationState();
2139
+ if (!ui) {
2140
+ printConsoleScreen(runtimeSession);
2141
+ }
2142
+ emitConsoleEntry("Nova sessão iniciada. Contexto local reiniciado.", "muted");
2143
+ return;
2144
+ }
1966
2145
  if (normalizedPrompt === "/model") {
1967
2146
  emitConsoleEntry(`Modelo ativo: ${getCliModelLabel(activeModel)}.`, "muted");
1968
2147
  emitConsoleEntry("Use /model fast ou /model thinking para trocar.", "muted");
@@ -2028,9 +2207,6 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
2028
2207
  emitUserPrompt(normalizedPrompt);
2029
2208
  emitConsoleEntry("", "muted");
2030
2209
  conversation.push({ role: "user", content: normalizedPrompt });
2031
- while (conversation.length > 18) {
2032
- conversation.shift();
2033
- }
2034
2210
  renderConversationState();
2035
2211
  let streamedAssistant = "";
2036
2212
  let assistantEntryId = null;
@@ -2134,9 +2310,6 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
2134
2310
  }
2135
2311
  if (finalAssistantSummary) {
2136
2312
  conversation.push({ role: "assistant", content: finalAssistantSummary });
2137
- while (conversation.length > 18) {
2138
- conversation.shift();
2139
- }
2140
2313
  renderConversationState();
2141
2314
  }
2142
2315
  };
@@ -2192,52 +2365,112 @@ async function printHelpView(rl) {
2192
2365
  { text: "otto-bridge version | otto-bridge unpair", tone: "primary" },
2193
2366
  { text: "Mostra a versao instalada ou remove o pairing local.", tone: "muted" },
2194
2367
  { text: "", tone: "muted" },
2195
- { text: "Dentro do console: /help, /model fast|thinking, /approval preview|confirm|trusted, /status, /clear, /exit", tone: "muted" },
2368
+ { text: "Dentro do console: /help, /model fast|thinking, /approval preview|confirm|trusted, /status, /clear, /new, /exit", tone: "muted" },
2196
2369
  ]));
2197
2370
  await pauseForEnter(rl);
2198
2371
  }
2199
- async function pickHomeChoice(rl, paired) {
2200
- printSection("Home");
2372
+ function renderHomeOptionLine(label, selected) {
2373
+ if (!selected) {
2374
+ return ` ${label}`;
2375
+ }
2376
+ return `${style("▸", ANSI.brandBlue, supportsAnsi())} ${style(label, `${ANSI.bold}${ANSI.white}`, supportsAnsi())}`;
2377
+ }
2378
+ async function pickHomeChoice(rl, paired, renderBaseScreen) {
2201
2379
  const options = paired
2202
2380
  ? [
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`,
2381
+ { value: "console", label: "Otto Console" },
2382
+ { value: "terminal", label: "Terminal" },
2383
+ { value: "setup", label: "Setup / parear novamente" },
2384
+ { value: "status", label: "Status detalhado" },
2385
+ { value: "extensions", label: "Extensões" },
2386
+ { value: "help", label: "Ajuda" },
2387
+ { value: "exit", label: "Sair" },
2210
2388
  ]
2211
2389
  : [
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`,
2390
+ { value: "setup", label: "Pairing setup" },
2391
+ { value: "terminal", label: "Terminal" },
2392
+ { value: "help", label: "Ajuda" },
2393
+ { value: "exit", label: "Sair" },
2216
2394
  ];
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";
2395
+ if (!supportsAnsi() || typeof input.setRawMode !== "function" || !input.isTTY) {
2396
+ printSection("Home");
2397
+ console.log(options.map((option, index) => `${style(`${index + 1}.`, ANSI.brandBlue, supportsAnsi())} ${option.label}`).join("\n"));
2398
+ const answer = await ask(rl, "Escolha");
2399
+ const numericIndex = Number(answer);
2400
+ if (Number.isInteger(numericIndex) && numericIndex >= 1 && numericIndex <= options.length) {
2401
+ return options[numericIndex - 1].value;
2402
+ }
2226
2403
  return "exit";
2227
2404
  }
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";
2405
+ rl.pause();
2406
+ input.setRawMode(true);
2407
+ input.resume();
2408
+ return await new Promise((resolve, reject) => {
2409
+ let selectedIndex = 0;
2410
+ let controlBuffer = "";
2411
+ const cleanup = () => {
2412
+ input.removeListener("data", onData);
2413
+ input.setRawMode(false);
2414
+ input.pause();
2415
+ rl.resume();
2416
+ };
2417
+ const render = () => {
2418
+ renderBaseScreen();
2419
+ printSection("Home");
2420
+ console.log(options.map((option, index) => renderHomeOptionLine(option.label, index === selectedIndex)).join("\n"));
2421
+ console.log("");
2422
+ printSoft(CONSOLE_MENU_HINT);
2423
+ };
2424
+ const consumeControlBuffer = () => {
2425
+ while (controlBuffer.length > 0) {
2426
+ const parsed = tryConsumeControlSequence(controlBuffer);
2427
+ if (!parsed) {
2428
+ controlBuffer = "";
2429
+ return;
2430
+ }
2431
+ if (parsed.action === "incomplete") {
2432
+ return;
2433
+ }
2434
+ controlBuffer = controlBuffer.slice(parsed.consumed);
2435
+ if (parsed.action === "move_up") {
2436
+ selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1;
2437
+ render();
2438
+ continue;
2439
+ }
2440
+ if (parsed.action === "move_down") {
2441
+ selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0;
2442
+ render();
2443
+ continue;
2444
+ }
2445
+ if (parsed.action === "ignore" || parsed.action === "cycle_approval" || parsed.action === "newline") {
2446
+ continue;
2447
+ }
2448
+ }
2449
+ };
2450
+ const onData = (chunk) => {
2451
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
2452
+ for (const char of Array.from(text)) {
2453
+ if (controlBuffer || char === "\u001b") {
2454
+ controlBuffer += char;
2455
+ consumeControlBuffer();
2456
+ continue;
2457
+ }
2458
+ if (char === "\u0003") {
2459
+ cleanup();
2460
+ reject(createCliExitError());
2461
+ return;
2462
+ }
2463
+ if (char === "\r" || char === "\n") {
2464
+ cleanup();
2465
+ output.write("\n");
2466
+ resolve(options[selectedIndex].value);
2467
+ return;
2468
+ }
2469
+ }
2470
+ };
2471
+ render();
2472
+ input.on("data", onData);
2473
+ });
2241
2474
  }
2242
2475
  export async function launchInteractiveCli(options) {
2243
2476
  let rl = await createPromptInterface();
@@ -2264,7 +2497,7 @@ export async function launchInteractiveCli(options) {
2264
2497
  await runtimeSession.waitForReady();
2265
2498
  for (;;) {
2266
2499
  printHubScreen(runtimeSession);
2267
- const choice = await pickHomeChoice(rl, true);
2500
+ const choice = await pickHomeChoice(rl, true, () => printHubScreen(runtimeSession));
2268
2501
  if (choice === "exit") {
2269
2502
  break;
2270
2503
  }
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.3";
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.3",
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.3");
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"], {