yaver-feedback-react-native 0.9.0 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -1
- package/app.plugin.js +126 -9
- package/dist/AuthOverlay.d.ts +1 -14
- package/dist/AuthOverlay.js +9 -62
- package/dist/BlackBox.js +9 -1
- package/dist/DeployPanel.js +65 -0
- package/dist/Discovery.js +13 -8
- package/dist/DogfoodRuntime.d.ts +124 -0
- package/dist/DogfoodRuntime.js +273 -0
- package/dist/FeedbackModal.js +505 -65
- package/dist/LoginScreen.d.ts +1 -5
- package/dist/LoginScreen.js +2 -6
- package/dist/MachinePickerScreen.d.ts +1 -3
- package/dist/MachinePickerScreen.js +6 -18
- package/dist/P2PClient.d.ts +178 -2
- package/dist/P2PClient.js +493 -3
- package/dist/P2PDogfoodDriver.d.ts +12 -0
- package/dist/P2PDogfoodDriver.js +118 -0
- package/dist/PairDeviceModal.d.ts +2 -3
- package/dist/ShakeDetector.js +2 -0
- package/dist/VibeChatScreen.d.ts +11 -1
- package/dist/VibeChatScreen.js +200 -41
- package/dist/YaverFeedback.d.ts +132 -0
- package/dist/YaverFeedback.js +633 -65
- package/dist/YaverModeBadge.d.ts +22 -0
- package/dist/YaverModeBadge.js +219 -0
- package/dist/__tests__/AuthDevices.test.js +1 -48
- package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
- package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
- package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
- package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
- package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
- package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
- package/dist/__tests__/DogfoodRuntime.test.js +116 -0
- package/dist/__tests__/P2PClient.test.js +2 -2
- package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
- package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
- package/dist/__tests__/ReportIdentity.test.d.ts +25 -0
- package/dist/__tests__/ReportIdentity.test.js +180 -0
- package/dist/__tests__/SDKToken.test.js +1 -1
- package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
- package/dist/__tests__/ShakeToggle.test.js +121 -0
- package/dist/__tests__/YaverFeedback.test.js +13 -3
- package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
- package/dist/__tests__/deviceDogfood.test.js +86 -0
- package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
- package/dist/__tests__/dogfoodPolicy.test.js +33 -0
- package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
- package/dist/__tests__/pickTargetDevice.test.js +89 -0
- package/dist/__tests__/reloadActions.test.d.ts +1 -0
- package/dist/__tests__/reloadActions.test.js +129 -0
- package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
- package/dist/__tests__/reloadActionsParity.test.js +42 -0
- package/dist/__tests__/types.test.js +5 -5
- package/dist/_core/ansi.d.ts +117 -0
- package/dist/_core/ansi.js +468 -0
- package/dist/_core/ansi.test.d.ts +1 -0
- package/dist/_core/ansi.test.js +225 -0
- package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
- package/dist/_core/buildFeedbackPrompt.js +18 -72
- package/dist/_core/constants.d.ts +19 -6
- package/dist/_core/constants.js +20 -7
- package/dist/_core/device.d.ts +1 -5
- package/dist/_core/device.js +5 -14
- package/dist/_core/endpoints.d.ts +0 -7
- package/dist/_core/endpoints.js +0 -7
- package/dist/_core/index.d.ts +4 -0
- package/dist/_core/index.js +4 -0
- package/dist/_core/remoteless.d.ts +44 -0
- package/dist/_core/remoteless.js +75 -0
- package/dist/_core/trace.d.ts +47 -0
- package/dist/_core/trace.js +38 -0
- package/dist/_core/trace.test.d.ts +1 -0
- package/dist/_core/trace.test.js +60 -0
- package/dist/auth.d.ts +3 -60
- package/dist/auth.js +6 -88
- package/dist/deviceDogfood.d.ts +53 -0
- package/dist/deviceDogfood.js +137 -0
- package/dist/dogfoodPolicy.d.ts +25 -0
- package/dist/dogfoodPolicy.js +24 -0
- package/dist/index.d.ts +17 -4
- package/dist/index.js +33 -7
- package/dist/reloadActions.d.ts +88 -0
- package/dist/reloadActions.js +200 -0
- package/dist/storeShots.d.ts +67 -0
- package/dist/storeShots.js +137 -0
- package/dist/types.d.ts +208 -8
- package/package.json +13 -4
- package/src/AuthOverlay.tsx +20 -106
- package/src/BlackBox.ts +10 -1
- package/src/DeployPanel.tsx +74 -0
- package/src/Discovery.ts +13 -8
- package/src/DogfoodRuntime.ts +373 -0
- package/src/FeedbackModal.tsx +619 -79
- package/src/LoginScreen.tsx +2 -22
- package/src/MachinePickerScreen.tsx +6 -21
- package/src/P2PClient.ts +580 -4
- package/src/P2PDogfoodDriver.ts +132 -0
- package/src/PairDeviceModal.tsx +2 -3
- package/src/ShakeDetector.ts +1 -0
- package/src/VibeChatScreen.tsx +232 -42
- package/src/YaverFeedback.ts +631 -68
- package/src/YaverModeBadge.tsx +234 -0
- package/src/__tests__/AuthDevices.test.ts +1 -52
- package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
- package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
- package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
- package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
- package/src/__tests__/DogfoodRuntime.test.ts +135 -0
- package/src/__tests__/P2PClient.test.ts +2 -2
- package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
- package/src/__tests__/ReportIdentity.test.ts +212 -0
- package/src/__tests__/SDKToken.test.ts +1 -1
- package/src/__tests__/ShakeToggle.test.ts +153 -0
- package/src/__tests__/YaverFeedback.test.ts +15 -3
- package/src/__tests__/deviceDogfood.test.ts +77 -0
- package/src/__tests__/dogfoodPolicy.test.ts +34 -0
- package/src/__tests__/pickTargetDevice.test.ts +101 -0
- package/src/__tests__/reloadActions.test.ts +171 -0
- package/src/__tests__/reloadActionsParity.test.ts +49 -0
- package/src/__tests__/types.test.ts +5 -5
- package/src/_core/ansi.test.ts +250 -0
- package/src/_core/ansi.ts +475 -0
- package/src/_core/buildFeedbackPrompt.ts +18 -95
- package/src/_core/constants.ts +20 -6
- package/src/_core/device.ts +5 -16
- package/src/_core/endpoints.ts +0 -7
- package/src/_core/index.ts +4 -0
- package/src/_core/remoteless.ts +110 -0
- package/src/_core/trace.test.ts +58 -0
- package/src/_core/trace.ts +75 -0
- package/src/auth.ts +7 -156
- package/src/deviceDogfood.ts +171 -0
- package/src/dogfoodPolicy.ts +40 -0
- package/src/index.ts +61 -11
- package/src/reloadActions.ts +273 -0
- package/src/storeShots.ts +189 -0
- package/src/types.ts +209 -8
- package/dist/GuestOnboardingScreen.d.ts +0 -8
- package/dist/GuestOnboardingScreen.js +0 -282
- package/src/GuestOnboardingScreen.tsx +0 -307
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ansi.ts — ANSI/terminal-stream tokenizer shared by every Yaver surface.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS (2026-08-09): the opencode runner's raw stdout is an ANSI
|
|
5
|
+
* VT stream (`\x1b[0m`, `$` prompts, `> build · <model>` banners, git diff
|
|
6
|
+
* +/- lines, 256-color + truecolor SGR codes, box-drawing TUIs). The
|
|
7
|
+
* dashboard and the mobile app both flattened it to plain text with
|
|
8
|
+
* stripAnsi — losing every colour the console has — while the xterm Terminal
|
|
9
|
+
* view kept the bytes. One shared tokenizer gives BOTH chat surfaces the
|
|
10
|
+
* console look from the SAME code path, so the two renderers can never drift
|
|
11
|
+
* (AGENTS.md: "one shared classifier, no copies"). The web and mobile
|
|
12
|
+
* renderers consume these tokens and paint them with their own primitives
|
|
13
|
+
* (spans / nested Text).
|
|
14
|
+
*
|
|
15
|
+
* This file is platform-neutral: no DOM, no RN. It lives in
|
|
16
|
+
* shared/client-core and is mirrored into mobile/src/_core via
|
|
17
|
+
* scripts/sync-client-core.sh (CI checks drift).
|
|
18
|
+
*
|
|
19
|
+
* Scope: the SGR family (\x1b[<params>m) plus the most common cursor/erase
|
|
20
|
+
* sequences (move, clear-line, clear-screen), which are dropped with a
|
|
21
|
+
* line-split marker so a TUI re-render reads as one block. OSC hyperlinks
|
|
22
|
+
* (\x1b]8;;url\x1b\\ ... \x1b]8;;\x1b\\) are extracted and carried on the
|
|
23
|
+
* token. Everything else is stripped.
|
|
24
|
+
*/
|
|
25
|
+
export interface AnsiToken {
|
|
26
|
+
/** Plain text content of this run (escape sequences removed). */
|
|
27
|
+
text: string;
|
|
28
|
+
/** Optional OSC-8 hyperlink URL attached to the run. */
|
|
29
|
+
href?: string;
|
|
30
|
+
fg?: AnsiColor;
|
|
31
|
+
bg?: AnsiColor;
|
|
32
|
+
bold?: boolean;
|
|
33
|
+
dim?: boolean;
|
|
34
|
+
italic?: boolean;
|
|
35
|
+
underline?: boolean;
|
|
36
|
+
strike?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface AnsiColor {
|
|
39
|
+
kind: "named" | "rgb" | "palette";
|
|
40
|
+
/** named: 0-15 (standard + bright). palette: 0-255 (xterm-256). */
|
|
41
|
+
index?: number;
|
|
42
|
+
/** rgb: [r,g,b]. */
|
|
43
|
+
rgb?: [number, number, number];
|
|
44
|
+
}
|
|
45
|
+
/** The 16 standard ANSI colours as RGB (bright variants at 8-15). */
|
|
46
|
+
export declare const ANSI_16_RGB: ReadonlyArray<readonly [number, number, number]>;
|
|
47
|
+
/** Map an xterm-256 palette index to RGB. */
|
|
48
|
+
export declare function paletteRgb(index: number): [number, number, number];
|
|
49
|
+
export interface TokenizeOptions {
|
|
50
|
+
/** Keep cursor/erase sequences as line breaks instead of dropping them
|
|
51
|
+
* silently (default true: a TUI repaint becomes a blank line). */
|
|
52
|
+
keepEraseLines?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Tokenize an ANSI/VT stream into styled runs. Pure, deterministic,
|
|
56
|
+
* dependency-free — unit-tested in ansi.test.ts.
|
|
57
|
+
*/
|
|
58
|
+
export declare function tokenizeAnsi(input: string, opts?: TokenizeOptions): AnsiToken[];
|
|
59
|
+
/** Convenience: flatten tokens to plain text (strip all styling). */
|
|
60
|
+
export declare function ansiToPlain(input: string): string;
|
|
61
|
+
/** Split a token stream into lines, preserving per-token styling. */
|
|
62
|
+
export interface AnsiLine {
|
|
63
|
+
tokens: AnsiToken[];
|
|
64
|
+
}
|
|
65
|
+
export declare function tokenStreamToLines(tokens: AnsiToken[]): AnsiLine[];
|
|
66
|
+
/**
|
|
67
|
+
* Structural console-line classification (opencode console look, 2026-08-09).
|
|
68
|
+
*
|
|
69
|
+
* The opencode runner's raw stream is full of shapes a plain-text render
|
|
70
|
+
* flattens: `> build · <model>` banner lines, `$ command` prompt lines, git
|
|
71
|
+
* patch output (`diff --git`, `+added`, `-removed`, `@@ hunk @@`), and
|
|
72
|
+
* tool/status prefixes. The terminal view renders these with xterm's own
|
|
73
|
+
* palette; the CHAT views on web + mobile used to strip every trace of them.
|
|
74
|
+
* One shared classifier lets both chat renderers paint the same console
|
|
75
|
+
* grammar from the same code path — a line is a hint, the renderer decides
|
|
76
|
+
* the exact colour, and the two surfaces can never drift.
|
|
77
|
+
*
|
|
78
|
+
* Pure, deterministic, unit-tested in ansi.test.ts.
|
|
79
|
+
*/
|
|
80
|
+
export type AnsiLineHint = "plain" | "banner" | "prompt" | "diff-header" | "diff-file" | "diff-hunk" | "diff-add" | "diff-del" | "tool-call";
|
|
81
|
+
export declare function classifyAnsiLine(plain: string): AnsiLineHint;
|
|
82
|
+
/**
|
|
83
|
+
* One-stop helper: tokenize a raw stream and return per-line hints aligned
|
|
84
|
+
* with the token lines, so a renderer can paint both colour runs and line
|
|
85
|
+
* grammar from a single pass.
|
|
86
|
+
*/
|
|
87
|
+
export interface AnsiStyledLine {
|
|
88
|
+
hint: AnsiLineHint;
|
|
89
|
+
plain: string;
|
|
90
|
+
tokens: AnsiToken[];
|
|
91
|
+
}
|
|
92
|
+
export declare function styleAnsiLines(input: string): AnsiStyledLine[];
|
|
93
|
+
/**
|
|
94
|
+
* Summarize a raw runner console for a small screen (phone / compact web
|
|
95
|
+
* pane). The full raw stream is megabytes of tool noise a human shouldn't
|
|
96
|
+
* have to read to find the answer; this is a DETERMINISTIC reducer — no LLM,
|
|
97
|
+
* no config:
|
|
98
|
+
*
|
|
99
|
+
* - `$ cmd` prompt echoes, runner-config banners (workdir/model/…), git
|
|
100
|
+
* diff hunks and punctuation-only TUI redraw edges are dropped.
|
|
101
|
+
* - 3+ consecutive identical lines collapse to one.
|
|
102
|
+
* - A line/byte budget bounds the render (a RUNNING task gets a tight
|
|
103
|
+
* budget — the user is watching the runner, not reading it; a finished
|
|
104
|
+
* task's tail gets a bigger one so the answer stays readable).
|
|
105
|
+
* - A trailing marker says how many lines were collapsed.
|
|
106
|
+
*
|
|
107
|
+
* Classification runs on ANSI-stripped text so escapes-prefixed lines still
|
|
108
|
+
* match; the KEPT lines keep their escapes intact for AnsiConsoleText to
|
|
109
|
+
* paint. Budgets default to mobile-sized; pass larger ones for web.
|
|
110
|
+
*/
|
|
111
|
+
export interface SummarizeConsoleOptions {
|
|
112
|
+
/** Max lines to keep. Default: 40 running / 200 finished. */
|
|
113
|
+
budgetLines?: number;
|
|
114
|
+
/** Max characters to keep. Default: 6 KiB running / 24 KiB finished. */
|
|
115
|
+
budgetChars?: number;
|
|
116
|
+
}
|
|
117
|
+
export declare function summarizeRawConsole(raw: string, running: boolean, opts?: SummarizeConsoleOptions): string;
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// AUTO-SYNCED from shared/client-core/src/ansi.ts.
|
|
3
|
+
// DO NOT EDIT IN PLACE. Edit the source and re-run
|
|
4
|
+
// scripts/sync-client-core.sh. CI checks drift via `--check`.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ANSI_16_RGB = void 0;
|
|
7
|
+
exports.paletteRgb = paletteRgb;
|
|
8
|
+
exports.tokenizeAnsi = tokenizeAnsi;
|
|
9
|
+
exports.ansiToPlain = ansiToPlain;
|
|
10
|
+
exports.tokenStreamToLines = tokenStreamToLines;
|
|
11
|
+
exports.classifyAnsiLine = classifyAnsiLine;
|
|
12
|
+
exports.styleAnsiLines = styleAnsiLines;
|
|
13
|
+
exports.summarizeRawConsole = summarizeRawConsole;
|
|
14
|
+
/** The 16 standard ANSI colours as RGB (bright variants at 8-15). */
|
|
15
|
+
exports.ANSI_16_RGB = [
|
|
16
|
+
[0, 0, 0], // 0 black
|
|
17
|
+
[194, 54, 33], // 1 red (opencode-ish)
|
|
18
|
+
[94, 164, 44], // 2 green
|
|
19
|
+
[163, 134, 52], // 3 yellow/orange
|
|
20
|
+
[0, 94, 168], // 4 blue
|
|
21
|
+
[101, 88, 186], // 5 magenta
|
|
22
|
+
[44, 142, 158], // 6 cyan
|
|
23
|
+
[190, 190, 190], // 7 white
|
|
24
|
+
[128, 128, 128], // 8 bright black
|
|
25
|
+
[255, 113, 89], // 9 bright red
|
|
26
|
+
[134, 214, 92], // 10 bright green
|
|
27
|
+
[229, 201, 84], // 11 bright yellow
|
|
28
|
+
[88, 156, 245], // 12 bright blue
|
|
29
|
+
[184, 146, 255], // 13 bright magenta
|
|
30
|
+
[112, 213, 226], // 14 bright cyan
|
|
31
|
+
[240, 240, 240], // 15 bright white
|
|
32
|
+
];
|
|
33
|
+
/** The xterm-256 colour cube / greys (index 16-255). */
|
|
34
|
+
const PALETTE_RGB = (() => {
|
|
35
|
+
const out = [];
|
|
36
|
+
// 16-231: 6x6x6 colour cube.
|
|
37
|
+
for (let i = 0; i < 216; i++) {
|
|
38
|
+
const r = Math.round((i / 36) % 6) * 51;
|
|
39
|
+
const g = Math.round((i / 6) % 6) * 51;
|
|
40
|
+
const b = (i % 6) * 51;
|
|
41
|
+
out.push([r, g, b]);
|
|
42
|
+
}
|
|
43
|
+
// 232-255: 24-step grey ramp.
|
|
44
|
+
for (let i = 0; i < 24; i++) {
|
|
45
|
+
const v = 8 + i * 10;
|
|
46
|
+
out.push([v, v, v]);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
})();
|
|
50
|
+
/** Map an xterm-256 palette index to RGB. */
|
|
51
|
+
function paletteRgb(index) {
|
|
52
|
+
if (index >= 0 && index < 16)
|
|
53
|
+
return [...exports.ANSI_16_RGB[index]];
|
|
54
|
+
if (index >= 16 && index < 256)
|
|
55
|
+
return PALETTE_RGB[index - 16];
|
|
56
|
+
return [190, 190, 190];
|
|
57
|
+
}
|
|
58
|
+
/** Truecolor/rgb approximation of a 256-colour index (used when a surface
|
|
59
|
+
* can't map the palette exactly). */
|
|
60
|
+
const OSC_LINK_OPEN = /\x1b\]8;;([^\x07]*)\x07/;
|
|
61
|
+
/** Schemes an OSC-8 hyperlink may carry. Anything else — `javascript:`,
|
|
62
|
+
* `data:`, `vbscript:`, a bare protocol-relative `//`, or a scheme-less
|
|
63
|
+
* string that React would still accept as a URL — is dropped so a
|
|
64
|
+
* prompt-injected link can never execute in the dashboard origin. This is
|
|
65
|
+
* the web-side half of the audit finding "OSC-8 `javascript:` href XSS"
|
|
66
|
+
* (docs/audits/webui-chat-vibing-gui-2026-08-12.md §6.1): the tokenizer is
|
|
67
|
+
* the single choke point every renderer (AnsiConsoleText, terminal views)
|
|
68
|
+
* shares, so sanitising here protects all of them.
|
|
69
|
+
*/
|
|
70
|
+
// http(s):// and mailto: (mailto has no "//"). Everything else fails.
|
|
71
|
+
const OSC_LINK_SAFE = /^(https?:\/\/|mailto:)/i;
|
|
72
|
+
/** Returns the href to carry on the token, or "" when the URL is unsafe. */
|
|
73
|
+
function sanitizeOscLink(url) {
|
|
74
|
+
const trimmed = (url || "").trim();
|
|
75
|
+
if (!trimmed)
|
|
76
|
+
return "";
|
|
77
|
+
// `javascript:alert(1)` fails the allowlist; `//evil.com` is
|
|
78
|
+
// protocol-relative and must not become a same-origin jump; a bare
|
|
79
|
+
// `alert(1)` without a scheme is not a URL at all. Only explicit
|
|
80
|
+
// http(s):// or mailto: links pass.
|
|
81
|
+
if (!OSC_LINK_SAFE.test(trimmed))
|
|
82
|
+
return "";
|
|
83
|
+
return trimmed;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Tokenize an ANSI/VT stream into styled runs. Pure, deterministic,
|
|
87
|
+
* dependency-free — unit-tested in ansi.test.ts.
|
|
88
|
+
*/
|
|
89
|
+
function tokenizeAnsi(input, opts) {
|
|
90
|
+
if (!input)
|
|
91
|
+
return [];
|
|
92
|
+
const keepEraseLines = opts?.keepEraseLines ?? true;
|
|
93
|
+
const tokens = [];
|
|
94
|
+
let fg;
|
|
95
|
+
let bg;
|
|
96
|
+
let bold = false;
|
|
97
|
+
let dim = false;
|
|
98
|
+
let italic = false;
|
|
99
|
+
let underline = false;
|
|
100
|
+
let strike = false;
|
|
101
|
+
let buf = "";
|
|
102
|
+
// Current open OSC-8 link (the ESC\ BEL...ST form), if any.
|
|
103
|
+
let linkUrl;
|
|
104
|
+
const pushBuf = (href) => {
|
|
105
|
+
if (!buf)
|
|
106
|
+
return;
|
|
107
|
+
tokens.push({
|
|
108
|
+
text: buf,
|
|
109
|
+
...(href || linkUrl ? { href: href || linkUrl } : {}),
|
|
110
|
+
...(fg ? { fg } : {}),
|
|
111
|
+
...(bg ? { bg } : {}),
|
|
112
|
+
...(bold ? { bold: true } : {}),
|
|
113
|
+
...(dim ? { dim: true } : {}),
|
|
114
|
+
...(italic ? { italic: true } : {}),
|
|
115
|
+
...(underline ? { underline: true } : {}),
|
|
116
|
+
...(strike ? { strike: true } : {}),
|
|
117
|
+
});
|
|
118
|
+
buf = "";
|
|
119
|
+
};
|
|
120
|
+
const applySgr = (params) => {
|
|
121
|
+
if (!params) { // bare \x1b[m === reset
|
|
122
|
+
fg = undefined;
|
|
123
|
+
bg = undefined;
|
|
124
|
+
bold = false;
|
|
125
|
+
dim = false;
|
|
126
|
+
italic = false;
|
|
127
|
+
underline = false;
|
|
128
|
+
strike = false;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const parts = params.split(";");
|
|
132
|
+
let i = 0;
|
|
133
|
+
while (i < parts.length) {
|
|
134
|
+
const code = parts[i];
|
|
135
|
+
if (code === "") {
|
|
136
|
+
fg = undefined;
|
|
137
|
+
bg = undefined;
|
|
138
|
+
bold = false;
|
|
139
|
+
dim = false;
|
|
140
|
+
italic = false;
|
|
141
|
+
underline = false;
|
|
142
|
+
strike = false;
|
|
143
|
+
i++;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const n = parseInt(code, 10);
|
|
147
|
+
if (isNaN(n)) {
|
|
148
|
+
i++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
switch (n) {
|
|
152
|
+
case 0:
|
|
153
|
+
fg = undefined;
|
|
154
|
+
bg = undefined;
|
|
155
|
+
bold = false;
|
|
156
|
+
dim = false;
|
|
157
|
+
italic = false;
|
|
158
|
+
underline = false;
|
|
159
|
+
strike = false;
|
|
160
|
+
break;
|
|
161
|
+
case 1:
|
|
162
|
+
bold = true;
|
|
163
|
+
break;
|
|
164
|
+
case 2:
|
|
165
|
+
dim = true;
|
|
166
|
+
break;
|
|
167
|
+
case 3:
|
|
168
|
+
italic = true;
|
|
169
|
+
break;
|
|
170
|
+
case 4:
|
|
171
|
+
underline = true;
|
|
172
|
+
break;
|
|
173
|
+
case 7: /* reverse video — swap fg/bg */ {
|
|
174
|
+
const t = fg;
|
|
175
|
+
fg = bg;
|
|
176
|
+
bg = t;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 9:
|
|
180
|
+
strike = true;
|
|
181
|
+
break;
|
|
182
|
+
case 22:
|
|
183
|
+
bold = false;
|
|
184
|
+
dim = false;
|
|
185
|
+
break;
|
|
186
|
+
case 23:
|
|
187
|
+
italic = false;
|
|
188
|
+
break;
|
|
189
|
+
case 24:
|
|
190
|
+
underline = false;
|
|
191
|
+
break;
|
|
192
|
+
case 29:
|
|
193
|
+
strike = false;
|
|
194
|
+
break;
|
|
195
|
+
case 30:
|
|
196
|
+
case 31:
|
|
197
|
+
case 32:
|
|
198
|
+
case 33:
|
|
199
|
+
case 34:
|
|
200
|
+
case 35:
|
|
201
|
+
case 36:
|
|
202
|
+
case 37:
|
|
203
|
+
fg = { kind: "named", index: n - 30 };
|
|
204
|
+
break;
|
|
205
|
+
case 38: {
|
|
206
|
+
const t = parseExtendedColor(parts, i + 1);
|
|
207
|
+
if (t) {
|
|
208
|
+
fg = t.color;
|
|
209
|
+
i = t.next;
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 39:
|
|
214
|
+
fg = undefined;
|
|
215
|
+
break;
|
|
216
|
+
case 40:
|
|
217
|
+
case 41:
|
|
218
|
+
case 42:
|
|
219
|
+
case 43:
|
|
220
|
+
case 44:
|
|
221
|
+
case 45:
|
|
222
|
+
case 46:
|
|
223
|
+
case 47:
|
|
224
|
+
bg = { kind: "named", index: n - 40 };
|
|
225
|
+
break;
|
|
226
|
+
case 48: {
|
|
227
|
+
const t = parseExtendedColor(parts, i + 1);
|
|
228
|
+
if (t) {
|
|
229
|
+
bg = t.color;
|
|
230
|
+
i = t.next;
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case 49:
|
|
235
|
+
bg = undefined;
|
|
236
|
+
break;
|
|
237
|
+
case 90:
|
|
238
|
+
case 91:
|
|
239
|
+
case 92:
|
|
240
|
+
case 93:
|
|
241
|
+
case 94:
|
|
242
|
+
case 95:
|
|
243
|
+
case 96:
|
|
244
|
+
case 97:
|
|
245
|
+
fg = { kind: "named", index: n - 90 + 8 };
|
|
246
|
+
break;
|
|
247
|
+
case 100:
|
|
248
|
+
case 101:
|
|
249
|
+
case 102:
|
|
250
|
+
case 103:
|
|
251
|
+
case 104:
|
|
252
|
+
case 105:
|
|
253
|
+
case 106:
|
|
254
|
+
case 107:
|
|
255
|
+
bg = { kind: "named", index: n - 100 + 8 };
|
|
256
|
+
break;
|
|
257
|
+
default: /* unknown — ignore */ break;
|
|
258
|
+
}
|
|
259
|
+
i++;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
/** Parse 38/48 extended colours: `;5;<idx>` (256) or `;2;r;g;b` (truecolor). */
|
|
263
|
+
const parseExtendedColor = (parts, from) => {
|
|
264
|
+
const mode = parts[from];
|
|
265
|
+
if (mode === "5") {
|
|
266
|
+
const idx = parseInt(parts[from + 1] ?? "", 10);
|
|
267
|
+
if (!isNaN(idx))
|
|
268
|
+
return { color: { kind: "palette", index: idx }, next: from + 1 };
|
|
269
|
+
}
|
|
270
|
+
else if (mode === "2") {
|
|
271
|
+
const r = parseInt(parts[from + 1] ?? "", 10);
|
|
272
|
+
const g = parseInt(parts[from + 2] ?? "", 10);
|
|
273
|
+
const b = parseInt(parts[from + 3] ?? "", 10);
|
|
274
|
+
if (!isNaN(r) && !isNaN(g) && !isNaN(b))
|
|
275
|
+
return { color: { kind: "rgb", rgb: [r, g, b] }, next: from + 3 };
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
};
|
|
279
|
+
// Master scan: match SGR/control escapes OR OSC-8 hyperlink shells in one
|
|
280
|
+
// pass so link offsets can never drift from the text they wrap.
|
|
281
|
+
const MASTER = /(\x1b\]8;;[^\x07]*\x07[\s\S]*?\x1b\]8;;\x07)|(\x1b\[[0-9;:]*[A-Za-z])/g;
|
|
282
|
+
MASTER.lastIndex = 0;
|
|
283
|
+
let m;
|
|
284
|
+
let prevEnd = 0;
|
|
285
|
+
while ((m = MASTER.exec(input)) !== null) {
|
|
286
|
+
const osc = m[1];
|
|
287
|
+
const esc = m[2];
|
|
288
|
+
const start = m.index;
|
|
289
|
+
// Accumulate the plain text since the previous escape into buf.
|
|
290
|
+
if (start > prevEnd) {
|
|
291
|
+
buf += input.slice(prevEnd, start);
|
|
292
|
+
}
|
|
293
|
+
if (osc) {
|
|
294
|
+
// OSC-8 hyperlink shell: \x1b]8;;<url>\x07<text>\x1b]8;;\x07
|
|
295
|
+
const open = osc.match(OSC_LINK_OPEN);
|
|
296
|
+
const url = sanitizeOscLink(open ? open[1] : "");
|
|
297
|
+
// Strip the shell, keep only the inner text.
|
|
298
|
+
const inner = osc.replace(/\x1b\]8;;[^\x07]*\x07/, "").replace(/\x1b\]8;;\x07$/, "");
|
|
299
|
+
buf += inner;
|
|
300
|
+
pushBuf(url || undefined);
|
|
301
|
+
linkUrl = undefined;
|
|
302
|
+
}
|
|
303
|
+
else if (esc) {
|
|
304
|
+
const params = esc.slice(2, -1);
|
|
305
|
+
const cmd = esc.slice(-1);
|
|
306
|
+
if (cmd === "m") {
|
|
307
|
+
pushBuf();
|
|
308
|
+
applySgr(params);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// Non-SGR control (cursor move, erase, etc.). Keep text before it,
|
|
312
|
+
// then treat the sequence as a boundary.
|
|
313
|
+
pushBuf();
|
|
314
|
+
if (keepEraseLines && (cmd === "K" || cmd === "J" || cmd === "H" || cmd === "A")) {
|
|
315
|
+
tokens.push({ text: "\n" });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
prevEnd = MASTER.lastIndex;
|
|
320
|
+
}
|
|
321
|
+
if (prevEnd < input.length) {
|
|
322
|
+
buf += input.slice(prevEnd);
|
|
323
|
+
}
|
|
324
|
+
pushBuf();
|
|
325
|
+
// Merge adjacent tokens with identical style so a stream that toggles
|
|
326
|
+
// `\x1b[0m` between runs (the opencode banner does this constantly) stays
|
|
327
|
+
// readable as contiguous text instead of fragmenting into per-escape runs.
|
|
328
|
+
const merged = [];
|
|
329
|
+
for (const t of tokens) {
|
|
330
|
+
const last = merged[merged.length - 1];
|
|
331
|
+
if (last &&
|
|
332
|
+
last.href === t.href &&
|
|
333
|
+
last.bold === t.bold &&
|
|
334
|
+
last.dim === t.dim &&
|
|
335
|
+
last.italic === t.italic &&
|
|
336
|
+
last.underline === t.underline &&
|
|
337
|
+
last.strike === t.strike &&
|
|
338
|
+
JSON.stringify(last.fg) === JSON.stringify(t.fg) &&
|
|
339
|
+
JSON.stringify(last.bg) === JSON.stringify(t.bg)) {
|
|
340
|
+
last.text += t.text;
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
merged.push({ ...t });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return merged;
|
|
347
|
+
}
|
|
348
|
+
/** Convenience: flatten tokens to plain text (strip all styling). */
|
|
349
|
+
function ansiToPlain(input) {
|
|
350
|
+
return tokenizeAnsi(input)
|
|
351
|
+
.map((t) => t.text)
|
|
352
|
+
.join("");
|
|
353
|
+
}
|
|
354
|
+
function tokenStreamToLines(tokens) {
|
|
355
|
+
const lines = [];
|
|
356
|
+
let current = [];
|
|
357
|
+
for (const t of tokens) {
|
|
358
|
+
const parts = t.text.split("\n");
|
|
359
|
+
parts.forEach((seg, idx) => {
|
|
360
|
+
if (idx > 0) {
|
|
361
|
+
lines.push({ tokens: current });
|
|
362
|
+
current = [];
|
|
363
|
+
}
|
|
364
|
+
if (seg.length > 0) {
|
|
365
|
+
current.push({ ...t, text: seg });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (current.length > 0)
|
|
370
|
+
lines.push({ tokens: current });
|
|
371
|
+
return lines;
|
|
372
|
+
}
|
|
373
|
+
const BANNER_RE = /^\s*>?\s*(build|plan|review|debug)\s*[·•|:]\s*\S+/;
|
|
374
|
+
const PROMPT_RE = /^\s*\$[ \t]/;
|
|
375
|
+
const DIFF_HEADER_RE = /^diff --git \S+ \S+$/;
|
|
376
|
+
const DIFF_FILE_RE = /^(\+\+\+|---) [ab]\/\S+/;
|
|
377
|
+
const DIFF_HUNK_RE = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
|
|
378
|
+
const TOOL_TAIL_RE = /^\s*(⎿|└|├)\s*/;
|
|
379
|
+
const STATUS_RE = /^\s*[✓✗]\s+\S/;
|
|
380
|
+
function classifyAnsiLine(plain) {
|
|
381
|
+
const s = plain.replace(/\s+$/g, "");
|
|
382
|
+
if (!s)
|
|
383
|
+
return "plain";
|
|
384
|
+
if (BANNER_RE.test(s))
|
|
385
|
+
return "banner";
|
|
386
|
+
if (PROMPT_RE.test(s))
|
|
387
|
+
return "prompt";
|
|
388
|
+
if (DIFF_HEADER_RE.test(s))
|
|
389
|
+
return "diff-header";
|
|
390
|
+
if (DIFF_FILE_RE.test(s))
|
|
391
|
+
return "diff-file";
|
|
392
|
+
if (DIFF_HUNK_RE.test(s))
|
|
393
|
+
return "diff-hunk";
|
|
394
|
+
if (s.startsWith("+") && !s.startsWith("+++"))
|
|
395
|
+
return "diff-add";
|
|
396
|
+
if (s.startsWith("-") && !s.startsWith("---"))
|
|
397
|
+
return "diff-del";
|
|
398
|
+
if (TOOL_TAIL_RE.test(s) || STATUS_RE.test(s))
|
|
399
|
+
return "tool-call";
|
|
400
|
+
return "plain";
|
|
401
|
+
}
|
|
402
|
+
function styleAnsiLines(input) {
|
|
403
|
+
const lines = tokenStreamToLines(tokenizeAnsi(input));
|
|
404
|
+
return lines.map((l) => {
|
|
405
|
+
const plain = l.tokens.map((t) => t.text).join("");
|
|
406
|
+
return { hint: classifyAnsiLine(plain), plain, tokens: l.tokens };
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
const CONSOLE_NOISE_RE = [
|
|
410
|
+
/^\$\s+/, // `$ cmd` echo
|
|
411
|
+
/^(workdir|model|provider|approval|sandbox|reasoning effort|session id|project path|project):/i, // runner config banners
|
|
412
|
+
/^(diff --git|index [0-9a-f]+\.\.[0-9a-f]+|@@ |--- |\+\+\+ )/, // diff hunks
|
|
413
|
+
/^[{}[\];(),.=><:+\-/*\\|'"`_~]+$/, // TUI redraw edges
|
|
414
|
+
];
|
|
415
|
+
function summarizeRawConsole(raw, running, opts) {
|
|
416
|
+
if (!raw)
|
|
417
|
+
return "";
|
|
418
|
+
const budgetLines = opts?.budgetLines ?? (running ? 40 : 200);
|
|
419
|
+
const budgetChars = opts?.budgetChars ?? (running ? 6 * 1024 : 24 * 1024);
|
|
420
|
+
const lines = raw.split("\n");
|
|
421
|
+
// KEEP THE TAIL, NOT THE HEAD (2026-08-13, web chat live-console bug):
|
|
422
|
+
// the old loop dropped NEW lines once the budget was reached, so a
|
|
423
|
+
// running task past ~150 lines froze its console at the START of the
|
|
424
|
+
// run — the header said "N LINES · LIVE" while every line after the
|
|
425
|
+
// budget never rendered, and auto-scroll had nothing to follow. A live
|
|
426
|
+
// console shows the newest output; evict the OLDEST kept line instead
|
|
427
|
+
// of rejecting the newest.
|
|
428
|
+
const kept = [];
|
|
429
|
+
let keptChars = 0;
|
|
430
|
+
let dropped = 0;
|
|
431
|
+
let prevKey = "";
|
|
432
|
+
let runLen = 0;
|
|
433
|
+
for (const rawLine of lines) {
|
|
434
|
+
const plain = ansiToPlain(rawLine).trim();
|
|
435
|
+
if (!plain)
|
|
436
|
+
continue;
|
|
437
|
+
if (CONSOLE_NOISE_RE.some((re) => re.test(plain))) {
|
|
438
|
+
dropped += 1;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (plain === prevKey) {
|
|
442
|
+
runLen += 1;
|
|
443
|
+
if (runLen > 2) {
|
|
444
|
+
dropped += 1;
|
|
445
|
+
continue;
|
|
446
|
+
} // 3+ repeats → one
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
prevKey = plain;
|
|
450
|
+
runLen = 1;
|
|
451
|
+
}
|
|
452
|
+
if (kept.length >= budgetLines || keptChars >= budgetChars) {
|
|
453
|
+
// Budget full: make room for the newest line by evicting the oldest
|
|
454
|
+
// kept line (tracking its char weight so the char budget stays
|
|
455
|
+
// accurate). This is what makes a streaming console follow the tail.
|
|
456
|
+
dropped += 1;
|
|
457
|
+
const evicted = kept.shift();
|
|
458
|
+
if (evicted)
|
|
459
|
+
keptChars -= evicted.length + 1;
|
|
460
|
+
}
|
|
461
|
+
kept.push(rawLine);
|
|
462
|
+
keptChars += rawLine.length + 1;
|
|
463
|
+
}
|
|
464
|
+
let out = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
465
|
+
if (dropped > 0)
|
|
466
|
+
out = `${out}\n… ${dropped} noisy lines collapsed`;
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|