@leg3ndy/otto-bridge 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 a release atual `1.1.0`, que consolida o baseline TTY single-line e inaugura a rail de coding com `Commit`, `Workspace` e `Snapshots`, 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.1`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_1_PATCH.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_1_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.0.tgz
41
+ npm install -g ./leg3ndy-otto-bridge-1.1.1.tgz
42
42
  ```
43
43
 
44
- Na linha `1.1.0`, `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.1`, `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.0` 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.1` 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.0` preserva o prompt TTY single-line como baseline estavel e adiciona 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.
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.1` adiciona 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.
49
49
 
50
50
  ## Publicacao
51
51
 
@@ -145,6 +145,7 @@ Dentro do console, use:
145
145
 
146
146
  - `/model fast` para `OttoAI Fast`
147
147
  - `/model thinking` para `OttoAI Thinking`
148
+ - `/approval preview`, `/approval confirm` ou `/approval trusted` para trocar o modo de aprovação do device
148
149
  - `/status` para ver detalhes técnicos do bridge e do runtime
149
150
  - `/workspace` ou `/workspace status` para ver o workspace ativo desta sessão
150
151
  - `/workspace list` para listar workspaces anexados ao device
@@ -152,6 +153,8 @@ Dentro do console, use:
152
153
  - `/workspace use <id|n>` para fixar um workspace anexado na sessão atual
153
154
  - `/workspace clear` para limpar o binding atual do chat/sessão
154
155
 
156
+ No TTY, o composer agora fica ancorado no rodapé com placeholder `Peça algo ao Otto`, quebra por largura real do terminal e deixa o transcript do Otto sempre visível acima. Logo abaixo do input, o console mostra o modelo ativo, a barra de uso de contexto/tokens e o modo de aprovação atual; `Shift+Tab` alterna esse modo sem sair do console.
157
+
155
158
  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.
156
159
 
157
160
  Quando o handoff local devolver resultado estruturado, o CLI agora mostra inline a listagem de arquivos e o conteúdo de `read_file`, em vez de só resumir que executou a tarefa.
@@ -166,7 +169,7 @@ Esse comando abre um shell local interativo para instalar extensoes, rodar coman
166
169
 
167
170
  ### WhatsApp Web em background
168
171
 
169
- Fluxo recomendado na linha `1.1.0`:
172
+ Fluxo recomendado na linha `1.1.1`:
170
173
 
171
174
  ```bash
172
175
  otto-bridge extensions --install whatsappweb
@@ -176,13 +179,13 @@ otto-bridge extensions --status whatsappweb
176
179
 
177
180
  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.
178
181
 
179
- Contrato da linha `1.1.0`:
182
+ Contrato da linha `1.1.1`:
180
183
 
