@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 +11 -8
- package/dist/cli_terminal.js +676 -58
- package/dist/http.js +9 -0
- package/dist/main.js +1 -0
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.mjs +1 -1
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
|
|
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.
|
|
41
|
+
npm install -g ./leg3ndy-otto-bridge-1.1.1.tgz
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Na linha `1.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
188
|
+
## Handoff rapido da linha 1.1.1
|
|
186
189
|
|
|
187
190
|
Ja fechado no codigo:
|
|
188
191
|
|
package/dist/cli_terminal.js
CHANGED
|
@@ -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
|
|
821
|
-
while (
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
&&
|
|
846
|
-
&&
|
|
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
|
-
:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1340
|
+
renderLegacyInput();
|
|
1007
1341
|
continue;
|
|
1008
1342
|
}
|
|
1009
|
-
if (char === "\
|
|
1343
|
+
if (char === "\t") {
|
|
1010
1344
|
continue;
|
|
1011
1345
|
}
|
|
1012
1346
|
value += char;
|
|
1013
|
-
|
|
1347
|
+
renderLegacyInput();
|
|
1014
1348
|
}
|
|
1015
1349
|
};
|
|
1016
|
-
|
|
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
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
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
|
-
|
|
1926
|
+
emitConsoleEntry("Modelo inválido. Use /model fast ou /model thinking.", "warning");
|
|
1352
1927
|
return;
|
|
1353
1928
|
}
|
|
1354
1929
|
if (nextMode === activeModel) {
|
|
1355
|
-
|
|
1930
|
+
emitConsoleEntry(`Modelo já está em ${getCliModelLabel(activeModel)}.`, "muted");
|
|
1356
1931
|
return;
|
|
1357
1932
|
}
|
|
1358
1933
|
activeModel = nextMode;
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
1365
|
-
renderStatusOverview(config, runtimeSession).forEach((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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2020
|
+
emitConsoleEntry("Pensando (OttoAI Thinking)", "headline");
|
|
2021
|
+
reasoningEntryId = ui
|
|
2022
|
+
? ui.pushEntry({ text: "", tone: "reasoning" })
|
|
2023
|
+
: null;
|
|
1424
2024
|
reasoningPrefixPrinted = true;
|
|
1425
2025
|
}
|
|
1426
|
-
|
|
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
|
-
|
|
2039
|
+
emitConsoleEntry("", "muted");
|
|
1435
2040
|
contentSeparatedFromReasoning = true;
|
|
1436
2041
|
}
|
|
1437
2042
|
if (!assistantPrefixPrinted) {
|
|
1438
|
-
|
|
2043
|
+
assistantEntryId = ui
|
|
2044
|
+
? ui.pushEntry({ text: "", tone: "assistant" })
|
|
2045
|
+
: null;
|
|
1439
2046
|
assistantPrefixPrinted = true;
|
|
1440
2047
|
}
|
|
1441
|
-
|
|
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
|
-
|
|
2068
|
+
emitConsoleEntry(bridgeSummary, "assistant");
|
|
1457
2069
|
}
|
|
1458
2070
|
if (jobId) {
|
|
1459
|
-
finalAssistantSummary = await
|
|
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
|
-
|
|
1473
|
-
|
|
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
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
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
|
+
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
package/scripts/postinstall.mjs
CHANGED
|
@@ -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.
|
|
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"], {
|