yaver-feedback-react-native 0.9.2 → 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/dist/AuthOverlay.d.ts +1 -14
- package/dist/AuthOverlay.js +9 -62
- package/dist/Discovery.js +0 -6
- package/dist/DogfoodRuntime.d.ts +124 -0
- package/dist/DogfoodRuntime.js +273 -0
- package/dist/FeedbackModal.js +347 -61
- 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 +116 -3
- package/dist/P2PClient.js +273 -3
- package/dist/P2PDogfoodDriver.d.ts +12 -0
- package/dist/P2PDogfoodDriver.js +118 -0
- package/dist/PairDeviceModal.d.ts +2 -3
- package/dist/VibeChatScreen.d.ts +11 -1
- package/dist/VibeChatScreen.js +200 -41
- package/dist/YaverFeedback.d.ts +34 -0
- package/dist/YaverFeedback.js +124 -19
- package/dist/YaverModeBadge.d.ts +22 -0
- package/dist/YaverModeBadge.js +219 -0
- package/dist/__tests__/AuthDevices.test.js +1 -48
- package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
- package/dist/__tests__/DogfoodRuntime.test.js +116 -0
- 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 -1
- package/dist/__tests__/ReportIdentity.test.js +34 -22
- 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/_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 +9 -23
- package/dist/_core/device.js +13 -28
- 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 +13 -4
- package/dist/index.js +24 -8
- package/dist/reloadActions.js +2 -2
- package/dist/types.d.ts +50 -7
- package/package.json +12 -3
- package/src/AuthOverlay.tsx +20 -106
- package/src/Discovery.ts +0 -6
- package/src/DogfoodRuntime.ts +373 -0
- package/src/FeedbackModal.tsx +445 -67
- package/src/LoginScreen.tsx +2 -22
- package/src/MachinePickerScreen.tsx +6 -21
- package/src/P2PClient.ts +347 -4
- package/src/P2PDogfoodDriver.ts +132 -0
- package/src/PairDeviceModal.tsx +2 -3
- package/src/VibeChatScreen.tsx +232 -42
- package/src/YaverFeedback.ts +133 -22
- package/src/YaverModeBadge.tsx +234 -0
- package/src/__tests__/AuthDevices.test.ts +1 -52
- package/src/__tests__/DogfoodRuntime.test.ts +135 -0
- package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
- package/src/__tests__/ReportIdentity.test.ts +36 -27
- 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/_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 +13 -30
- 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 +40 -11
- package/src/reloadActions.ts +2 -2
- package/src/types.ts +45 -7
- package/dist/GuestOnboardingScreen.d.ts +0 -8
- package/dist/GuestOnboardingScreen.js +0 -282
- package/src/GuestOnboardingScreen.tsx +0 -307
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const DogfoodRuntime_1 = require("../DogfoodRuntime");
|
|
4
|
+
const expo = {
|
|
5
|
+
name: 'Example', workDir: '/workspace/example', framework: 'expo', lane: 'browser',
|
|
6
|
+
};
|
|
7
|
+
describe('DogfoodController', () => {
|
|
8
|
+
test('does no work before the explicit trigger', () => {
|
|
9
|
+
const driver = { start: jest.fn(async () => ({ lane: 'browser' })) };
|
|
10
|
+
const controller = new DogfoodRuntime_1.DogfoodController(expo, driver);
|
|
11
|
+
expect(driver.start).not.toHaveBeenCalled();
|
|
12
|
+
expect(controller.snapshot().phase).toBe('idle');
|
|
13
|
+
});
|
|
14
|
+
test('preserves raw npm output and hands a live session to the host', async () => {
|
|
15
|
+
const stop = jest.fn();
|
|
16
|
+
const closeLogs = jest.fn();
|
|
17
|
+
const controller = new DogfoodRuntime_1.DogfoodController(expo, {
|
|
18
|
+
async start(ctx) {
|
|
19
|
+
ctx.registerCleanup(stop, 'session');
|
|
20
|
+
ctx.registerCleanup(closeLogs, 'transient');
|
|
21
|
+
ctx.log('$ npm install --legacy-peer-deps');
|
|
22
|
+
ctx.log('npm warn deprecated example@1.0.0');
|
|
23
|
+
return { lane: 'browser', sessionId: 's1', url: 'http://agent/dev/' };
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
await controller.trigger();
|
|
27
|
+
expect(controller.snapshot().logs.map((line) => line.text)).toEqual([
|
|
28
|
+
'$ npm install --legacy-peer-deps',
|
|
29
|
+
'npm warn deprecated example@1.0.0',
|
|
30
|
+
]);
|
|
31
|
+
expect(await controller.handoff()).toMatchObject({ sessionId: 's1' });
|
|
32
|
+
expect(closeLogs).toHaveBeenCalledTimes(1);
|
|
33
|
+
expect(stop).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
test('cleans a partial session before exposing a structured failure', async () => {
|
|
36
|
+
const stop = jest.fn();
|
|
37
|
+
const controller = new DogfoodRuntime_1.DogfoodController(expo, {
|
|
38
|
+
async start(ctx) {
|
|
39
|
+
ctx.registerCleanup(stop);
|
|
40
|
+
throw new DogfoodRuntime_1.DogfoodRuntimeError({
|
|
41
|
+
code: 'DOGFOOD_RENDER_FAILED', error: 'Metro exited', remedy: 'Fix Metro and retry.', retryable: true,
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
await expect(controller.trigger()).rejects.toThrow('Metro exited');
|
|
46
|
+
expect(stop).toHaveBeenCalledTimes(1);
|
|
47
|
+
expect(controller.snapshot()).toMatchObject({ phase: 'failed', failure: { code: 'DOGFOOD_RENDER_FAILED' } });
|
|
48
|
+
});
|
|
49
|
+
test('an obsolete attempt cannot clean up the replacement attempt', async () => {
|
|
50
|
+
let releaseFirst;
|
|
51
|
+
const firstGate = new Promise((resolve) => { releaseFirst = resolve; });
|
|
52
|
+
let firstStarted;
|
|
53
|
+
const firstStartedGate = new Promise((resolve) => { firstStarted = resolve; });
|
|
54
|
+
const stopFirst = jest.fn();
|
|
55
|
+
const stopSecond = jest.fn();
|
|
56
|
+
let starts = 0;
|
|
57
|
+
const controller = new DogfoodRuntime_1.DogfoodController(expo, {
|
|
58
|
+
async start(ctx) {
|
|
59
|
+
starts += 1;
|
|
60
|
+
if (starts === 1) {
|
|
61
|
+
ctx.registerCleanup(stopFirst);
|
|
62
|
+
firstStarted();
|
|
63
|
+
await firstGate;
|
|
64
|
+
return { lane: 'browser', sessionId: 'old' };
|
|
65
|
+
}
|
|
66
|
+
ctx.registerCleanup(stopSecond);
|
|
67
|
+
return { lane: 'browser', sessionId: 'new' };
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const old = controller.trigger();
|
|
71
|
+
await firstStartedGate;
|
|
72
|
+
await controller.stop();
|
|
73
|
+
await expect(controller.trigger()).resolves.toMatchObject({ sessionId: 'new' });
|
|
74
|
+
releaseFirst();
|
|
75
|
+
await expect(old).rejects.toMatchObject({ failure: { code: 'DOGFOOD_ATTEMPT_REPLACED' } });
|
|
76
|
+
expect(stopFirst).toHaveBeenCalledTimes(1);
|
|
77
|
+
expect(stopSecond).not.toHaveBeenCalled();
|
|
78
|
+
await controller.stop();
|
|
79
|
+
expect(stopSecond).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('Dogfood lanes and console events', () => {
|
|
83
|
+
test('Flutter is first-class on browser but cannot be mislabeled Hermes', () => {
|
|
84
|
+
expect((0, DogfoodRuntime_1.validateDogfoodProject)({ ...expo, framework: 'flutter', lane: 'browser' })).toBeNull();
|
|
85
|
+
expect((0, DogfoodRuntime_1.validateDogfoodProject)({ ...expo, framework: 'flutter', lane: 'hermes' })?.code)
|
|
86
|
+
.toBe('DOGFOOD_HERMES_FRAMEWORK_UNSUPPORTED');
|
|
87
|
+
});
|
|
88
|
+
test('uses one three-lane matrix with browser default for React Native and Flutter', () => {
|
|
89
|
+
const rn = (0, DogfoodRuntime_1.dogfoodLaneOptions)('expo', { nativeRuntimeAvailable: true });
|
|
90
|
+
expect((0, DogfoodRuntime_1.defaultDogfoodLane)('expo')).toBe('browser');
|
|
91
|
+
expect(rn.map((option) => [option.lane, option.supported])).toEqual([
|
|
92
|
+
['browser', true], ['hermes', true], ['webrtc', true],
|
|
93
|
+
]);
|
|
94
|
+
const flutter = (0, DogfoodRuntime_1.dogfoodLaneOptions)('flutter', { nativeRuntimeAvailable: true });
|
|
95
|
+
expect((0, DogfoodRuntime_1.defaultDogfoodLane)('flutter')).toBe('browser');
|
|
96
|
+
expect(flutter.find((option) => option.lane === 'hermes')?.supported).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
test('keeps Yaver self-development on the same RN three-lane contract', () => {
|
|
99
|
+
const options = (0, DogfoodRuntime_1.dogfoodLaneOptions)('expo', { nativeRuntimeAvailable: true, selfDevelopment: true });
|
|
100
|
+
expect(options).toHaveLength(3);
|
|
101
|
+
expect(options.find((option) => option.lane === 'hermes')).toMatchObject({ supported: true });
|
|
102
|
+
expect(options.find((option) => option.lane === 'webrtc')).toMatchObject({ supported: true });
|
|
103
|
+
});
|
|
104
|
+
test('native-only projects use WebRTC rather than fake browser or Hermes lanes', () => {
|
|
105
|
+
const swift = (0, DogfoodRuntime_1.dogfoodLaneOptions)('swift', { nativeRuntimeAvailable: true });
|
|
106
|
+
expect(swift.map((option) => [option.lane, option.supported])).toEqual([
|
|
107
|
+
['browser', false], ['hermes', false], ['webrtc', true],
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
test('reads raw and replayed package-manager logs from /dev/events', () => {
|
|
111
|
+
expect((0, DogfoodRuntime_1.dogfoodLogLinesFromDevEvent)({ type: 'log', logLine: '$ npm ci\u001b[0m' }))
|
|
112
|
+
.toEqual(['$ npm ci\u001b[0m']);
|
|
113
|
+
expect((0, DogfoodRuntime_1.dogfoodLogLinesFromDevEvent)({ type: 'snapshot', snapshot: { recentLogs: ['npm warn x', 'Metro ready'] } }))
|
|
114
|
+
.toEqual(['npm warn x', 'Metro ready']);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const DogfoodRuntime_1 = require("../DogfoodRuntime");
|
|
4
|
+
const P2PDogfoodDriver_1 = require("../P2PDogfoodDriver");
|
|
5
|
+
describe('createP2PDogfoodDriver', () => {
|
|
6
|
+
it('keeps the browser lane compiling until the ordinary Projects status reports a URL', async () => {
|
|
7
|
+
const stop = jest.fn(async () => { });
|
|
8
|
+
const status = jest.fn()
|
|
9
|
+
.mockResolvedValueOnce({ building: true, framework: 'flutter' })
|
|
10
|
+
.mockResolvedValueOnce({ running: true, serving: true, framework: 'flutter', bundleUrl: '/dev/' });
|
|
11
|
+
const client = {
|
|
12
|
+
subscribeDogfoodDevEvents: (onEvent) => {
|
|
13
|
+
onEvent({ type: 'log', logLine: '$ flutter run -d web-server' });
|
|
14
|
+
return jest.fn();
|
|
15
|
+
},
|
|
16
|
+
startDogfoodDevServer: jest.fn(async () => ({ starting: true, framework: 'flutter' })),
|
|
17
|
+
getDogfoodDevServerStatus: status,
|
|
18
|
+
stopDogfoodDevServer: stop,
|
|
19
|
+
resolveDogfoodUrl: (path) => `http://agent.test${path}`,
|
|
20
|
+
};
|
|
21
|
+
const snapshots = [];
|
|
22
|
+
const controller = new DogfoodRuntime_1.DogfoodController({ name: 'Flutter app', framework: 'flutter', workDir: '/workspace/app', lane: 'browser' }, (0, P2PDogfoodDriver_1.createP2PDogfoodDriver)(client, { pollIntervalMs: 1, startupTimeoutMs: 100 }), { onChange: (snapshot) => snapshots.push(snapshot.logs.map((line) => line.text)) });
|
|
23
|
+
const result = await controller.trigger();
|
|
24
|
+
expect(result.url).toBe('http://agent.test/dev/');
|
|
25
|
+
expect(status).toHaveBeenCalledTimes(2);
|
|
26
|
+
expect(snapshots.flat()).toContain('$ flutter run -d web-server');
|
|
27
|
+
expect(stop).not.toHaveBeenCalled();
|
|
28
|
+
await controller.stop();
|
|
29
|
+
expect(stop).toHaveBeenCalledTimes(1);
|
|
30
|
+
});
|
|
31
|
+
it('starts and cleans up an available native WebRTC runtime', async () => {
|
|
32
|
+
const closeRuntime = jest.fn(async () => { });
|
|
33
|
+
const client = {
|
|
34
|
+
subscribeDogfoodDevEvents: () => jest.fn(),
|
|
35
|
+
getDogfoodRemoteRuntimeCapabilities: jest.fn(async () => ({
|
|
36
|
+
targets: [
|
|
37
|
+
{ id: 'browser-window', label: 'Browser', enabled: true },
|
|
38
|
+
{ id: 'ios-simulator', label: 'iPhone simulator', enabled: true },
|
|
39
|
+
],
|
|
40
|
+
})),
|
|
41
|
+
startDogfoodRemoteRuntime: jest.fn(async () => ({
|
|
42
|
+
id: 'runtime-1', status: 'starting', targetId: 'ios-simulator',
|
|
43
|
+
})),
|
|
44
|
+
stopDogfoodRemoteRuntime: closeRuntime,
|
|
45
|
+
};
|
|
46
|
+
const controller = new DogfoodRuntime_1.DogfoodController({ name: 'Native app', framework: 'flutter', workDir: '/workspace/app', lane: 'webrtc' }, (0, P2PDogfoodDriver_1.createP2PDogfoodDriver)(client));
|
|
47
|
+
await expect(controller.trigger()).resolves.toMatchObject({ lane: 'webrtc', sessionId: 'runtime-1' });
|
|
48
|
+
expect(closeRuntime).not.toHaveBeenCalled();
|
|
49
|
+
await controller.stop();
|
|
50
|
+
expect(closeRuntime).toHaveBeenCalledWith('runtime-1');
|
|
51
|
+
});
|
|
52
|
+
it('delivers Hermes without stopping an unrelated dev server on exit', async () => {
|
|
53
|
+
const stop = jest.fn(async () => { });
|
|
54
|
+
const client = {
|
|
55
|
+
subscribeDogfoodDevEvents: () => jest.fn(),
|
|
56
|
+
startDogfoodDevServer: jest.fn(async () => ({ running: true, framework: 'expo', workDir: '/workspace/app' })),
|
|
57
|
+
stopDogfoodDevServer: stop,
|
|
58
|
+
};
|
|
59
|
+
const controller = new DogfoodRuntime_1.DogfoodController({ name: 'RN app', framework: 'expo', workDir: '/workspace/app', lane: 'hermes' }, (0, P2PDogfoodDriver_1.createP2PDogfoodDriver)(client));
|
|
60
|
+
await expect(controller.trigger()).resolves.toMatchObject({ lane: 'hermes', metadata: { delivered: true } });
|
|
61
|
+
await controller.stop();
|
|
62
|
+
expect(stop).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
declare const mockReactNative: {
|
|
2
|
+
Platform: {
|
|
3
|
+
OS: string;
|
|
4
|
+
};
|
|
5
|
+
NativeModules: {};
|
|
6
|
+
};
|
|
7
|
+
declare const mockExpoModule: {
|
|
8
|
+
default: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
/** Mirrors mobile/app.json in the Talos repo — the SDK's first external consumer. */
|
|
11
|
+
declare const EXPO_CONFIG: {
|
|
12
|
+
name: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
version: string;
|
|
15
|
+
ios: {
|
|
16
|
+
bundleIdentifier: string;
|
|
17
|
+
buildNumber: string;
|
|
18
|
+
};
|
|
19
|
+
android: {
|
|
20
|
+
package: string;
|
|
21
|
+
versionCode: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
declare function mockExpoConstants(expoConfig: unknown, extra?: Record<string, unknown>): void;
|
|
25
|
+
declare function loadResolve(): typeof import('../P2PClient').resolveReportIdentity;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
2
|
// resolveAppIdentity() has always fed /vibing/execute and /dev/reload-app so
|
|
4
3
|
// the agent could route "vibe on THIS app" to the right repo, but nothing fed
|
|
5
4
|
// /feedback — reports carried no identity at all, so the agent's fix router
|
|
6
5
|
// had nothing to resolve and fell back to whatever directory it was sitting
|
|
7
6
|
// in. resolveReportIdentity() closes that gap for the feedback path.
|
|
8
|
-
|
|
7
|
+
const mockReactNative = {
|
|
9
8
|
Platform: { OS: 'ios' },
|
|
10
9
|
NativeModules: {},
|
|
11
|
-
}
|
|
10
|
+
};
|
|
11
|
+
const mockExpoModule = { default: {} };
|
|
12
|
+
jest.mock('react-native', () => mockReactNative);
|
|
13
|
+
jest.mock('expo-constants', () => mockExpoModule, { virtual: true });
|
|
12
14
|
/** Mirrors mobile/app.json in the Talos repo — the SDK's first external consumer. */
|
|
13
15
|
const EXPO_CONFIG = {
|
|
14
16
|
name: 'Talos',
|
|
@@ -18,15 +20,30 @@ const EXPO_CONFIG = {
|
|
|
18
20
|
android: { package: 'works.talos.mobile', versionCode: 423 },
|
|
19
21
|
};
|
|
20
22
|
function mockExpoConstants(expoConfig, extra = {}) {
|
|
21
|
-
|
|
23
|
+
mockExpoModule.default = { expoConfig, ...extra };
|
|
24
|
+
}
|
|
25
|
+
function loadResolve() {
|
|
26
|
+
let resolve;
|
|
27
|
+
jest.isolateModules(() => {
|
|
28
|
+
jest.doMock('react-native', () => mockReactNative);
|
|
29
|
+
jest.doMock('expo-constants', () => mockExpoModule, { virtual: true });
|
|
30
|
+
resolve = require('../P2PClient').resolveReportIdentity;
|
|
31
|
+
});
|
|
32
|
+
return resolve;
|
|
22
33
|
}
|
|
23
34
|
beforeEach(() => {
|
|
35
|
+
// P2PClient is imported by many suites with different virtual RN/Expo
|
|
36
|
+
// modules. Re-evaluate it against this suite's mutable modules every time;
|
|
37
|
+
// an order-dependent green test is not an identity-routing guard.
|
|
24
38
|
jest.resetModules();
|
|
39
|
+
mockReactNative.Platform.OS = 'ios';
|
|
40
|
+
mockReactNative.NativeModules = {};
|
|
41
|
+
mockExpoModule.default = {};
|
|
25
42
|
});
|
|
26
43
|
describe('resolveReportIdentity', () => {
|
|
27
44
|
it('resolves app name and bundle id from the Expo config', () => {
|
|
28
45
|
mockExpoConstants(EXPO_CONFIG);
|
|
29
|
-
const
|
|
46
|
+
const resolve = loadResolve();
|
|
30
47
|
const identity = resolve();
|
|
31
48
|
// The agent's fix router reads DeviceInfo.AppName first.
|
|
32
49
|
expect(identity.appName).toBe('Talos');
|
|
@@ -39,7 +56,7 @@ describe('resolveReportIdentity', () => {
|
|
|
39
56
|
});
|
|
40
57
|
it('never reports a project path', () => {
|
|
41
58
|
mockExpoConstants(EXPO_CONFIG);
|
|
42
|
-
const
|
|
59
|
+
const resolve = loadResolve();
|
|
43
60
|
// The agent ignores client-supplied paths on feedback reports (an
|
|
44
61
|
// untrusted guest could otherwise aim the fix task's CWD at ~/.ssh).
|
|
45
62
|
// Sending one would be misleading at best.
|
|
@@ -47,7 +64,7 @@ describe('resolveReportIdentity', () => {
|
|
|
47
64
|
});
|
|
48
65
|
it('prefers the native runtime version over the manifest version', () => {
|
|
49
66
|
mockExpoConstants(EXPO_CONFIG, { nativeAppVersion: '1.9.158', nativeBuildVersion: '428' });
|
|
50
|
-
const
|
|
67
|
+
const resolve = loadResolve();
|
|
51
68
|
const identity = resolve();
|
|
52
69
|
// A stale manifest shouldn't misreport which build is actually running.
|
|
53
70
|
expect(identity.app.version).toBe('1.9.158');
|
|
@@ -55,7 +72,7 @@ describe('resolveReportIdentity', () => {
|
|
|
55
72
|
});
|
|
56
73
|
it('falls back to the manifest version when no native version is exposed', () => {
|
|
57
74
|
mockExpoConstants(EXPO_CONFIG);
|
|
58
|
-
const
|
|
75
|
+
const resolve = loadResolve();
|
|
59
76
|
const identity = resolve();
|
|
60
77
|
expect(identity.app.version).toBe('1.9.157');
|
|
61
78
|
expect(identity.app.buildNumber).toBe('427');
|
|
@@ -71,16 +88,13 @@ describe('resolveReportIdentity', () => {
|
|
|
71
88
|
android: { package: 'io.yaver.mobile' },
|
|
72
89
|
};
|
|
73
90
|
function mockContainer(yaverInfo) {
|
|
74
|
-
|
|
75
|
-
Platform: { OS: 'ios' },
|
|
76
|
-
NativeModules: { YaverInfo: { isYaver: true, ...yaverInfo } },
|
|
77
|
-
}));
|
|
91
|
+
mockReactNative.NativeModules = { YaverInfo: { isYaver: true, ...yaverInfo } };
|
|
78
92
|
// The host's manifest — what expo-constants answers for a guest bundle.
|
|
79
93
|
mockExpoConstants(YAVER_CONFIG);
|
|
80
94
|
}
|
|
81
95
|
it("never reports the host's bundle id as the guest's", () => {
|
|
82
96
|
mockContainer({ inheritedGuestProjectName: 'talos / mobile' });
|
|
83
|
-
const
|
|
97
|
+
const resolve = loadResolve();
|
|
84
98
|
const identity = resolve();
|
|
85
99
|
// The agent routes on bundle id FIRST. Leaking io.yaver.mobile here
|
|
86
100
|
// would resolve to yaver.io and the project name would never be read.
|
|
@@ -91,14 +105,14 @@ describe('resolveReportIdentity', () => {
|
|
|
91
105
|
});
|
|
92
106
|
it('reports the guest project Yaver pinned', () => {
|
|
93
107
|
mockContainer({ inheritedGuestProjectName: 'talos / mobile' });
|
|
94
|
-
const
|
|
108
|
+
const resolve = loadResolve();
|
|
95
109
|
const identity = resolve();
|
|
96
110
|
expect(identity.appName).toBe('talos / mobile');
|
|
97
111
|
expect(identity.project?.projectName).toBe('talos / mobile');
|
|
98
112
|
});
|
|
99
113
|
it("lets the app's own declaration win over the pinned project", () => {
|
|
100
114
|
mockContainer({ inheritedGuestProjectName: 'something-else' });
|
|
101
|
-
const
|
|
115
|
+
const resolve = loadResolve();
|
|
102
116
|
// A bundle knows its own identity; nothing ambient should override it.
|
|
103
117
|
const identity = resolve({ projectName: 'Talos', bundleId: 'works.talos.mobile' });
|
|
104
118
|
expect(identity.appName).toBe('Talos');
|
|
@@ -106,7 +120,7 @@ describe('resolveReportIdentity', () => {
|
|
|
106
120
|
});
|
|
107
121
|
it('reports nothing rather than something wrong when no project is pinned', () => {
|
|
108
122
|
mockContainer({});
|
|
109
|
-
const
|
|
123
|
+
const resolve = loadResolve();
|
|
110
124
|
// Better for the agent to reject/fall back than to confidently edit
|
|
111
125
|
// the wrong repo.
|
|
112
126
|
const identity = resolve();
|
|
@@ -117,14 +131,14 @@ describe('resolveReportIdentity', () => {
|
|
|
117
131
|
});
|
|
118
132
|
it('lets an explicit declaration override the ambient identity', () => {
|
|
119
133
|
mockExpoConstants(EXPO_CONFIG);
|
|
120
|
-
const
|
|
134
|
+
const resolve = loadResolve();
|
|
121
135
|
const identity = resolve({ projectName: 'Override', bundleId: 'com.override.app' });
|
|
122
136
|
expect(identity.appName).toBe('Override');
|
|
123
137
|
expect(identity.project?.bundleId).toBe('com.override.app');
|
|
124
138
|
});
|
|
125
139
|
it('passes declared surfaces stacks and voice metadata through', () => {
|
|
126
140
|
mockExpoConstants(EXPO_CONFIG);
|
|
127
|
-
const
|
|
141
|
+
const resolve = loadResolve();
|
|
128
142
|
const identity = resolve({
|
|
129
143
|
projectName: 'Omni',
|
|
130
144
|
bundleId: 'io.example.omni',
|
|
@@ -156,10 +170,8 @@ describe('resolveReportIdentity', () => {
|
|
|
156
170
|
it('omits the project block when nothing identifies the app', () => {
|
|
157
171
|
// Bare RN with no expo-constants and no native modules. The report must
|
|
158
172
|
// still upload — the agent just resolves it by its own means.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}, { virtual: true });
|
|
162
|
-
const { resolveReportIdentity: resolve } = require('../P2PClient');
|
|
173
|
+
mockExpoModule.default = {};
|
|
174
|
+
const resolve = loadResolve();
|
|
163
175
|
const identity = resolve();
|
|
164
176
|
expect(identity.project).toBeUndefined();
|
|
165
177
|
expect(identity.appName).toBeUndefined();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const react_native_1 = require("react-native");
|
|
3
4
|
const YaverFeedback_1 = require("../YaverFeedback");
|
|
4
5
|
// Mock react-native: DeviceEventEmitter for event dispatch + Platform so
|
|
5
6
|
// ShakeDetector.start() can branch on iOS without hitting a real RN runtime.
|
|
@@ -33,13 +34,10 @@ jest.mock('../auth', () => ({
|
|
|
33
34
|
needsAuth: false,
|
|
34
35
|
runnerDown: false,
|
|
35
36
|
lastHeartbeat: Date.now(),
|
|
36
|
-
isGuest: false,
|
|
37
|
-
accessScope: 'owner',
|
|
38
37
|
quicHost: '127.0.0.1',
|
|
39
38
|
quicPort: 18080,
|
|
40
39
|
},
|
|
41
40
|
],
|
|
42
|
-
shared: [],
|
|
43
41
|
})),
|
|
44
42
|
DEFAULT_CONVEX_SITE_URL: 'https://example.convex.site',
|
|
45
43
|
}));
|
|
@@ -51,6 +49,18 @@ beforeEach(() => {
|
|
|
51
49
|
jest.clearAllMocks();
|
|
52
50
|
});
|
|
53
51
|
describe('YaverFeedback', () => {
|
|
52
|
+
describe('Dogfood onboarding', () => {
|
|
53
|
+
it('starts with Yaver OAuth when the host has no session', () => {
|
|
54
|
+
YaverFeedback_1.YaverFeedback.init({ enabled: true });
|
|
55
|
+
YaverFeedback_1.YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app', label: 'Example' });
|
|
56
|
+
expect(react_native_1.DeviceEventEmitter.emit).toHaveBeenCalledWith('yaverFeedback:startLogin');
|
|
57
|
+
});
|
|
58
|
+
it('asks for a machine after an existing OAuth session', () => {
|
|
59
|
+
YaverFeedback_1.YaverFeedback.init({ enabled: true, authToken: 'owner-token' });
|
|
60
|
+
YaverFeedback_1.YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app', label: 'Example' });
|
|
61
|
+
expect(react_native_1.DeviceEventEmitter.emit).toHaveBeenCalledWith('yaverFeedback:startMachinePicker');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
54
64
|
describe('init()', () => {
|
|
55
65
|
it('sets config correctly with defaults', () => {
|
|
56
66
|
YaverFeedback_1.YaverFeedback.init({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const tweetnacl_1 = __importDefault(require("tweetnacl"));
|
|
7
|
+
const buffer_1 = require("buffer");
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const deviceDogfood_1 = require("../deviceDogfood");
|
|
10
|
+
class MemorySecureStore {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.values = new Map();
|
|
13
|
+
}
|
|
14
|
+
async getItemAsync(key) { return this.values.get(key) ?? null; }
|
|
15
|
+
async setItemAsync(key, value) { this.values.set(key, value); }
|
|
16
|
+
async deleteItemAsync(key) { this.values.delete(key); }
|
|
17
|
+
}
|
|
18
|
+
function response(body, status = 200) {
|
|
19
|
+
return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
|
20
|
+
}
|
|
21
|
+
describe('YaverDeviceDogfood', () => {
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
if (!globalThis.crypto)
|
|
24
|
+
globalThis.crypto = crypto_1.webcrypto;
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => { jest.restoreAllMocks(); });
|
|
27
|
+
test('keeps identity stable and proves possession rather than trusting the UUID', async () => {
|
|
28
|
+
const store = new MemorySecureStore();
|
|
29
|
+
const client = new deviceDogfood_1.YaverDeviceDogfood({ appId: 'io.example.test', secureStore: store, backendUrl: 'https://dogfood.test' });
|
|
30
|
+
const first = await client.enrollmentInfo();
|
|
31
|
+
const second = await client.enrollmentInfo();
|
|
32
|
+
expect(second).toEqual(first);
|
|
33
|
+
let proof;
|
|
34
|
+
jest.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
|
35
|
+
const url = String(input);
|
|
36
|
+
if (url.endsWith('/dogfood/enroll/start'))
|
|
37
|
+
return response({ status: 'pending', challenge: 'server-nonce' });
|
|
38
|
+
if (url.endsWith('/dogfood/enroll/prove')) {
|
|
39
|
+
proof = JSON.parse(String(init?.body));
|
|
40
|
+
return response({ status: 'pending', proofVerified: true });
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`unexpected URL ${url}`);
|
|
43
|
+
});
|
|
44
|
+
await client.enroll('ios');
|
|
45
|
+
const message = new TextEncoder().encode(`yaver-dogfood-enroll-v1\nio.example.test\n${first.installationId}\nserver-nonce`);
|
|
46
|
+
expect(tweetnacl_1.default.sign.detached.verify(message, new Uint8Array(buffer_1.Buffer.from(proof.signature, 'base64')), new Uint8Array(buffer_1.Buffer.from(first.publicKey, 'base64')))).toBe(true);
|
|
47
|
+
const attacker = tweetnacl_1.default.sign.keyPair();
|
|
48
|
+
expect(tweetnacl_1.default.sign.detached.verify(message, new Uint8Array(buffer_1.Buffer.from(proof.signature, 'base64')), attacker.publicKey)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
test('re-register rotates key and installation while preserving only the logical slot', async () => {
|
|
51
|
+
const store = new MemorySecureStore();
|
|
52
|
+
const client = new deviceDogfood_1.YaverDeviceDogfood({ appId: 'io.example.test', secureStore: store, backendUrl: 'https://dogfood.test' });
|
|
53
|
+
const before = await client.enrollmentInfo();
|
|
54
|
+
jest.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
|
55
|
+
if (String(input).endsWith('/dogfood/enroll/start'))
|
|
56
|
+
return response({ status: 'pending', challenge: 'rotate-nonce' });
|
|
57
|
+
return response({ status: 'pending', proofVerified: true });
|
|
58
|
+
});
|
|
59
|
+
await client.reRegister('ios');
|
|
60
|
+
const after = await client.enrollmentInfo();
|
|
61
|
+
expect(after.registrationSlot).toBe(before.registrationSlot);
|
|
62
|
+
expect(after.installationId).not.toBe(before.installationId);
|
|
63
|
+
expect(after.publicKey).not.toBe(before.publicKey);
|
|
64
|
+
});
|
|
65
|
+
test('exchanges an approved key proof for a short-lived scoped session', async () => {
|
|
66
|
+
const client = new deviceDogfood_1.YaverDeviceDogfood({ appId: 'io.example.test', secureStore: new MemorySecureStore(), backendUrl: 'https://dogfood.test' });
|
|
67
|
+
const identity = await client.enrollmentInfo();
|
|
68
|
+
let sessionProof;
|
|
69
|
+
jest.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
|
70
|
+
const url = String(input);
|
|
71
|
+
if (url.includes('/dogfood/enroll/status'))
|
|
72
|
+
return response({ status: 'active' });
|
|
73
|
+
if (url.endsWith('/dogfood/session/challenge'))
|
|
74
|
+
return response({ challenge: 'session-nonce' });
|
|
75
|
+
if (url.endsWith('/dogfood/session')) {
|
|
76
|
+
sessionProof = JSON.parse(String(init?.body));
|
|
77
|
+
return response({ token: 'short-token', expiresAt: 123, scopes: ['feedback', 'blackbox'], projectSlug: 'example' });
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`unexpected URL ${url}`);
|
|
80
|
+
});
|
|
81
|
+
const session = await client.session();
|
|
82
|
+
expect(session).toMatchObject({ active: true, token: 'short-token', scopes: ['feedback', 'blackbox'] });
|
|
83
|
+
const message = new TextEncoder().encode(`yaver-dogfood-session-v1\nio.example.test\n${identity.installationId}\nsession-nonce`);
|
|
84
|
+
expect(tweetnacl_1.default.sign.detached.verify(message, new Uint8Array(buffer_1.Buffer.from(sessionProof.signature, 'base64')), new Uint8Array(buffer_1.Buffer.from(identity.publicKey, 'base64')))).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const dogfoodPolicy_1 = require("../dogfoodPolicy");
|
|
4
|
+
describe('resolveSDKDogfood', () => {
|
|
5
|
+
it('fails closed unless Dogfood is explicitly enabled', () => {
|
|
6
|
+
expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ accountIds: ['acct-1'], currentAccountId: 'acct-1' })).toMatchObject({
|
|
7
|
+
active: false,
|
|
8
|
+
code: 'SDK_DOGFOOD_DISABLED',
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
it('requires an exact approved app-account ID', () => {
|
|
12
|
+
expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, accountIds: ['acct-1'], currentAccountId: '' }).code)
|
|
13
|
+
.toBe('SDK_DOGFOOD_ACCOUNT_REQUIRED');
|
|
14
|
+
expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, accountIds: ['acct-1'], currentAccountId: 'acct-10' }).code)
|
|
15
|
+
.toBe('SDK_DOGFOOD_ACCOUNT_NOT_ALLOWED');
|
|
16
|
+
expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, accountIds: ['acct-1'], currentAccountId: 'acct-1' })).toMatchObject({
|
|
17
|
+
active: true,
|
|
18
|
+
code: 'SDK_DOGFOOD_ACTIVE',
|
|
19
|
+
accountId: 'acct-1',
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
it('supports a key-enrolled installation without an app account', () => {
|
|
23
|
+
expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, appId: 'io.example', installationStatus: 'active' }).code)
|
|
24
|
+
.toBe('SDK_DOGFOOD_INSTALLATION_REQUIRED');
|
|
25
|
+
expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, appId: 'io.example', installationId: 'install-1', installationStatus: 'pending' }).code)
|
|
26
|
+
.toBe('SDK_DOGFOOD_INSTALLATION_NOT_ACTIVE');
|
|
27
|
+
expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, appId: 'io.example', installationId: 'install-1', installationStatus: 'active' })).toMatchObject({
|
|
28
|
+
active: true,
|
|
29
|
+
code: 'SDK_DOGFOOD_ACTIVE',
|
|
30
|
+
accountId: 'install-1',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -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;
|