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.
Files changed (103) hide show
  1. package/README.md +102 -1
  2. package/dist/AuthOverlay.d.ts +1 -14
  3. package/dist/AuthOverlay.js +9 -62
  4. package/dist/Discovery.js +0 -6
  5. package/dist/DogfoodRuntime.d.ts +124 -0
  6. package/dist/DogfoodRuntime.js +273 -0
  7. package/dist/FeedbackModal.js +347 -61
  8. package/dist/LoginScreen.d.ts +1 -5
  9. package/dist/LoginScreen.js +2 -6
  10. package/dist/MachinePickerScreen.d.ts +1 -3
  11. package/dist/MachinePickerScreen.js +6 -18
  12. package/dist/P2PClient.d.ts +116 -3
  13. package/dist/P2PClient.js +273 -3
  14. package/dist/P2PDogfoodDriver.d.ts +12 -0
  15. package/dist/P2PDogfoodDriver.js +118 -0
  16. package/dist/PairDeviceModal.d.ts +2 -3
  17. package/dist/VibeChatScreen.d.ts +11 -1
  18. package/dist/VibeChatScreen.js +200 -41
  19. package/dist/YaverFeedback.d.ts +34 -0
  20. package/dist/YaverFeedback.js +124 -19
  21. package/dist/YaverModeBadge.d.ts +22 -0
  22. package/dist/YaverModeBadge.js +219 -0
  23. package/dist/__tests__/AuthDevices.test.js +1 -48
  24. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  25. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  26. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  27. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  28. package/dist/__tests__/ReportIdentity.test.d.ts +25 -1
  29. package/dist/__tests__/ReportIdentity.test.js +34 -22
  30. package/dist/__tests__/YaverFeedback.test.js +13 -3
  31. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  32. package/dist/__tests__/deviceDogfood.test.js +86 -0
  33. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  34. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  35. package/dist/_core/ansi.d.ts +117 -0
  36. package/dist/_core/ansi.js +468 -0
  37. package/dist/_core/ansi.test.d.ts +1 -0
  38. package/dist/_core/ansi.test.js +225 -0
  39. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  40. package/dist/_core/buildFeedbackPrompt.js +18 -72
  41. package/dist/_core/constants.d.ts +19 -6
  42. package/dist/_core/constants.js +20 -7
  43. package/dist/_core/device.d.ts +9 -23
  44. package/dist/_core/device.js +13 -28
  45. package/dist/_core/endpoints.d.ts +0 -7
  46. package/dist/_core/endpoints.js +0 -7
  47. package/dist/_core/index.d.ts +4 -0
  48. package/dist/_core/index.js +4 -0
  49. package/dist/_core/remoteless.d.ts +44 -0
  50. package/dist/_core/remoteless.js +75 -0
  51. package/dist/_core/trace.d.ts +47 -0
  52. package/dist/_core/trace.js +38 -0
  53. package/dist/_core/trace.test.d.ts +1 -0
  54. package/dist/_core/trace.test.js +60 -0
  55. package/dist/auth.d.ts +3 -60
  56. package/dist/auth.js +6 -88
  57. package/dist/deviceDogfood.d.ts +53 -0
  58. package/dist/deviceDogfood.js +137 -0
  59. package/dist/dogfoodPolicy.d.ts +25 -0
  60. package/dist/dogfoodPolicy.js +24 -0
  61. package/dist/index.d.ts +13 -4
  62. package/dist/index.js +24 -8
  63. package/dist/reloadActions.js +2 -2
  64. package/dist/types.d.ts +50 -7
  65. package/package.json +12 -3
  66. package/src/AuthOverlay.tsx +20 -106
  67. package/src/Discovery.ts +0 -6
  68. package/src/DogfoodRuntime.ts +373 -0
  69. package/src/FeedbackModal.tsx +445 -67
  70. package/src/LoginScreen.tsx +2 -22
  71. package/src/MachinePickerScreen.tsx +6 -21
  72. package/src/P2PClient.ts +347 -4
  73. package/src/P2PDogfoodDriver.ts +132 -0
  74. package/src/PairDeviceModal.tsx +2 -3
  75. package/src/VibeChatScreen.tsx +232 -42
  76. package/src/YaverFeedback.ts +133 -22
  77. package/src/YaverModeBadge.tsx +234 -0
  78. package/src/__tests__/AuthDevices.test.ts +1 -52
  79. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  80. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  81. package/src/__tests__/ReportIdentity.test.ts +36 -27
  82. package/src/__tests__/YaverFeedback.test.ts +15 -3
  83. package/src/__tests__/deviceDogfood.test.ts +77 -0
  84. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  85. package/src/_core/ansi.test.ts +250 -0
  86. package/src/_core/ansi.ts +475 -0
  87. package/src/_core/buildFeedbackPrompt.ts +18 -95
  88. package/src/_core/constants.ts +20 -6
  89. package/src/_core/device.ts +13 -30
  90. package/src/_core/endpoints.ts +0 -7
  91. package/src/_core/index.ts +4 -0
  92. package/src/_core/remoteless.ts +110 -0
  93. package/src/_core/trace.test.ts +58 -0
  94. package/src/_core/trace.ts +75 -0
  95. package/src/auth.ts +7 -156
  96. package/src/deviceDogfood.ts +171 -0
  97. package/src/dogfoodPolicy.ts +40 -0
  98. package/src/index.ts +40 -11
  99. package/src/reloadActions.ts +2 -2
  100. package/src/types.ts +45 -7
  101. package/dist/GuestOnboardingScreen.d.ts +0 -8
  102. package/dist/GuestOnboardingScreen.js +0 -282
  103. package/src/GuestOnboardingScreen.tsx +0 -307
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ // AUTO-SYNCED from shared/client-core/src/ansi.test.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
+ /**
7
+ * ansi.test.ts — guards for the shared ANSI tokenizer.
8
+ *
9
+ * The bug this exists for (2026-08-09): the dashboard and mobile app both
10
+ * flattened opencode's raw ANSI stream to plain text with stripAnsi, losing
11
+ * every colour the console has, while the xterm Terminal view kept the
12
+ * bytes — two surfaces, two looks, one product. The tokenizer below is the
13
+ * single classifier both chat renderers consume; these tests pin the tokens
14
+ * so a renderer drift (web spans vs mobile nested Text) can never silently
15
+ * change what a user sees.
16
+ *
17
+ * Run: npx tsx shared/client-core/src/ansi.test.ts
18
+ */
19
+ const ansi_1 = require("./ansi");
20
+ let failures = 0;
21
+ const eq = (got, want, label) => {
22
+ if (JSON.stringify(got) === JSON.stringify(want))
23
+ console.log(`ok ${label}`);
24
+ else {
25
+ console.error(`FAIL ${label}:\n got ${JSON.stringify(got)}\n want ${JSON.stringify(want)}`);
26
+ failures++;
27
+ }
28
+ };
29
+ const ok = (c, label) => eq(Boolean(c), true, label);
30
+ // ── plain text passes through untouched ─────────────────────────────────
31
+ {
32
+ const t = (0, ansi_1.tokenizeAnsi)("hello world");
33
+ eq(t, [{ text: "hello world" }], "plain text → single unstyled token");
34
+ }
35
+ // ── SGR colours ──────────────────────────────────────────────────────────
36
+ {
37
+ const t = (0, ansi_1.tokenizeAnsi)("\x1b[31mred\x1b[0m plain");
38
+ eq(t, [
39
+ { text: "red", fg: { kind: "named", index: 1 } },
40
+ { text: " plain" },
41
+ ], "red text then reset → two tokens");
42
+ }
43
+ {
44
+ const t = (0, ansi_1.tokenizeAnsi)("\x1b[32m+\x1b[0m added");
45
+ eq(t, [
46
+ { text: "+", fg: { kind: "named", index: 2 } },
47
+ { text: " added" },
48
+ ], "green + line (git patch)");
49
+ }
50
+ {
51
+ const t = (0, ansi_1.tokenizeAnsi)("\x1b[1m\x1b[33m$ ls\x1b[0m");
52
+ eq(t, [
53
+ { text: "$ ls", fg: { kind: "named", index: 3 }, bold: true },
54
+ ], "bold yellow $ prompt");
55
+ }
56
+ // ── 256-colour palette ──────────────────────────────────────────────────
57
+ {
58
+ const t = (0, ansi_1.tokenizeAnsi)("\x1b[38;5;208morange\x1b[0m");
59
+ eq(t, [
60
+ { text: "orange", fg: { kind: "palette", index: 208 } },
61
+ ], "xterm-256 orange (208)");
62
+ }
63
+ {
64
+ const [r, g, b] = (0, ansi_1.paletteRgb)(208);
65
+ ok(r > 200 && g > 80 && g < 160 && b < 60, "palette 208 is orange-ish RGB");
66
+ }
67
+ {
68
+ const [r, g, b] = (0, ansi_1.paletteRgb)(2);
69
+ eq([r, g, b], [...ansi_1.ANSI_16_RGB[2]], "palette index <16 maps to the standard 16");
70
+ }
71
+ // ── truecolor ───────────────────────────────────────────────────────────
72
+ {
73
+ const t = (0, ansi_1.tokenizeAnsi)("\x1b[38;2;255;120;50mtrue\x1b[0m");
74
+ eq(t, [
75
+ { text: "true", fg: { kind: "rgb", rgb: [255, 120, 50] } },
76
+ ], "truecolor SGR 38;2;r;g;b");
77
+ }
78
+ // ── background ──────────────────────────────────────────────────────────
79
+ {
80
+ const t = (0, ansi_1.tokenizeAnsi)("\x1b[48;5;236mgray bg\x1b[0m");
81
+ eq(t, [
82
+ { text: "gray bg", bg: { kind: "palette", index: 236 } },
83
+ ], "xterm-256 background (patch gray)");
84
+ }
85
+ // ── bold / dim / underline / strike ─────────────────────────────────────
86
+ {
87
+ const t = (0, ansi_1.tokenizeAnsi)("\x1b[1mbold\x1b[22m \x1b[2mdim\x1b[0m \x1b[4munder\x1b[0m \x1b[9mstrike\x1b[0m");
88
+ eq(t, [
89
+ { text: "bold", bold: true },
90
+ { text: " " },
91
+ { text: "dim", dim: true },
92
+ { text: " " },
93
+ { text: "under", underline: true },
94
+ { text: " " },
95
+ { text: "strike", strike: true },
96
+ ], "bold/dim/underline/strike attributes");
97
+ }
98
+ // ── bare \x1b[m resets (the opencode banner emits this constantly) ─────
99
+ {
100
+ const t = (0, ansi_1.tokenizeAnsi)("\x1b[0m\n> build · deepseek-v4-flash\n\x1b[0m\n");
101
+ eq(t, [
102
+ { text: "\n> build · deepseek-v4-flash\n\n" },
103
+ ], "bare [0m sequences are dropped, text survives");
104
+ }
105
+ // ── $ prompt lines in a real opencode run ───────────────────────────────
106
+ {
107
+ const raw = "\x1b[0m\n> build · deepseek-v4-flash\n\x1b[0m\n\x1b[0m$ \x1b[0mls -la\n";
108
+ const t = (0, ansi_1.tokenizeAnsi)(raw);
109
+ eq((0, ansi_1.ansiToPlain)(raw), "\n> build · deepseek-v4-flash\n\n$ ls -la\n", "plain extraction matches the visible console text");
110
+ const lines = (0, ansi_1.tokenStreamToLines)(t);
111
+ ok(lines.length >= 4, "token stream splits into lines at newlines");
112
+ const last = lines[lines.length - 1];
113
+ ok(last.tokens.some((tk) => tk.text.includes("$ ls -la")), "last line carries the $ prompt text");
114
+ }
115
+ // ── OSC-8 hyperlink extraction ──────────────────────────────────────────
116
+ {
117
+ const t = (0, ansi_1.tokenizeAnsi)("\x1b]8;;https://example.com\x07link text\x1b]8;;\x07 rest");
118
+ eq(t[0]?.href, "https://example.com", "OSC-8 link URL carried on the token");
119
+ eq(t[0]?.text, "link text", "OSC-8 link inner text kept");
120
+ eq(t[1]?.text, " rest", "text after link kept unstyled");
121
+ }
122
+ // ── OSC-8 scheme allowlist (the audit XSS finding, §6.1) ───────────────
123
+ // A prompt-injected \x1b]8;;javascript:…\x07 link used to reach the renderer
124
+ // verbatim and become an <a href> in the dashboard origin. The tokenizer now
125
+ // drops every scheme except http(s):// and mailto:// — the link becomes plain
126
+ // text, never a clickable/executable URL.
127
+ {
128
+ const evil = "\x1b]8;;javascript:alert(1)\x07click\x1b]8;;\x07";
129
+ const t = (0, ansi_1.tokenizeAnsi)(evil);
130
+ eq(t[0]?.href, undefined, "javascript: OSC-8 link is not carried as href");
131
+ eq(t[0]?.text, "click", "the inner text survives as plain text");
132
+ const data = (0, ansi_1.tokenizeAnsi)("\x1b]8;;data:text/html,<script>1</script>\x07x\x1b]8;;\x07");
133
+ eq(data[0]?.href, undefined, "data: OSC-8 link is dropped");
134
+ const proto = (0, ansi_1.tokenizeAnsi)("\x1b]8;;//evil.example/path\x07x\x1b]8;;\x07");
135
+ eq(proto[0]?.href, undefined, "protocol-relative OSC-8 link is dropped");
136
+ const bare = (0, ansi_1.tokenizeAnsi)("\x1b]8;;alert(1)\x07x\x1b]8;;\x07");
137
+ eq(bare[0]?.href, undefined, "scheme-less OSC-8 string is dropped");
138
+ const mail = (0, ansi_1.tokenizeAnsi)("\x1b]8;;mailto:a@b.co\x07mail\x1b]8;;\x07");
139
+ eq(mail[0]?.href, "mailto:a@b.co", "mailto: OSC-8 link is allowed");
140
+ }
141
+ // ── cursor/erase sequences become line breaks (TUI repaint) ─────────────
142
+ {
143
+ const t = (0, ansi_1.tokenizeAnsi)("line1\x1b[2Kline2");
144
+ const plain = t.map((x) => x.text).join("");
145
+ ok(plain.includes("\n"), "erase-line (K) leaves a line boundary for a repaint");
146
+ }
147
+ // ── unknown SGR codes are ignored, text survives ────────────────────────
148
+ {
149
+ const t = (0, ansi_1.tokenizeAnsi)("\x1b[999mweird\x1b[0m ok");
150
+ eq(t, [
151
+ { text: "weird ok" },
152
+ ], "unknown SGR code ignored, adjacent runs merge (no visible style change)");
153
+ }
154
+ if (failures > 0) {
155
+ console.error(`\n${failures} FAILURE(s)`);
156
+ process.exit(1);
157
+ }
158
+ // ── structural console-line classification ──────────────────────────────
159
+ {
160
+ eq((0, ansi_1.classifyAnsiLine)("> build · deepseek-v4-flash"), "banner", "opencode build banner");
161
+ eq((0, ansi_1.classifyAnsiLine)("> plan · gpt-5.4"), "banner", "opencode plan banner");
162
+ eq((0, ansi_1.classifyAnsiLine)("$ ls -la"), "prompt", "$ prompt");
163
+ eq((0, ansi_1.classifyAnsiLine)("$"), "plain", "lone $ is plain (no command)");
164
+ eq((0, ansi_1.classifyAnsiLine)("diff --git a/x.ts b/x.ts"), "diff-header", "git diff header");
165
+ eq((0, ansi_1.classifyAnsiLine)("+++ b/src/x.ts"), "diff-file", "+++ file line");
166
+ eq((0, ansi_1.classifyAnsiLine)("--- a/src/x.ts"), "diff-file", "--- file line");
167
+ eq((0, ansi_1.classifyAnsiLine)("@@ -1,2 +1,3 @@"), "diff-hunk", "hunk header");
168
+ eq((0, ansi_1.classifyAnsiLine)("+const x = 1;"), "diff-add", "patch add");
169
+ eq((0, ansi_1.classifyAnsiLine)("-const y = 2;"), "diff-del", "patch del");
170
+ eq((0, ansi_1.classifyAnsiLine)(" + indented keeps leading"), "plain", "indented + is not a patch add");
171
+ eq((0, ansi_1.classifyAnsiLine)("⎿ run npx tsc"), "tool-call", "opencode tool tail");
172
+ eq((0, ansi_1.classifyAnsiLine)("✓ 3 files changed"), "tool-call", "checkmark status");
173
+ eq((0, ansi_1.classifyAnsiLine)("normal output line"), "plain", "ordinary line");
174
+ }
175
+ {
176
+ const styled = (0, ansi_1.styleAnsiLines)("\x1b[0m\n> build · deepseek-v4-flash\n\x1b[0m\n$ \x1b[0mls\n");
177
+ ok(styled.length >= 4, "styleAnsiLines splits into lines");
178
+ const hints = styled.map((l) => l.hint);
179
+ ok(hints.includes("banner"), "banner line classified through the one-stop helper");
180
+ ok(hints.includes("prompt"), "$ prompt classified through the one-stop helper");
181
+ }
182
+ // ── summarizeRawConsole (shared noisy-console reducer) ──────────────────
183
+ {
184
+ const raw = [
185
+ "> build · deepseek-v4-flash", // banner — kept
186
+ "$ npm run build", // prompt echo — dropped
187
+ "workdir: /repo", // runner config banner — dropped
188
+ "compiled 42 files", // real output — kept
189
+ "compiled 42 files", // repeat
190
+ "compiled 42 files", // 3rd repeat — collapsed
191
+ "compiled 42 files", // 4th — dropped
192
+ "diff --git a/x.ts b/x.ts", // diff hunk — dropped
193
+ "──────────", // TUI redraw edge — dropped
194
+ "✓ done", // status — kept
195
+ ].join("\n");
196
+ const out = (0, ansi_1.summarizeRawConsole)(raw, false);
197
+ ok(out.includes("> build · deepseek-v4-flash"), "banner survives");
198
+ ok(!out.includes("$ npm run build"), "$ echo dropped");
199
+ ok(!out.includes("workdir:"), "config banner dropped");
200
+ ok(out.includes("compiled 42 files"), "real output kept");
201
+ ok(!out.includes("diff --git"), "diff hunk dropped");
202
+ ok(!out.includes("──────────"), "TUI redraw edge dropped");
203
+ ok(out.includes("✓ done"), "status kept");
204
+ ok(out.includes("noisy lines collapsed"), "collapse count reported");
205
+ // Running budget is tighter than finished.
206
+ const big = Array.from({ length: 200 }, (_, i) => `line ${i}`).join("\n");
207
+ const running = (0, ansi_1.summarizeRawConsole)(big, true).split("\n").length;
208
+ const finished = (0, ansi_1.summarizeRawConsole)(big, false).split("\n").length;
209
+ ok(running < finished, "running budget tighter than finished budget");
210
+ // KEEP THE TAIL (2026-08-13): the summarizer evicts the OLDEST kept line
211
+ // when the budget is full, so a live console follows the newest output —
212
+ // never drops it. The old head-keeping bug froze the web task console at
213
+ // the START of a long run while the header claimed it was live.
214
+ const tail = (0, ansi_1.summarizeRawConsole)(big, true).split("\n");
215
+ ok(tail.some((l) => l.startsWith("line 199")), "newest output survives the budget (tail kept)");
216
+ ok(!tail.some((l) => l.startsWith("line 0")), "oldest output evicted to make room");
217
+ // The tail must still be recognizably the END of the stream, and the
218
+ // collapse marker must not masquerade as a kept line.
219
+ const noMarker = tail.filter((l) => !l.startsWith("… "));
220
+ ok(noMarker[0].startsWith("line 160"), "first kept line is the oldest evicted window start (budget 40 of 200)");
221
+ // ANSI on kept lines survives (so AnsiConsoleText can still paint them).
222
+ const ansiLine = "\x1b[31mred text\x1b[0m";
223
+ ok((0, ansi_1.summarizeRawConsole)(ansiLine, false).includes("\x1b[31m"), "kept lines keep their escapes");
224
+ }
225
+ console.log("\nall ansi tests pass");
@@ -1,13 +1,8 @@
1
- export interface BuildFeedbackPromptInput {
1
+ export interface FeedbackPromptInput {
2
2
  userPrompt: string;
3
- /** Hot-Reload project name when running inside Yaver mobile, OR
4
- * the host app's bundle/package name when running standalone. */
5
3
  projectName?: string;
6
- /** Absolute path on the host where the project lives (only known
7
- * when running inside Yaver mobile via Hot Reload). */
8
4
  projectPath?: string;
9
- /** True when the caller has attached a screenshot of the current
10
- * screen as the first image in the task's images array. */
11
- hasScreenshot: boolean;
5
+ hasScreenshot?: boolean;
12
6
  }
13
- export declare function buildFeedbackPrompt(input: BuildFeedbackPromptInput): string;
7
+ /** Shared, deterministic feedback-task prompt used by every client surface. */
8
+ export declare function buildFeedbackPrompt(input: FeedbackPromptInput): string;
@@ -1,77 +1,23 @@
1
1
  "use strict";
2
- // buildFeedbackPrompt shared prompt enrichment used by every Yaver
3
- // feedback surface, in-Yaver native pane (mirrored in Swift + Kotlin)
4
- // AND the standalone RN feedback SDK (this file). Keep all three
5
- // implementations in lockstep — the wording is what the AI on the
6
- // remote is conditioned to expect.
7
- //
8
- // The bare user text on its own loses crucial context: WHICH app the
9
- // user is testing, WHICH screen they're looking at, and whether a
10
- // screenshot is attached for visual reference. Without that the agent
11
- // guesses, edits the wrong project, or asks clarifying questions
12
- // instead of acting. The wrapper below tells the agent:
13
- // - this feedback comes from the in-app drawer while the user is
14
- // mid-test,
15
- // - which project the user is in (when known),
16
- // - that the FIRST attached image (when present) is a snapshot of
17
- // the current screen — open it to see what the user is pointing
18
- // at,
19
- // - that changes should be applied to that project's source +
20
- // saved so the user can trigger a Hermes reload to see them.
21
- //
22
- // Cross-reference: mobile/ios/Yaver/YaverFeedbackPane.swift's
23
- // `buildFeedbackPrompt` and mobile/android/.../YaverFeedbackPane.kt's
24
- // `buildFeedbackPrompt`. All three must match.
2
+ // AUTO-SYNCED from shared/client-core/src/buildFeedbackPrompt.ts.
3
+ // DO NOT EDIT IN PLACE. Edit the source and re-run
4
+ // scripts/sync-client-core.sh. CI checks drift via `--check`.
25
5
  Object.defineProperty(exports, "__esModule", { value: true });
26
6
  exports.buildFeedbackPrompt = buildFeedbackPrompt;
7
+ /** Shared, deterministic feedback-task prompt used by every client surface. */
27
8
  function buildFeedbackPrompt(input) {
28
- const userPrompt = input.userPrompt ?? "";
29
- const projectName = (input.projectName ?? "").trim();
30
- const projectPath = (input.projectPath ?? "").trim();
31
- const hasScreenshot = !!input.hasScreenshot;
32
- const lines = [];
33
- lines.push("[Mobile feedback from inside Yaver]");
34
- lines.push("The user is providing this feedback while running a mobile app inside the Yaver mobile container " +
35
- "and is currently looking at a specific screen of that app.");
36
- lines.push("");
37
- if (projectName || projectPath) {
38
- lines.push("App being tested:");
39
- if (projectName)
40
- lines.push(` name: ${projectName}`);
41
- if (projectPath)
42
- lines.push(` path: ${projectPath}`);
43
- lines.push("");
44
- }
45
- if (hasScreenshot) {
46
- lines.push("A screenshot of the current screen is attached as the first image. " +
47
- "Open it before deciding what to change — the user is pointing at what they SEE, " +
48
- "not necessarily what is named most prominently in the source.");
49
- lines.push("");
50
- }
51
- else {
52
- lines.push("(The user chose not to attach a screenshot for this round.)");
53
- lines.push("");
54
- }
55
- lines.push("Operation contract:");
56
- lines.push("1. Locate the file(s) responsible for what the user described and EDIT them in place. " +
57
- "Save the changes — that is the deliverable.");
58
- lines.push("2. Stream a CONCISE Claude-Code / Codex-style narration as you work: " +
59
- "one short line per step (e.g. \"Reading app/index.tsx\", " +
60
- "\"Editing safe.backgroundColor\", \"Saved app/index.tsx\"). Show small diffs only — " +
61
- "never dump entire files, never paste node_modules contents, never echo build / install logs.");
62
- lines.push("3. Do NOT run npm install / yarn / pnpm / git clone / cargo build / docker pull or any other " +
63
- "long-running install / fetch command. The repo is already prepared on this machine. " +
64
- "If a dependency is genuinely missing, say so in one line and stop — the user will install it.");
65
- lines.push("4. Do NOT trigger a Hermes reload yourself. The user has a Reload button in the drawer " +
66
- "and decides when to refresh.");
67
- lines.push("5. Keep total output under a few hundred lines. Heavy ripgrep / find / cat with no filter " +
68
- "are usually the wrong tool — use targeted reads.");
69
- if (!projectName && !projectPath) {
70
- lines.push("6. If you can identify the project from the prompt or the screenshot, work there. " +
71
- "Otherwise ask the user briefly which project to target — one short line, no exhaustive list.");
72
- }
73
- lines.push("");
74
- lines.push("User feedback:");
75
- lines.push(userPrompt);
76
- return lines.join("\n");
9
+ const request = String(input.userPrompt || '').trim();
10
+ const context = [];
11
+ if (input.projectName?.trim())
12
+ context.push(`Project: ${input.projectName.trim()}`);
13
+ if (input.projectPath?.trim())
14
+ context.push(`Working directory: ${input.projectPath.trim()}`);
15
+ if (input.hasScreenshot)
16
+ context.push('A screenshot is attached; use it as visual evidence.');
17
+ return [
18
+ 'Investigate and implement this feedback request in the named project.',
19
+ ...context,
20
+ '',
21
+ request || 'Inspect the attached evidence and fix the visible issue.',
22
+ ].join('\n');
77
23
  }
@@ -27,13 +27,26 @@ export declare const DEFAULT_BEACON_UDP_PORT = 19837;
27
27
  /**
28
28
  * How old an agent's last heartbeat can be before the device is
29
29
  * considered offline. Mirrors `backend/convex/devices.ts` so
30
- * Convex + every client agree on the same threshold. The agent
31
- * heartbeats every 5 min (see `desktop/agent/main.go::heartbeatLoop`),
32
- * so 6 min tolerates one missed beat + 60 s of jitter without
33
- * flapping. Sub-minute death detection comes from the P2P bus, not
34
- * this threshold.
30
+ * Convex + every client agree on the same threshold.
31
+ *
32
+ * 900_000, not 90_000. This file said 90_000 while the authority named in the
33
+ * comment `backend/convex/devices.ts:112`, `const HEARTBEAT_STALE_MS = 900 *
34
+ * 1000` — and `mobile/src/_core/constants.ts` both said 900_000. A 10x drift in
35
+ * the constant whose own docstring promises it does not drift is exactly the
36
+ * "green on one, yellow on the other" glitch described above (2026-07-20).
37
+ *
38
+ * Sub-minute death detection is not this threshold's job — it comes from the
39
+ * P2P bus and from an actual reachability probe. Shortening this here would
40
+ * only make a live box flap offline between 5-minute heartbeats.
41
+ */
42
+ export declare const HEARTBEAT_STALE_MS = 900000;
43
+ /**
44
+ * How long after the last relay-presence ping a device still counts as
45
+ * having a "live bus signal". Same product number as the web dashboard's
46
+ * hasRecentLiveSignal default (`web/lib/device-lifecycle.ts`, maxAgeMs =
47
+ * 360_000) so the phone and the browser agree on what "live" means.
35
48
  */
36
- export declare const HEARTBEAT_STALE_MS = 360000;
49
+ export declare const BUS_PRESENCE_STALE_MS = 360000;
37
50
  /**
38
51
  * How long after the last UDP beacon an agent is still considered
39
52
  * "locally present". Re-broadcast interval is 3 s, so 10 s covers
@@ -3,7 +3,7 @@
3
3
  // DO NOT EDIT IN PLACE. Edit the source and re-run
4
4
  // scripts/sync-client-core.sh. CI checks drift via `--check`.
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.OAUTH_REDIRECT = exports.RELAY_PROBE_TIMEOUT_MS = exports.PROBE_TIMEOUT_MS = exports.BEACON_STALE_MS = exports.HEARTBEAT_STALE_MS = exports.DEFAULT_BEACON_UDP_PORT = exports.DEFAULT_AGENT_HTTP_PORT = exports.WEB_BASE_URL = exports.CONVEX_SITE_URL = void 0;
6
+ exports.OAUTH_REDIRECT = exports.RELAY_PROBE_TIMEOUT_MS = exports.PROBE_TIMEOUT_MS = exports.BEACON_STALE_MS = exports.BUS_PRESENCE_STALE_MS = exports.HEARTBEAT_STALE_MS = exports.DEFAULT_BEACON_UDP_PORT = exports.DEFAULT_AGENT_HTTP_PORT = exports.WEB_BASE_URL = exports.CONVEX_SITE_URL = void 0;
7
7
  /**
8
8
  * Canonical client-side constants shared between:
9
9
  * - Yaver mobile (`mobile/`)
@@ -51,13 +51,26 @@ exports.DEFAULT_BEACON_UDP_PORT = 19837;
51
51
  /**
52
52
  * How old an agent's last heartbeat can be before the device is
53
53
  * considered offline. Mirrors `backend/convex/devices.ts` so
54
- * Convex + every client agree on the same threshold. The agent
55
- * heartbeats every 5 min (see `desktop/agent/main.go::heartbeatLoop`),
56
- * so 6 min tolerates one missed beat + 60 s of jitter without
57
- * flapping. Sub-minute death detection comes from the P2P bus, not
58
- * this threshold.
54
+ * Convex + every client agree on the same threshold.
55
+ *
56
+ * 900_000, not 90_000. This file said 90_000 while the authority named in the
57
+ * comment `backend/convex/devices.ts:112`, `const HEARTBEAT_STALE_MS = 900 *
58
+ * 1000` — and `mobile/src/_core/constants.ts` both said 900_000. A 10x drift in
59
+ * the constant whose own docstring promises it does not drift is exactly the
60
+ * "green on one, yellow on the other" glitch described above (2026-07-20).
61
+ *
62
+ * Sub-minute death detection is not this threshold's job — it comes from the
63
+ * P2P bus and from an actual reachability probe. Shortening this here would
64
+ * only make a live box flap offline between 5-minute heartbeats.
65
+ */
66
+ exports.HEARTBEAT_STALE_MS = 900000;
67
+ /**
68
+ * How long after the last relay-presence ping a device still counts as
69
+ * having a "live bus signal". Same product number as the web dashboard's
70
+ * hasRecentLiveSignal default (`web/lib/device-lifecycle.ts`, maxAgeMs =
71
+ * 360_000) so the phone and the browser agree on what "live" means.
59
72
  */
60
- exports.HEARTBEAT_STALE_MS = 360000;
73
+ exports.BUS_PRESENCE_STALE_MS = 360000;
61
74
  /**
62
75
  * How long after the last UDP beacon an agent is still considered
63
76
  * "locally present". Re-broadcast interval is 3 s, so 10 s covers
@@ -10,10 +10,6 @@ export interface CoreDevice {
10
10
  runnerDown: boolean;
11
11
  /** Unix ms of the latest heartbeat the agent sent to Convex. */
12
12
  lastHeartbeat: number;
13
- isGuest: boolean;
14
- hostName?: string;
15
- hostEmail?: string;
16
- accessScope?: 'owner' | 'shared-scoped' | 'shared-legacy';
17
13
  /** Primary LAN IP (or tunnel host) the agent advertised. */
18
14
  quicHost: string;
19
15
  quicPort: number;
@@ -39,28 +35,18 @@ export declare function mergeDeviceEntries(a: CoreDevice, b: CoreDevice): CoreDe
39
35
  */
40
36
  export declare function collapseDevices(devices: CoreDevice[]): CoreDevice[];
41
37
  /**
42
- * "Fresh" matches the mobile app: online + heartbeat within
43
- * HEARTBEAT_STALE_MS. Clients read Convex's `isOnline` first (the backend
44
- * applies the same gate from the server clock), then use this helper when
45
- * they need the phone-side freshness opinion too — e.g. for auto-connect
46
- * picks.
38
+ * "Fresh" matches the mobile app: online + heartbeat < 90 s. Clients
39
+ * read Convex's `isOnline` first (backend already applies its own 90 s
40
+ * gate from the server clock), then use this helper when they need the
41
+ * phone-side freshness opinion too — e.g. for auto-connect picks.
47
42
  */
48
43
  export declare function isDeviceFresh(d: CoreDevice, now?: number): boolean;
49
44
  /**
50
- * Choose the best candidate for an auto-connect attempt.
51
- *
52
- * An explicit `preferredDeviceId` is honoured by id ALONE, or not at all:
53
- * - A missing `quicHost` is not grounds to reroute. Relay transport
54
- * addresses a device by id (`<relay>/d/<deviceId>`), so the entry is
55
- * still reachable off-LAN — which is precisely when quicHost is absent.
56
- * - If the id is not in the list, return null. Falling through to another
57
- * machine silently lands the user's fix on the wrong host: they pick the
58
- * Mac mini, the commit shows up on the laptop, and nothing reports an
59
- * error. Not connecting is the better failure — the caller surfaces
60
- * "selected machine missing, re-select it".
61
- *
62
- * With no preference, fall back: fresh + quicHost → online + quicHost →
63
- * first with a quicHost.
45
+ * Choose the best candidate for an auto-connect attempt. Preference:
46
+ * 1. explicit `preferredDeviceId`, by identity alone; missing means fail
47
+ * 2. fresh (online + recent heartbeat) + has a quicHost
48
+ * 3. online + has a quicHost
49
+ * 4. first with a quicHost
64
50
  */
65
51
  export declare function pickTargetDevice(devices: CoreDevice[], preferredDeviceId?: string): CoreDevice | null;
66
52
  /**
@@ -50,10 +50,6 @@ function deviceIdentityKey(d) {
50
50
  return `hwid:${d.hwid}`;
51
51
  if (d.publicKey)
52
52
  return `pub:${d.publicKey}`;
53
- if (d.isGuest) {
54
- const scope = d.hostEmail || d.hostName || 'guest';
55
- return `guest:${scope}:${d.deviceId || d.name}`;
56
- }
57
53
  const n = normName(d.name);
58
54
  const os = String(d.platform || '').trim().toLowerCase();
59
55
  if (n && os)
@@ -63,8 +59,6 @@ function deviceIdentityKey(d) {
63
59
  return `name:${d.name}`;
64
60
  }
65
61
  function deviceAliasKey(d) {
66
- if (d.isGuest)
67
- return null;
68
62
  const n = normName(d.name);
69
63
  const os = String(d.platform || '').trim().toLowerCase();
70
64
  if (!n || !os)
@@ -72,8 +66,6 @@ function deviceAliasKey(d) {
72
66
  return `${os}:${n}`;
73
67
  }
74
68
  function deviceEndpointKey(d) {
75
- if (d.isGuest)
76
- return null;
77
69
  const h = normHost(d.quicHost);
78
70
  if (!h)
79
71
  return null;
@@ -182,11 +174,10 @@ function collapseDevices(devices) {
182
174
  }
183
175
  // ── Freshness + target pick ───────────────────────────────────────────
184
176
  /**
185
- * "Fresh" matches the mobile app: online + heartbeat within
186
- * HEARTBEAT_STALE_MS. Clients read Convex's `isOnline` first (the backend
187
- * applies the same gate from the server clock), then use this helper when
188
- * they need the phone-side freshness opinion too — e.g. for auto-connect
189
- * picks.
177
+ * "Fresh" matches the mobile app: online + heartbeat < 90 s. Clients
178
+ * read Convex's `isOnline` first (backend already applies its own 90 s
179
+ * gate from the server clock), then use this helper when they need the
180
+ * phone-side freshness opinion too — e.g. for auto-connect picks.
190
181
  */
191
182
  function isDeviceFresh(d, now = Date.now()) {
192
183
  if (!d.isOnline)
@@ -196,26 +187,20 @@ function isDeviceFresh(d, now = Date.now()) {
196
187
  return now - d.lastHeartbeat < constants_1.HEARTBEAT_STALE_MS;
197
188
  }
198
189
  /**
199
- * Choose the best candidate for an auto-connect attempt.
200
- *
201
- * An explicit `preferredDeviceId` is honoured by id ALONE, or not at all:
202
- * - A missing `quicHost` is not grounds to reroute. Relay transport
203
- * addresses a device by id (`<relay>/d/<deviceId>`), so the entry is
204
- * still reachable off-LAN — which is precisely when quicHost is absent.
205
- * - If the id is not in the list, return null. Falling through to another
206
- * machine silently lands the user's fix on the wrong host: they pick the
207
- * Mac mini, the commit shows up on the laptop, and nothing reports an
208
- * error. Not connecting is the better failure — the caller surfaces
209
- * "selected machine missing, re-select it".
210
- *
211
- * With no preference, fall back: fresh + quicHost → online + quicHost →
212
- * first with a quicHost.
190
+ * Choose the best candidate for an auto-connect attempt. Preference:
191
+ * 1. explicit `preferredDeviceId`, by identity alone; missing means fail
192
+ * 2. fresh (online + recent heartbeat) + has a quicHost
193
+ * 3. online + has a quicHost
194
+ * 4. first with a quicHost
213
195
  */
214
196
  function pickTargetDevice(devices, preferredDeviceId) {
215
197
  if (!devices.length)
216
198
  return null;
217
199
  if (preferredDeviceId) {
218
- return devices.find((d) => d.deviceId === preferredDeviceId) ?? null;
200
+ // An off-LAN device normally has no quicHost; the relay addresses it by
201
+ // id. Never fall through to a different healthy machine after the user
202
+ // selected one explicitly — failure is safer than misrouting their work.
203
+ return devices.find((d) => d.deviceId === preferredDeviceId) || null;
219
204
  }
220
205
  const fresh = devices.find((d) => isDeviceFresh(d) && d.quicHost);
221
206
  if (fresh)
@@ -51,13 +51,6 @@ export declare const CONVEX_ENDPOINTS: {
51
51
  readonly authLogin: "/auth/login";
52
52
  readonly userSettings: "/settings";
53
53
  readonly platformConfig: "/config";
54
- readonly guestsList: "/guests/list";
55
- readonly guestsHosts: "/guests/hosts";
56
- readonly guestsAllowed: "/guests/allowed";
57
- readonly guestsInvite: "/guests/invite";
58
- readonly guestsAccept: "/guests/accept";
59
- readonly guestsAcceptCode: "/guests/accept-code";
60
- readonly guestsRevoke: "/guests/revoke";
61
54
  };
62
55
  /** Routes on a relay server. */
63
56
  export declare const RELAY_ENDPOINTS: {
@@ -57,13 +57,6 @@ exports.CONVEX_ENDPOINTS = {
57
57
  authLogin: '/auth/login',
58
58
  userSettings: '/settings',
59
59
  platformConfig: '/config',
60
- guestsList: '/guests/list',
61
- guestsHosts: '/guests/hosts',
62
- guestsAllowed: '/guests/allowed',
63
- guestsInvite: '/guests/invite',
64
- guestsAccept: '/guests/accept',
65
- guestsAcceptCode: '/guests/accept-code',
66
- guestsRevoke: '/guests/revoke',
67
60
  };
68
61
  /** Routes on a relay server. */
69
62
  exports.RELAY_ENDPOINTS = {
@@ -13,3 +13,7 @@
13
13
  export * from './constants';
14
14
  export * from './endpoints';
15
15
  export * from './device';
16
+ export * from './ansi';
17
+ export * from './trace';
18
+ export * from './remoteless';
19
+ export * from './buildFeedbackPrompt';
@@ -32,3 +32,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
32
32
  __exportStar(require("./constants"), exports);
33
33
  __exportStar(require("./endpoints"), exports);
34
34
  __exportStar(require("./device"), exports);
35
+ __exportStar(require("./ansi"), exports);
36
+ __exportStar(require("./trace"), exports);
37
+ __exportStar(require("./remoteless"), exports);
38
+ __exportStar(require("./buildFeedbackPrompt"), exports);
@@ -0,0 +1,44 @@
1
+ export type RemotelessCapability = "analysis-chat" | "code-edit" | "git-read" | "git-commit" | "git-push" | "static-preflight" | "existing-web-artifact" | "dev-server" | "web-build" | "flutter-render" | "native-build" | "simulator" | "shell" | "test" | "deploy" | "container";
2
+ export type RemotelessSupport = "supported" | "bounded" | "unavailable";
3
+ export type RemotelessCapabilityResult = {
4
+ capability: RemotelessCapability;
5
+ support: RemotelessSupport;
6
+ code: string;
7
+ summary: string;
8
+ detail: string;
9
+ route: {
10
+ label: string;
11
+ path: "/devices" | "/cloud-onboarding";
12
+ };
13
+ alternateRoute?: {
14
+ label: string;
15
+ path: "/devices" | "/cloud-onboarding";
16
+ };
17
+ };
18
+ export type ExecutionCandidate = {
19
+ id: string;
20
+ name: string;
21
+ role: "explicit" | "primary" | "secondary" | "focused";
22
+ connected: boolean;
23
+ };
24
+ export type RemotelessPlacement = {
25
+ lane: "remote";
26
+ target: ExecutionCandidate;
27
+ degraded: boolean;
28
+ banner: string | null;
29
+ } | {
30
+ lane: "remoteless";
31
+ capability: RemotelessCapabilityResult;
32
+ banner: string;
33
+ } | {
34
+ lane: "blocked";
35
+ capability: RemotelessCapabilityResult;
36
+ banner: string;
37
+ };
38
+ export declare function remotelessCapability(capability: RemotelessCapability, surface: "ios" | "android" | "web" | "companion"): RemotelessCapabilityResult;
39
+ export declare function resolveRemotelessPlacement(input: {
40
+ capability: RemotelessCapability;
41
+ surface: "ios" | "android" | "web" | "companion";
42
+ candidates: ExecutionCandidate[];
43
+ forceLocal?: boolean;
44
+ }): RemotelessPlacement;