181
184
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
182
185
  - `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
183
186
  - ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
184
187
 
185
- ## Handoff rapido da linha 1.1.0
188
+ ## Handoff rapido da linha 1.1.1
186
189
 
187
190
  Ja fechado no codigo:
188
191
 
@@ -4,9 +4,10 @@ import { readFile } from "node:fs/promises";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { clearScreenDown, cursorTo, moveCursor, } from "node:readline";
6
6
  import process, { stdin as input, stdout as output } from "node:process";
7
- import { defaultDeviceName, getBridgeConfigPath, loadBridgeConfig, normalizeInstalledExtensions, resolveApiBaseUrl, resolveExecutorConfig, } from "./config.js";
7
+ import { defaultDeviceName, getBridgeConfigPath, loadBridgeConfig, normalizeInstalledExtensions, resolveApiBaseUrl, resolveExecutorConfig, saveBridgeConfig, } from "./config.js";
8
8
  import { activateWorkspaceForRuntime, attachWorkspaceForRuntime, clearRuntimeWorkspaceForSession, getRuntimeSessionWorkspace, listAttachedWorkspacesForRuntime, } from "./attached_workspaces.js";
9
9
  import { streamDeviceCliChat, } from "./chat_cli_client.js";
10
+ import { patchDeviceJson } from "./http.js";
10
11
  import { formatManagedBridgeExtensionStatus, isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "./extensions.js";
11
12
  import { pairDevice } from "./pairing.js";
12
13
  import { BridgeRuntime, } from "./runtime.js";
@@ -41,11 +42,13 @@ const CLI_MODEL_REGISTRY = {
41
42
  label: "OttoAI Fast",
42
43
  requestModel: "deepseek-chat",
43
44
  aliases: ["fast", "chat", "default", "ottoai fast", "otto fast"],
45
+ contextWindowTokens: 128_000,
44
46
  },
45
47
  thinking: {
46
48
  label: "OttoAI Thinking",
47
49
  requestModel: "deepseek-reasoner",
48
50
  aliases: ["thinking", "reasoner", "think", "ottoai thinking", "otto thinking"],
51
+ contextWindowTokens: 128_000,
49
52
  },
50
53
  };
51
54
  const MAX_RENDERED_LIST_ENTRIES = 28;
@@ -53,9 +56,11 @@ const MAX_RENDERED_LIST_ENTRIES_COMPACT = 10;
53
56
  const MAX_RENDERED_FILE_CHARS = 6_000;
54
57
  const MAX_RENDERED_FILE_CHARS_COMPACT = 1_400;
55
58
  const CONSOLE_PLACEHOLDER = "Peça algo ao Otto";
56
- const CONSOLE_COMMAND_HINT = "/help, /model [fast|thinking], /workspace, /status, /clear, /exit";
59
+ const CONSOLE_COMMAND_HINT = "/help, /model [fast|thinking], /approval [preview|confirm|trusted], /workspace, /status, /clear, /exit";
57
60
  const CONSOLE_COMPOSER_PROMPT_WIDTH = 2;
58
61
  const CONSOLE_COMPOSER_CURSOR_COLUMN = 2;
62
+ const CONSOLE_EST_CHARS_PER_TOKEN = 4;
63
+ const CONSOLE_EST_MESSAGE_OVERHEAD_TOKENS = 8;
59
64
  class CliRuntimeSession {
60
65
  config;
61
66
  runtime = null;
@@ -297,6 +302,267 @@ function getCliModelLabel(mode) {
297
302
  function getCliModelRequestModel(mode) {
298
303
  return CLI_MODEL_REGISTRY[mode].requestModel;
299
304
  }
305
+ function getCliModelContextWindowTokens(mode) {
306
+ return CLI_MODEL_REGISTRY[mode].contextWindowTokens;
307
+ }
308
+ function countConsoleChars(value) {
309
+ if (value === null || value === undefined) {
310
+ return 0;
311
+ }
312
+ if (typeof value === "string") {
313
+ return value.length;
314
+ }
315
+ if (typeof value === "number" || typeof value === "boolean") {
316
+ return String(value).length;
317
+ }
318
+ if (Array.isArray(value)) {
319
+ return value.reduce((total, item) => total + countConsoleChars(item), 0);
320
+ }
321
+ if (typeof value === "object") {
322
+ return Object.values(value).reduce((total, item) => total + countConsoleChars(item), 0);
323
+ }
324
+ return String(value).length;
325
+ }
326
+ function estimateConsoleMessageTokens(messages) {
327
+ let charCount = 0;
328
+ let messageCount = 0;
329
+ for (const message of messages) {
330
+ if (!message || typeof message !== "object") {
331
+ continue;
332
+ }
333
+ messageCount += 1;
334
+ charCount += countConsoleChars(message.role);
335
+ charCount += countConsoleChars(message.content);
336
+ }
337
+ const estimatedTokens = Math.ceil(charCount / CONSOLE_EST_CHARS_PER_TOKEN);
338
+ return Math.max(0, estimatedTokens + (messageCount * CONSOLE_EST_MESSAGE_OVERHEAD_TOKENS));
339
+ }
340
+ function estimateConsoleContextTokens(messages, draftValue) {
341
+ const draft = String(draftValue || "");
342
+ const requestMessages = draft
343
+ ? [...messages, { role: "user", content: draft }]
344
+ : [...messages];
345
+ return estimateConsoleMessageTokens(requestMessages);
346
+ }
347
+ function formatCompactTokenCount(tokens) {
348
+ const value = Math.max(0, Math.round(tokens));
349
+ if (value < 1_000) {
350
+ return String(value);
351
+ }
352
+ if (value < 10_000) {
353
+ return `${(value / 1_000).toFixed(1)}K`;
354
+ }
355
+ if (value < 1_000_000) {
356
+ return `${Math.round(value / 1_000)}K`;
357
+ }
358
+ return `${(value / 1_000_000).toFixed(1)}M`;
359
+ }
360
+ function getConsoleApprovalFooterState(mode) {
361
+ if (mode === "trusted") {
362
+ return {
363
+ tone: "warning",
364
+ label: "bypass permissões ativo",
365
+ };
366
+ }
367
+ if (mode === "confirm") {
368
+ return {
369
+ tone: "primary",
370
+ label: "confirmação manual",
371
+ };
372
+ }
373
+ return {
374
+ tone: "muted",
375
+ label: "permissões padrão",
376
+ };
377
+ }
378
+ function getNextApprovalMode(mode) {
379
+ if (mode === "preview") {
380
+ return "confirm";
381
+ }
382
+ if (mode === "confirm") {
383
+ return "trusted";
384
+ }
385
+ return "preview";
386
+ }
387
+ function styleTranscriptLine(text, tone, enabled) {
388
+ if (!enabled) {
389
+ return text;
390
+ }
391
+ if (tone === "headline") {
392
+ return `${ANSI.bold}${ANSI.brandBlue}${text}${ANSI.reset}`;
393
+ }
394
+ if (tone === "assistant") {
395
+ return `${ANSI.white}${text}${ANSI.reset}`;
396
+ }
397
+ if (tone === "user") {
398
+ return `${ANSI.bold}${ANSI.white}${text}${ANSI.reset}`;
399
+ }
400
+ if (tone === "reasoning") {
401
+ return `${ANSI.slateItalic}${text}${ANSI.reset}`;
402
+ }
403
+ if (tone === "success") {
404
+ return `${ANSI.green}${text}${ANSI.reset}`;
405
+ }
406
+ if (tone === "warning") {
407
+ return `${ANSI.amber}${text}${ANSI.reset}`;
408
+ }
409
+ if (tone === "error") {
410
+ return `${ANSI.red}${text}${ANSI.reset}`;
411
+ }
412
+ return `${ANSI.slate}${text}${ANSI.reset}`;
413
+ }
414
+ function buildConsoleUsageBar(width, usageRatio, enabled) {
415
+ const clampedWidth = Math.max(10, width);
416
+ const clampedRatio = Math.max(0, Math.min(usageRatio, 1));
417
+ const filled = Math.round(clampedWidth * clampedRatio);
418
+ const empty = Math.max(0, clampedWidth - filled);
419
+ const filledBar = style("█".repeat(filled), ANSI.brandBlue, enabled);
420
+ const emptyBar = style("░".repeat(empty), ANSI.slate, enabled);
421
+ return `${style("[", ANSI.white, enabled)}${filledBar}${emptyBar}${style("]", ANSI.white, enabled)}`;
422
+ }
423
+ function buildConsoleFooterStatusLine(width, modelMode, usageTokens, enabled) {
424
+ const contextLimit = getCliModelContextWindowTokens(modelMode);
425
+ const usageRatio = contextLimit > 0 ? usageTokens / contextLimit : 0;
426
+ const percent = Math.round(Math.max(0, Math.min(usageRatio, 1)) * 100);
427
+ const barWidth = Math.max(12, Math.min(28, Math.floor(width * 0.18)));
428
+ return `${getCliModelLabel(modelMode)} | ${buildConsoleUsageBar(barWidth, usageRatio, enabled)} ${percent}% | ${formatCompactTokenCount(usageTokens)} tokens`;
429
+ }
430
+ function buildConsoleFooterApprovalLine(mode, enabled, statusSuffix) {
431
+ const state = getConsoleApprovalFooterState(mode);
432
+ const hint = statusSuffix || "Shift+Tab para alternar";
433
+ return style(`${state.label} (${hint})`, state.tone === "warning" ? ANSI.red : state.tone === "primary" ? ANSI.amber : ANSI.slate, enabled);
434
+ }
435
+ class ConsoleScreenRenderer {
436
+ modelMode;
437
+ approvalMode;
438
+ transcript = [];
439
+ onResize = () => {
440
+ this.render();
441
+ };
442
+ nextEntryId = 1;
443
+ active = false;
444
+ draftValue = "";
445
+ conversationMessages = [];
446
+ approvalStatusSuffix = null;
447
+ constructor(modelMode, approvalMode) {
448
+ this.modelMode = modelMode;
449
+ this.approvalMode = approvalMode;
450
+ }
451
+ activate() {
452
+ if (this.active || !supportsAnsi() || !input.isTTY || !output.isTTY) {
453
+ return;
454
+ }
455
+ this.active = true;
456
+ output.on("resize", this.onResize);
457
+ this.render();
458
+ }
459
+ dispose() {
460
+ if (!this.active) {
461
+ return;
462
+ }
463
+ output.off("resize", this.onResize);
464
+ this.active = false;
465
+ output.write("\u001b[?25h");
466
+ }
467
+ isActive() {
468
+ return this.active;
469
+ }
470
+ clearTranscript() {
471
+ this.transcript.splice(0, this.transcript.length);
472
+ this.render();
473
+ }
474
+ pushEntry(entry) {
475
+ const id = this.nextEntryId++;
476
+ this.transcript.push({
477
+ id,
478
+ ...entry,
479
+ });
480
+ this.render();
481
+ return id;
482
+ }
483
+ appendToEntry(id, text) {
484
+ if (!id || !text) {
485
+ return;
486
+ }
487
+ const entry = this.transcript.find((item) => item.id === id);
488
+ if (!entry) {
489
+ return;
490
+ }
491
+ entry.text += text;
492
+ this.render();
493
+ }
494
+ setDraftValue(value) {
495
+ this.draftValue = value;
496
+ this.render();
497
+ }
498
+ setConversationMessages(messages) {
499
+ this.conversationMessages = [...messages];
500
+ this.render();
501
+ }
502
+ setModelMode(mode) {
503
+ this.modelMode = mode;
504
+ this.render();
505
+ }
506
+ setApprovalMode(mode) {
507
+ this.approvalMode = mode;
508
+ this.approvalStatusSuffix = null;
509
+ this.render();
510
+ }
511
+ setApprovalStatusSuffix(value) {
512
+ this.approvalStatusSuffix = value;
513
+ this.render();
514
+ }
515
+ buildEntryLines(entry, width) {
516
+ const logicalLines = entry.text.split("\n");
517
+ const prefix = entry.prefix || "";
518
+ const continuationPrefix = entry.continuationPrefix ?? " ".repeat(prefix.length);
519
+ const rendered = [];
520
+ let isFirstVisualLine = true;
521
+ for (const logicalLine of logicalLines) {
522
+ const currentPrefix = isFirstVisualLine ? prefix : continuationPrefix;
523
+ const wrapped = sliceByWidth(logicalLine, Math.max(1, width - currentPrefix.length));
524
+ wrapped.forEach((segment, index) => {
525
+ const linePrefix = isFirstVisualLine && index === 0 ? prefix : continuationPrefix;
526
+ rendered.push(styleTranscriptLine(`${linePrefix}${segment}`, entry.tone, supportsAnsi()));
527
+ });
528
+ if (logicalLine.length === 0 && wrapped.length === 0) {
529
+ rendered.push("");
530
+ }
531
+ isFirstVisualLine = false;
532
+ }
533
+ return rendered;
534
+ }
535
+ render() {
536
+ if (!this.active) {
537
+ return;
538
+ }
539
+ const enabled = supportsAnsi();
540
+ const width = Math.max(48, Number(output.columns || 96));
541
+ const height = Math.max(12, Number(output.rows || 24));
542
+ const separator = style("─".repeat(width), ANSI.brandBlue, enabled);
543
+ const composer = renderConsoleComposerLines(this.draftValue, width, enabled);
544
+ const usageTokens = Math.min(estimateConsoleContextTokens(this.conversationMessages, this.draftValue), getCliModelContextWindowTokens(this.modelMode));
545
+ const footerLines = [
546
+ separator,
547
+ ...composer.renderedLines,
548
+ separator,
549
+ buildConsoleFooterStatusLine(width, this.modelMode, usageTokens, enabled),
550
+ buildConsoleFooterApprovalLine(this.approvalMode, enabled, this.approvalStatusSuffix),
551
+ ];
552
+ const transcriptHeight = Math.max(0, height - footerLines.length);
553
+ const transcriptLines = this.transcript.flatMap((entry) => this.buildEntryLines(entry, width));
554
+ const visibleTranscript = transcriptLines.slice(-transcriptHeight);
555
+ const paddedTranscript = [
556
+ ...Array.from({ length: Math.max(0, transcriptHeight - visibleTranscript.length) }, () => ""),
557
+ ...visibleTranscript,
558
+ ];
559
+ output.write("\u001b[?25l");
560
+ output.write("\u001b[H\u001b[2J");
561
+ output.write([...paddedTranscript, ...footerLines].join("\n"));
562
+ cursorTo(output, Math.min(width - 1, CONSOLE_COMPOSER_CURSOR_COLUMN + composer.cursorColumn), transcriptHeight + 1 + composer.cursorLineIndex);
563
+ output.write("\u001b[?25h");
564
+ }
565
+ }
300
566
  export function resolveCliModelMode(value) {
301
567
  const normalized = normalizeText(value).toLowerCase();
302
568
  if (!normalized) {
@@ -806,6 +1072,16 @@ async function handleConsoleWorkspaceCommand(normalizedPrompt, config, sessionId
806
1072
  printWarning("Comando de workspace inválido. Use /workspace help.");
807
1073
  return true;
808
1074
  }
1075
+ async function updateConsoleApprovalMode(config, approvalMode) {
1076
+ const response = await patchDeviceJson(config.apiBaseUrl, config.deviceToken, "/v1/devices/cli/device/settings", {
1077
+ approval_mode: approvalMode,
1078
+ });
1079
+ const resolvedMode = normalizeText(response.device?.approval_mode);
1080
+ const nextMode = resolvedMode || approvalMode;
1081
+ config.approvalMode = nextMode;
1082
+ await saveBridgeConfig(config);
1083
+ return nextMode;
1084
+ }
809
1085
  function renderPromptFrameLine(width, edgeLeft, edgeRight) {
810
1086
  return style(`${edgeLeft}${"─".repeat(width)}${edgeRight}`, ANSI.brandBlue, supportsAnsi());
811
1087
  }
@@ -817,11 +1093,18 @@ function sliceByWidth(text, width) {
817
1093
  return [""];
818
1094
  }
819
1095
  const parts = [];
820
- let offset = 0;
821
- while (offset < text.length) {
822
- parts.push(text.slice(offset, offset + width));
823
- offset += width;
1096
+ let remaining = text;
1097
+ while (remaining.length > width) {
1098
+ const breakpoint = Math.max(remaining.lastIndexOf(" ", width), remaining.lastIndexOf("\t", width));
1099
+ if (breakpoint > 0) {
1100
+ parts.push(remaining.slice(0, breakpoint).trimEnd());
1101
+ remaining = remaining.slice(breakpoint).replace(/^[ \t]+/, "");
1102
+ continue;
1103
+ }
1104
+ parts.push(remaining.slice(0, width));
1105
+ remaining = remaining.slice(width);
824
1106
  }
1107
+ parts.push(remaining);
825
1108
  return parts.length ? parts : [""];
826
1109
  }
827
1110
  export function buildConsoleComposerLayout(value, innerWidth) {
@@ -834,16 +1117,16 @@ export function buildConsoleComposerLayout(value, innerWidth) {
834
1117
  };
835
1118
  }
836
1119
  const logicalLines = value.split("\n");
837
- const lastLogicalLine = logicalLines[logicalLines.length - 1] ?? "";
838
1120
  const lines = [];
839
1121
  for (const logicalLine of logicalLines) {
840
1122
  const wrapped = sliceByWidth(logicalLine, lineContentWidth);
841
1123
  lines.push(...wrapped);
842
1124
  }
843
1125
  const trailingNewline = value.endsWith("\n");
1126
+ const lastVisualLine = lines[lines.length - 1] ?? "";
844
1127
  const needsSoftWrapCursorRow = !trailingNewline
845
- && lastLogicalLine.length > 0
846
- && lastLogicalLine.length % lineContentWidth === 0;
1128
+ && lastVisualLine.length > 0
1129
+ && lastVisualLine.length === lineContentWidth;
847
1130
  if (needsSoftWrapCursorRow) {
848
1131
  lines.push("");
849
1132
  }
@@ -856,7 +1139,7 @@ export function buildConsoleComposerLayout(value, innerWidth) {
856
1139
  ? 0
857
1140
  : needsSoftWrapCursorRow
858
1141
  ? 0
859
- : lastLogicalLine.length % lineContentWidth;
1142
+ : lastVisualLine.length;
860
1143
  return {
861
1144
  lines,
862
1145
  cursorLineIndex,
@@ -906,6 +1189,19 @@ export function tryConsumeControlSequence(buffer) {
906
1189
  };
907
1190
  }
908
1191
  }
1192
+ const approvalCycleSequences = [
1193
+ "\u001b[Z",
1194
+ "\u001b[1;2Z",
1195
+ "\u001b[27;2;9~",
1196
+ ];
1197
+ for (const sequence of approvalCycleSequences) {
1198
+ if (buffer.startsWith(sequence)) {
1199
+ return {
1200
+ consumed: sequence.length,
1201
+ action: "cycle_approval",
1202
+ };
1203
+ }
1204
+ }
909
1205
  if (buffer === "\u001b" || /^\u001b\[[0-9;?]*$/.test(buffer)) {
910
1206
  return {
911
1207
  consumed: 0,
@@ -934,7 +1230,7 @@ export function tryConsumeControlSequence(buffer) {
934
1230
  }
935
1231
  return null;
936
1232
  }
937
- async function askConsoleInput(rl) {
1233
+ async function askConsoleInput(rl, options) {
938
1234
  if (!supportsAnsi() || typeof input.setRawMode !== "function" || !input.isTTY) {
939
1235
  return normalizeText(await question(rl, "> "));
940
1236
  }
@@ -943,16 +1239,19 @@ async function askConsoleInput(rl) {
943
1239
  input.resume();
944
1240
  return await new Promise((resolve, reject) => {
945
1241
  const enabled = supportsAnsi();
1242
+ const ui = options?.ui && options.ui.isActive() ? options.ui : null;
946
1243
  const availableWidth = Number(output.columns || 96);
947
1244
  const innerWidth = Math.max(42, Math.min(availableWidth - 8, 116));
948
1245
  const sectionTopOffsetFromInputLine = 1;
949
- let renderedOnce = false;
950
1246
  let value = "";
1247
+ let renderedOnce = false;
1248
+ let controlBuffer = "";
951
1249
  const cleanup = () => {
952
1250
  input.removeListener("data", onData);
953
1251
  input.setRawMode(false);
954
1252
  input.pause();
955
1253
  rl.resume();
1254
+ ui?.setDraftValue("");
956
1255
  };
957
1256
  const renderInputContent = () => {
958
1257
  const promptPlain = "> ";
@@ -968,7 +1267,11 @@ async function askConsoleInput(rl) {
968
1267
  : value;
969
1268
  return `${promptStyled} ${visibleValue}${" ".repeat(Math.max(0, maxValueLength - visibleValue.length))}`;
970
1269
  };
971
- const render = () => {
1270
+ const renderLegacyInput = () => {
1271
+ if (ui) {
1272
+ ui.setDraftValue(value);
1273
+ return;
1274
+ }
972
1275
  if (renderedOnce) {
973
1276
  cursorTo(output, 0);
974
1277
  moveCursor(output, 0, -sectionTopOffsetFromInputLine);
@@ -987,9 +1290,38 @@ async function askConsoleInput(rl) {
987
1290
  const visibleValueLength = Math.min(value.length, Math.max(0, innerWidth - 2));
988
1291
  cursorTo(output, 4 + visibleValueLength);
989
1292
  };
1293
+ const consumeControlBuffer = () => {
1294
+ while (controlBuffer.length > 0) {
1295
+ const parsed = tryConsumeControlSequence(controlBuffer);
1296
+ if (!parsed) {
1297
+ controlBuffer = "";
1298
+ return;
1299
+ }
1300
+ if (parsed.action === "incomplete") {
1301
+ return;
1302
+ }
1303
+ controlBuffer = controlBuffer.slice(parsed.consumed);
1304
+ if (parsed.action === "ignore") {
1305
+ continue;
1306
+ }
1307
+ if (parsed.action === "cycle_approval") {
1308
+ void options?.onCycleApprovalMode?.();
1309
+ continue;
1310
+ }
1311
+ if (parsed.action === "newline") {
1312
+ value += "\n";
1313
+ renderLegacyInput();
1314
+ }
1315
+ }
1316
+ };
990
1317
  const onData = (chunk) => {
991
1318
  const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
992
1319
  for (const char of Array.from(text)) {
1320
+ if (controlBuffer || char === "\u001b") {
1321
+ controlBuffer += char;
1322
+ consumeControlBuffer();
1323
+ continue;
1324
+ }
993
1325
  if (char === "\u0003") {
994
1326
  cleanup();
995
1327
  reject(createCliExitError());
@@ -997,23 +1329,25 @@ async function askConsoleInput(rl) {
997
1329
  }
998
1330
  if (char === "\r" || char === "\n") {
999
1331
  cleanup();
1000
- output.write("\n");
1332
+ if (!ui) {
1333
+ output.write("\n");
1334
+ }
1001
1335
  resolve(normalizeText(value));
1002
1336
  return;
1003
1337
  }
1004
1338
  if (char === "\u007f" || char === "\b") {
1005
1339
  value = value.slice(0, -1);
1006
- render();
1340
+ renderLegacyInput();
1007
1341
  continue;
1008
1342
  }
1009
- if (char === "\u001b") {
1343
+ if (char === "\t") {
1010
1344
  continue;
1011
1345
  }
1012
1346
  value += char;
1013
- render();
1347
+ renderLegacyInput();
1014
1348
  }
1015
1349
  };
1016
- render();
1350
+ renderLegacyInput();
1017
1351
  input.on("data", onData);
1018
1352
  });
1019
1353
  }
@@ -1315,15 +1649,252 @@ async function followConsoleJob(rl, config, jobId) {
1315
1649
  }
1316
1650
  async function runOttoConsole(rl, config, runtimeSession, options) {
1317
1651
  let activeModel = "fast";
1318
- printConsoleScreen(runtimeSession, activeModel);
1319
1652
  const sessionId = randomUUID();
1320
1653
  const conversation = [];
1654
+ const ui = supportsAnsi() && typeof input.setRawMode === "function" && input.isTTY && output.isTTY
1655
+ ? new ConsoleScreenRenderer(activeModel, config.approvalMode)
1656
+ : null;
1657
+ ui?.activate();
1658
+ if (!ui) {
1659
+ printConsoleScreen(runtimeSession, activeModel);
1660
+ }
1661
+ const renderConversationState = () => {
1662
+ ui?.setConversationMessages(conversation);
1663
+ ui?.setModelMode(activeModel);
1664
+ };
1665
+ renderConversationState();
1666
+ const emitConsoleEntry = (text, tone, options) => {
1667
+ if (ui) {
1668
+ ui.pushEntry({
1669
+ text,
1670
+ tone,
1671
+ prefix: options?.prefix,
1672
+ continuationPrefix: options?.continuationPrefix,
1673
+ });
1674
+ return;
1675
+ }
1676
+ if (options?.prefix) {
1677
+ console.log(`${options.prefix}${text}`);
1678
+ return;
1679
+ }
1680
+ if (tone === "warning") {
1681
+ printWarning(text);
1682
+ return;
1683
+ }
1684
+ if (tone === "error") {
1685
+ printError(text);
1686
+ return;
1687
+ }
1688
+ if (tone === "success") {
1689
+ printSuccess(text);
1690
+ return;
1691
+ }
1692
+ if (tone === "muted" || tone === "reasoning") {
1693
+ printSoft(text);
1694
+ return;
1695
+ }
1696
+ console.log(text);
1697
+ };
1698
+ const emitUserPrompt = (text) => {
1699
+ emitConsoleEntry(text, "user", {
1700
+ prefix: "> ",
1701
+ continuationPrefix: " ",
1702
+ });
1703
+ };
1704
+ const setApprovalMode = async (nextMode) => {
1705
+ if (ui) {
1706
+ ui.setApprovalStatusSuffix("atualizando...");
1707
+ }
1708
+ try {
1709
+ const resolvedMode = await updateConsoleApprovalMode(config, nextMode);
1710
+ ui?.setApprovalMode(resolvedMode);
1711
+ emitConsoleEntry(`Modo de aprovação: ${getConsoleApprovalFooterState(resolvedMode).label}.`, "muted");
1712
+ }
1713
+ catch (error) {
1714
+ const detail = error instanceof Error ? error.message : String(error || "Falha ao atualizar o modo de aprovação.");
1715
+ ui?.setApprovalStatusSuffix("falha ao atualizar");
1716
+ emitConsoleEntry(detail, "error");
1717
+ }
1718
+ };
1719
+ const cycleApprovalMode = () => {
1720
+ void setApprovalMode(getNextApprovalMode(config.approvalMode));
1721
+ };
1722
+ const readConsoleInput = async () => {
1723
+ return await askConsoleInput(rl, {
1724
+ ui,
1725
+ onCycleApprovalMode: cycleApprovalMode,
1726
+ });
1727
+ };
1728
+ const askConsoleDecision = async (promptText, defaultValue = true) => {
1729
+ emitConsoleEntry(`${promptText} ${defaultValue ? "[Y/n]" : "[y/N]"}`, "warning");
1730
+ const answer = normalizeText(await readConsoleInput()).toLowerCase();
1731
+ emitUserPrompt(answer || (defaultValue ? "sim" : "não"));
1732
+ if (!answer) {
1733
+ return defaultValue;
1734
+ }
1735
+ return ["y", "yes", "s", "sim"].includes(answer);
1736
+ };
1321
1737
  const printConsoleHelp = () => {
1322
- printSection("Console");
1323
- printSoft("Comandos: /help, /model [fast|thinking], /workspace, /status, /clear, /exit");
1324
- printSoft("Bridge: otto-bridge terminal, otto-bridge extensions --install <name>, otto-bridge update");
1325
- printSoft("Composer: Enter envia. Multiline desativado para evitar duplicacao no terminal.");
1326
- printSoft("Workspace: /workspace list, /workspace attach <path>, /workspace use <id|n>, /workspace clear");
1738
+ emitConsoleEntry("Console", "headline");
1739
+ emitConsoleEntry("Comandos: /help, /model [fast|thinking], /approval [preview|confirm|trusted], /workspace, /status, /clear, /exit", "muted");
1740
+ emitConsoleEntry("Composer: Enter envia, Shift+Enter adiciona linha e Shift+Tab alterna o modo de aprovação.", "muted");
1741
+ emitConsoleEntry("Workspace: /workspace list, /workspace attach <path>, /workspace use <id|n>, /workspace clear", "muted");
1742
+ };
1743
+ const printConsoleWorkspaceStatus = async () => {
1744
+ const [workspaces, activeWorkspace] = await Promise.all([
1745
+ listAttachedWorkspacesForRuntime({
1746
+ apiBaseUrl: config.apiBaseUrl,
1747
+ deviceToken: config.deviceToken,
1748
+ baseCwd: process.cwd(),
1749
+ }),
1750
+ getRuntimeSessionWorkspace({
1751
+ apiBaseUrl: config.apiBaseUrl,
1752
+ deviceToken: config.deviceToken,
1753
+ sessionId,
1754
+ baseCwd: process.cwd(),
1755
+ }),
1756
+ ]);
1757
+ emitConsoleEntry("Workspace", "headline");
1758
+ if (activeWorkspace) {
1759
+ emitConsoleEntry(`Ativo neste console: ${formatConsoleWorkspaceLine(activeWorkspace, { isActive: true })}`, "success");
1760
+ }
1761
+ else {
1762
+ emitConsoleEntry("Nenhum workspace ativo neste console.", "muted");
1763
+ }
1764
+ if (workspaces.length === 0) {
1765
+ emitConsoleEntry("Nenhum workspace anexado. Use /workspace attach <caminho>.", "muted");
1766
+ return;
1767
+ }
1768
+ workspaces.forEach((workspace, index) => {
1769
+ const isActive = normalizeText(workspace.workspace_id) === normalizeText(activeWorkspace?.workspace_id);
1770
+ emitConsoleEntry(formatConsoleWorkspaceLine(workspace, { index, isActive }), "muted");
1771
+ });
1772
+ };
1773
+ const handleWorkspaceCommand = async (normalizedPrompt) => {
1774
+ if (!normalizedPrompt.startsWith("/workspace")) {
1775
+ return false;
1776
+ }
1777
+ const remainder = normalizeText(normalizedPrompt.slice("/workspace".length));
1778
+ if (!remainder || remainder === "list" || remainder === "status") {
1779
+ await printConsoleWorkspaceStatus();
1780
+ return true;
1781
+ }
1782
+ if (remainder === "help") {
1783
+ emitConsoleEntry("Workspace", "headline");
1784
+ emitConsoleEntry("/workspace lista os workspaces anexados e o ativo", "muted");
1785
+ emitConsoleEntry("/workspace attach <path> anexa uma pasta ou repo neste device", "muted");
1786
+ emitConsoleEntry("/workspace use <id|n> ativa o workspace para o console atual", "muted");
1787
+ emitConsoleEntry("/workspace clear remove o workspace ativo deste console", "muted");
1788
+ return true;
1789
+ }
1790
+ if (remainder.startsWith("attach ")) {
1791
+ const rootPath = normalizeText(remainder.slice("attach ".length));
1792
+ if (!rootPath) {
1793
+ emitConsoleEntry("Use /workspace attach <caminho>.", "warning");
1794
+ return true;
1795
+ }
1796
+ const workspace = await attachWorkspaceForRuntime({
1797
+ apiBaseUrl: config.apiBaseUrl,
1798
+ deviceToken: config.deviceToken,
1799
+ rootPath,
1800
+ baseCwd: process.cwd(),
1801
+ });
1802
+ emitConsoleEntry(`Workspace anexado: ${formatConsoleWorkspaceLine(workspace)}`, "success");
1803
+ return true;
1804
+ }
1805
+ if (remainder.startsWith("use ")) {
1806
+ const selectionToken = normalizeText(remainder.slice("use ".length));
1807
+ if (!selectionToken) {
1808
+ emitConsoleEntry("Use /workspace use <id|numero>.", "warning");
1809
+ return true;
1810
+ }
1811
+ const workspaces = await listAttachedWorkspacesForRuntime({
1812
+ apiBaseUrl: config.apiBaseUrl,
1813
+ deviceToken: config.deviceToken,
1814
+ baseCwd: process.cwd(),
1815
+ });
1816
+ const selectedWorkspace = resolveConsoleWorkspaceSelection(selectionToken, workspaces);
1817
+ if (!selectedWorkspace?.workspace_id) {
1818
+ emitConsoleEntry("Workspace não encontrado. Rode /workspace para listar as opções.", "warning");
1819
+ return true;
1820
+ }
1821
+ const activated = await activateWorkspaceForRuntime({
1822
+ apiBaseUrl: config.apiBaseUrl,
1823
+ deviceToken: config.deviceToken,
1824
+ workspaceId: selectedWorkspace.workspace_id,
1825
+ sessionId,
1826
+ baseCwd: process.cwd(),
1827
+ });
1828
+ if (activated) {
1829
+ emitConsoleEntry(`Workspace ativo neste console: ${formatConsoleWorkspaceLine(activated, { isActive: true })}`, "success");
1830
+ }
1831
+ else {
1832
+ emitConsoleEntry("Não foi possível ativar o workspace selecionado.", "warning");
1833
+ }
1834
+ return true;
1835
+ }
1836
+ if (remainder === "clear") {
1837
+ await clearRuntimeWorkspaceForSession({
1838
+ apiBaseUrl: config.apiBaseUrl,
1839
+ deviceToken: config.deviceToken,
1840
+ sessionId,
1841
+ });
1842
+ emitConsoleEntry("Workspace ativo deste console removido.", "muted");
1843
+ return true;
1844
+ }
1845
+ emitConsoleEntry("Comando de workspace inválido. Use /workspace help.", "warning");
1846
+ return true;
1847
+ };
1848
+ const followConsoleJobUi = async (jobId) => {
1849
+ let lastStatus = "";
1850
+ let lastStepId = "";
1851
+ let awaitingDecision = false;
1852
+ for (;;) {
1853
+ const envelope = await getRuntimeCliJob(config, jobId);
1854
+ const job = envelope.job || {};
1855
+ const status = normalizeText(job.status).toLowerCase() || "unknown";
1856
+ const stepId = extractJobStepId(job);
1857
+ if (status !== lastStatus || stepId !== lastStepId) {
1858
+ const statusLabel = `${status}${stepId ? ` · ${stepId}` : ""}`;
1859
+ emitConsoleEntry(`local: ${statusLabel}`, "muted");
1860
+ lastStatus = status;
1861
+ lastStepId = stepId;
1862
+ }
1863
+ if (status === "confirm_required" && !awaitingDecision) {
1864
+ awaitingDecision = true;
1865
+ emitConsoleEntry(extractConfirmationPrompt(job), "warning");
1866
+ const approve = await askConsoleDecision("Aprovar este passo", true);
1867
+ if (approve) {
1868
+ await confirmRuntimeCliJob(config, jobId, "approve");
1869
+ }
1870
+ else {
1871
+ const reject = await askConsoleDecision("Rejeitar explicitamente este passo", true);
1872
+ if (reject) {
1873
+ await confirmRuntimeCliJob(config, jobId, "reject");
1874
+ }
1875
+ else {
1876
+ await cancelRuntimeCliJob(config, jobId, "Cancelado no Otto Console");
1877
+ }
1878
+ }
1879
+ awaitingDecision = false;
1880
+ continue;
1881
+ }
1882
+ if (status === "completed" || status === "failed" || status === "cancelled") {
1883
+ const summary = extractJobSummary(job)
1884
+ || (status === "completed"
1885
+ ? "Execução local concluída."
1886
+ : status === "failed"
1887
+ ? "Execução local falhou."
1888
+ : "Execução local cancelada.");
1889
+ emitConsoleEntry(summary, status === "failed" ? "error" : status === "cancelled" ? "warning" : "assistant");
1890
+ const rendered = renderStructuredOutcome(job);
1891
+ if (rendered) {
1892
+ emitConsoleEntry(rendered, "muted");
1893
+ }
1894
+ return buildConversationSummary(summary, job);
1895
+ }
1896
+ await delay(1400);
1897
+ }
1327
1898
  };
1328
1899
  const handlePrompt = async (promptText) => {
1329
1900
  const normalizedPrompt = normalizeText(promptText);
@@ -1336,62 +1907,88 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1336
1907
  }
1337
1908
  if (normalizedPrompt === "/clear") {
1338
1909
  conversation.splice(0, conversation.length);
1339
- printConsoleScreen(runtimeSession, activeModel);
1340
- printMuted("Contexto local do console limpo.");
1910
+ ui?.clearTranscript();
1911
+ renderConversationState();
1912
+ if (!ui) {
1913
+ printConsoleScreen(runtimeSession, activeModel);
1914
+ }
1915
+ emitConsoleEntry("Contexto local do console limpo.", "muted");
1341
1916
  return;
1342
1917
  }
1343
1918
  if (normalizedPrompt === "/model") {
1344
- printMuted(`Modelo ativo: ${getCliModelLabel(activeModel)}.`);
1345
- printSoft("Use /model fast ou /model thinking para trocar.");
1919
+ emitConsoleEntry(`Modelo ativo: ${getCliModelLabel(activeModel)}.`, "muted");
1920
+ emitConsoleEntry("Use /model fast ou /model thinking para trocar.", "muted");
1346
1921
  return;
1347
1922
  }
1348
1923
  if (normalizedPrompt.startsWith("/model ")) {
1349
1924
  const nextMode = resolveCliModelMode(normalizedPrompt.slice("/model ".length));
1350
1925
  if (!nextMode) {
1351
- printWarning("Modelo inválido. Use /model fast ou /model thinking.");
1926
+ emitConsoleEntry("Modelo inválido. Use /model fast ou /model thinking.", "warning");
1352
1927
  return;
1353
1928
  }
1354
1929
  if (nextMode === activeModel) {
1355
- printMuted(`Modelo já está em ${getCliModelLabel(activeModel)}.`);
1930
+ emitConsoleEntry(`Modelo já está em ${getCliModelLabel(activeModel)}.`, "muted");
1356
1931
  return;
1357
1932
  }
1358
1933
  activeModel = nextMode;
1359
- printConsoleScreen(runtimeSession, activeModel);
1360
- printSuccess(`Modelo ativo: ${getCliModelLabel(activeModel)}.`);
1934
+ renderConversationState();
1935
+ emitConsoleEntry(`Modelo ativo: ${getCliModelLabel(activeModel)}.`, "success");
1936
+ return;
1937
+ }
1938
+ if (normalizedPrompt === "/approval") {
1939
+ emitConsoleEntry(`Modo atual: ${getConsoleApprovalFooterState(config.approvalMode).label}.`, "muted");
1940
+ emitConsoleEntry("Use /approval preview, /approval confirm ou /approval trusted.", "muted");
1941
+ return;
1942
+ }
1943
+ if (normalizedPrompt.startsWith("/approval ")) {
1944
+ const nextMode = normalizeText(normalizedPrompt.slice("/approval ".length)).toLowerCase();
1945
+ if (!["preview", "confirm", "trusted"].includes(nextMode)) {
1946
+ emitConsoleEntry("Modo inválido. Use /approval preview, /approval confirm ou /approval trusted.", "warning");
1947
+ return;
1948
+ }
1949
+ if (nextMode === config.approvalMode) {
1950
+ emitConsoleEntry(`O modo de aprovação já está em ${getConsoleApprovalFooterState(config.approvalMode).label}.`, "muted");
1951
+ return;
1952
+ }
1953
+ await setApprovalMode(nextMode);
1361
1954
  return;
1362
1955
  }
1363
1956
  if (normalizedPrompt === "/status") {
1364
- console.log(`${style("Model", ANSI.brandBlue, supportsAnsi())}: ${getCliModelLabel(activeModel)}`);
1365
- renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
1957
+ emitConsoleEntry(`Model: ${getCliModelLabel(activeModel)}`, "muted");
1958
+ renderStatusOverview(config, runtimeSession).forEach((line) => emitConsoleEntry(line, "muted"));
1366
1959
  const runtimeFailure = runtimeSession.getLastError();
1367
1960
  if (runtimeFailure) {
1368
- printWarning(`Runtime reportou erro: ${runtimeFailure}`);
1961
+ emitConsoleEntry(`Runtime reportou erro: ${runtimeFailure}`, "warning");
1369
1962
  }
1370
1963
  return;
1371
1964
  }
1372
1965
  if (normalizedPrompt.startsWith("/workspace")) {
1373
1966
  try {
1374
- if (await handleConsoleWorkspaceCommand(normalizedPrompt, config, sessionId)) {
1967
+ if (await handleWorkspaceCommand(normalizedPrompt)) {
1375
1968
  return;
1376
1969
  }
1377
1970
  }
1378
1971
  catch (error) {
1379
1972
  const detail = error instanceof Error ? error.message : String(error || "Erro ao gerenciar workspace.");
1380
- printError(detail);
1973
+ emitConsoleEntry(detail, "error");
1381
1974
  return;
1382
1975
  }
1383
1976
  }
1384
1977
  if (normalizedPrompt === "/exit") {
1385
1978
  throw createCliExitError();
1386
1979
  }
1980
+ emitUserPrompt(normalizedPrompt);
1387
1981
  conversation.push({ role: "user", content: normalizedPrompt });
1388
1982
  while (conversation.length > 18) {
1389
1983
  conversation.shift();
1390
1984
  }
1985
+ renderConversationState();
1391
1986
  let streamedAssistant = "";
1987
+ let assistantEntryId = null;
1392
1988
  let assistantPrefixPrinted = false;
1393
1989
  let reasoningPrefixPrinted = false;
1394
1990
  let contentSeparatedFromReasoning = false;
1991
+ let reasoningEntryId = null;
1395
1992
  let handoffPayload = null;
1396
1993
  await streamDeviceCliChat(config, {
1397
1994
  messages: conversation,
@@ -1407,7 +2004,7 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1407
2004
  if (chunkType === "search_status") {
1408
2005
  const status = normalizeText(event.status);
1409
2006
  if (status) {
1410
- printMuted(`Busca: ${status}`);
2007
+ emitConsoleEntry(`Busca: ${status}`, "muted");
1411
2008
  }
1412
2009
  return;
1413
2010
  }
@@ -1420,10 +2017,18 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1420
2017
  : "";
1421
2018
  if (reasoningChunk) {
1422
2019
  if (!reasoningPrefixPrinted) {
1423
- output.write(`${style("Pensando (OttoAI Thinking)\n", ANSI.brandBlue, supportsAnsi())}`);
2020
+ emitConsoleEntry("Pensando (OttoAI Thinking)", "headline");
2021
+ reasoningEntryId = ui
2022
+ ? ui.pushEntry({ text: "", tone: "reasoning" })
2023
+ : null;
1424
2024
  reasoningPrefixPrinted = true;
1425
2025
  }
1426
- output.write(style(reasoningChunk, ANSI.slateItalic, supportsAnsi()));
2026
+ if (ui && reasoningEntryId) {
2027
+ ui.appendToEntry(reasoningEntryId, reasoningChunk);
2028
+ }
2029
+ else {
2030
+ output.write(style(reasoningChunk, ANSI.slateItalic, supportsAnsi()));
2031
+ }
1427
2032
  return;
1428
2033
  }
1429
2034
  const contentChunk = typeof event.content === "string" ? event.content : "";
@@ -1431,17 +2036,24 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1431
2036
  return;
1432
2037
  }
1433
2038
  if (reasoningPrefixPrinted && !contentSeparatedFromReasoning) {
1434
- output.write("\n\n");
2039
+ emitConsoleEntry("", "muted");
1435
2040
  contentSeparatedFromReasoning = true;
1436
2041
  }
1437
2042
  if (!assistantPrefixPrinted) {
1438
- output.write(`${style("•", ANSI.brandBlue, supportsAnsi())} `);
2043
+ assistantEntryId = ui
2044
+ ? ui.pushEntry({ text: "", tone: "assistant" })
2045
+ : null;
1439
2046
  assistantPrefixPrinted = true;
1440
2047
  }
1441
- output.write(contentChunk);
2048
+ if (ui && assistantEntryId) {
2049
+ ui.appendToEntry(assistantEntryId, contentChunk);
2050
+ }
2051
+ else {
2052
+ output.write(contentChunk);
2053
+ }
1442
2054
  streamedAssistant += contentChunk;
1443
2055
  });
1444
- if (assistantPrefixPrinted) {
2056
+ if (assistantPrefixPrinted && !ui) {
1445
2057
  output.write("\n");
1446
2058
  }
1447
2059
  let finalAssistantSummary = normalizeText(streamedAssistant);
@@ -1453,10 +2065,10 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1453
2065
  : null;
1454
2066
  const jobId = normalizeText(job?.id);
1455
2067
  if (bridgeSummary && !jobId) {
1456
- printAssistantMessage(bridgeSummary);
2068
+ emitConsoleEntry(bridgeSummary, "assistant");
1457
2069
  }
1458
2070
  if (jobId) {
1459
- finalAssistantSummary = await followConsoleJob(rl, config, jobId);
2071
+ finalAssistantSummary = await followConsoleJobUi(jobId);
1460
2072
  }
1461
2073
  else if (bridgeSummary) {
1462
2074
  finalAssistantSummary = bridgeSummary;
@@ -1467,23 +2079,29 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1467
2079
  while (conversation.length > 18) {
1468
2080
  conversation.shift();
1469
2081
  }
2082
+ renderConversationState();
1470
2083
  }
1471
2084
  };
1472
- if (options?.initialPrompt) {
1473
- await handlePrompt(options.initialPrompt);
1474
- }
1475
- for (;;) {
1476
- const promptText = await askConsoleInput(rl);
1477
- try {
1478
- await handlePrompt(promptText);
2085
+ try {
2086
+ if (options?.initialPrompt) {
2087
+ await handlePrompt(options.initialPrompt);
1479
2088
  }
1480
- catch (error) {
1481
- if (isCliExitError(error)) {
1482
- break;
2089
+ for (;;) {
2090
+ const promptText = await readConsoleInput();
2091
+ try {
2092
+ await handlePrompt(promptText);
2093
+ }
2094
+ catch (error) {
2095
+ if (isCliExitError(error)) {
2096
+ break;
2097
+ }
2098
+ throw error;
1483
2099
  }
1484
- throw error;
1485
2100
  }
1486
2101
  }
2102
+ finally {
2103
+ ui?.dispose();
2104
+ }
1487
2105
  }
1488
2106
  async function printStatusView(rl, config, runtimeSession) {
1489
2107
  printSection("Bridge Status");
@@ -1516,7 +2134,7 @@ async function printHelpView(rl) {
1516
2134
  { text: "otto-bridge version | otto-bridge unpair", tone: "primary" },
1517
2135
  { text: "Mostra a versao instalada ou remove o pairing local.", tone: "muted" },
1518
2136
  { text: "", tone: "muted" },
1519
- { text: "Dentro do console: /help, /model fast|thinking, /status, /clear, /exit", tone: "muted" },
2137
+ { text: "Dentro do console: /help, /model fast|thinking, /approval preview|confirm|trusted, /status, /clear, /exit", tone: "muted" },
1520
2138
  ]));
1521
2139
  await pauseForEnter(rl);
1522
2140
  }
package/dist/http.js CHANGED
@@ -60,6 +60,15 @@ export async function postDeviceJson(apiBaseUrl, deviceToken, pathname, body) {
60
60
  body: JSON.stringify(body),
61
61
  });
62
62
  }
63
+ export async function patchDeviceJson(apiBaseUrl, deviceToken, pathname, body) {
64
+ return await requestJson(apiBaseUrl, pathname, {
65
+ method: "PATCH",
66
+ headers: buildDeviceAuthHeaders(deviceToken, {
67
+ "Content-Type": "application/json",
68
+ }),
69
+ body: JSON.stringify(body),
70
+ });
71
+ }
63
72
  export async function deleteDeviceJson(apiBaseUrl, deviceToken, pathname) {
64
73
  return await requestJson(apiBaseUrl, pathname, {
65
74
  method: "DELETE",
package/dist/main.js CHANGED
@@ -122,6 +122,7 @@ Console:
122
122
  /help
123
123
  /model fast
124
124
  /model thinking
125
+ /approval preview|confirm|trusted
125
126
  /workspace
126
127
  /workspace attach <path>
127
128
  /workspace use <id|n>
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "1.1.0";
2
+ export const BRIDGE_VERSION = "1.1.1";
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.0",
3
+ "version": "1.1.1",
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.0");
27
+ console.log("\n[otto-bridge] Welcome to OTTOAI 1.1.1");
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"], {