farming-code 2.2.8 → 2.2.12

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 (107) hide show
  1. package/README.md +42 -28
  2. package/README.zh_cn.md +42 -28
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1592 -741
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +54 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +35 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/executable-discovery.js +2 -0
  22. package/backend/farming-net-pass.js +285 -0
  23. package/backend/farming-net-registry.js +112 -0
  24. package/backend/farming-net-server.js +298 -0
  25. package/backend/farming-session-store.js +5 -13
  26. package/backend/git-worktree-info.js +181 -0
  27. package/backend/local-session-engine.js +411 -186
  28. package/backend/main-page-session.js +5 -2
  29. package/backend/native-pty-controller-generation.js +106 -0
  30. package/backend/native-pty-host-client.js +275 -7
  31. package/backend/native-pty-host-identity.js +86 -0
  32. package/backend/native-pty-host.js +813 -114
  33. package/backend/native-session-engine.js +100 -28
  34. package/backend/packaged-node-pty.js +22 -2
  35. package/backend/provider-adapters.js +253 -0
  36. package/backend/provider-session-service.js +241 -0
  37. package/backend/runtime-observation.js +81 -0
  38. package/backend/server.js +354 -84
  39. package/backend/session-engine-bridge.js +21 -2
  40. package/backend/session-engine-router.js +1 -1
  41. package/backend/session-engine.js +1 -1
  42. package/backend/session-stream-protocol.js +185 -0
  43. package/backend/storage-layout.js +55 -0
  44. package/backend/terminal-attach-checkpoint.js +74 -0
  45. package/backend/terminal-exit-quiescence.js +39 -0
  46. package/backend/terminal-reducer-flow-control.js +97 -0
  47. package/backend/terminal-screen-state.js +11 -2
  48. package/backend/terminal-screen-worker-pool.js +59 -6
  49. package/backend/terminal-screen-worker-thread.js +97 -57
  50. package/backend/terminal-screen-worker.js +133 -51
  51. package/backend/terminal-state-serialization.js +127 -0
  52. package/backend/terminal-status.js +23 -4
  53. package/backend/usage-monitor.js +176 -17
  54. package/backend/workspace-directory.js +232 -0
  55. package/backend/workspace-file-router.js +182 -76
  56. package/backend/workspace-file-service.js +319 -4
  57. package/backend/workspace-root-registry.js +164 -0
  58. package/dist/assets/App-DWsa7DXG.js +206 -0
  59. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
  60. package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
  61. package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
  62. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
  63. package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
  64. package/dist/assets/code-dark-DPiuyeSp.css +1 -0
  65. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  66. package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
  67. package/dist/assets/main-Bqtb5kuz.css +1 -0
  68. package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
  69. package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
  70. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  71. package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
  72. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
  73. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  74. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  75. package/dist/farming-2/site.webmanifest +2 -0
  76. package/dist/index.html +1 -1
  77. package/frontend/farming-net/app.css +625 -0
  78. package/frontend/farming-net/app.js +268 -0
  79. package/frontend/farming-net/index.html +86 -0
  80. package/frontend/reading-anchor.js +198 -0
  81. package/frontend/session-bridge.js +12 -3
  82. package/frontend/session-modal-bridge.js +5 -12
  83. package/frontend/skins/crt/app.js +1978 -793
  84. package/frontend/skins/crt/index.html +313 -23
  85. package/frontend/skins/crt/styles/billing.css +294 -223
  86. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  87. package/frontend/terminal-replay.js +372 -0
  88. package/package.json +10 -3
  89. package/shared/browser-protocol.d.ts +5 -0
  90. package/shared/browser-protocol.js +130 -0
  91. package/dist/assets/App-8dYAM6ql.js +0 -124
  92. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  93. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  94. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  95. package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
  96. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  97. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  98. package/dist/assets/main-D073SnW4.css +0 -1
  99. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  100. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  101. package/dist/assets/qoder-gHCinseV.svg +0 -1
  102. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  103. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  104. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  105. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  106. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  107. /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
@@ -0,0 +1,268 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ const copy = {
5
+ en: {
6
+ checking: 'Checking',
7
+ count: count => `${count} ${count === 1 ? 'environment' : 'environments'}`,
8
+ defaultSubtitle: 'All your deployed Farming workspaces, one click away.',
9
+ emptyBody: 'Try another name, owner, or platform.',
10
+ emptyTitle: 'No matching environment',
11
+ environments: 'Environments',
12
+ eyebrow: 'YOUR FARMING NETWORK',
13
+ failed: 'Farming Net could not load its private registry. Refresh to try again.',
14
+ footer: 'One portal token. Short-lived passes for enrolled Farms.',
15
+ justUpdated: 'Updated just now',
16
+ privateIndex: 'Private index',
17
+ reachable: 'Reachable',
18
+ refresh: 'Refresh',
19
+ scope: {
20
+ 'this-device': 'THIS DEVICE',
21
+ intranet: 'INTRANET',
22
+ remote: 'REMOTE',
23
+ tunnel: 'TUNNEL',
24
+ },
25
+ searchPlaceholder: 'Search environments',
26
+ unknown: 'Not verified',
27
+ },
28
+ zh: {
29
+ checking: '检查中',
30
+ count: count => `${count} 个环境`,
31
+ defaultSubtitle: '所有已经部署的 Farming,一个入口。',
32
+ emptyBody: '试试搜索环境名、所有者或平台。',
33
+ emptyTitle: '没有匹配的环境',
34
+ environments: '环境',
35
+ eyebrow: '你的 FARMING 网络',
36
+ failed: 'Farming Net 暂时无法读取私有环境列表,请刷新重试。',
37
+ footer: '一个门户 Token,短时通行已登记的 Farming。',
38
+ justUpdated: '刚刚更新',
39
+ privateIndex: '私有门户',
40
+ reachable: '可访问',
41
+ refresh: '刷新',
42
+ scope: {
43
+ 'this-device': '本机',
44
+ intranet: '内网',
45
+ remote: '远程',
46
+ tunnel: '隧道',
47
+ },
48
+ searchPlaceholder: '搜索环境',
49
+ unknown: '未确认',
50
+ },
51
+ };
52
+
53
+ const language = /^zh\b/i.test(navigator.language || '') ? 'zh' : 'en';
54
+ const t = copy[language];
55
+ const state = {
56
+ query: '',
57
+ registry: { title: 'Farming Net', subtitle: '', instances: [] },
58
+ availability: new Map(),
59
+ };
60
+
61
+ const elements = {
62
+ count: document.getElementById('instance-count'),
63
+ empty: document.getElementById('empty-state'),
64
+ grid: document.getElementById('instance-grid'),
65
+ notice: document.getElementById('notice'),
66
+ refresh: document.querySelector('[data-testid="net-refresh"]'),
67
+ search: document.querySelector('[data-testid="net-search"]'),
68
+ subtitle: document.getElementById('portal-subtitle'),
69
+ title: document.getElementById('portal-title'),
70
+ updated: document.getElementById('last-updated'),
71
+ };
72
+
73
+ function applyCopy() {
74
+ document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en';
75
+ document.querySelectorAll('[data-copy]').forEach(element => {
76
+ const value = t[element.dataset.copy];
77
+ if (typeof value === 'string') element.textContent = value;
78
+ });
79
+ document.querySelectorAll('[data-copy-placeholder]').forEach(element => {
80
+ const value = t[element.dataset.copyPlaceholder];
81
+ if (typeof value === 'string') element.setAttribute('placeholder', value);
82
+ });
83
+ }
84
+
85
+ function cardSearchText(instance) {
86
+ return [
87
+ instance.name,
88
+ instance.owner,
89
+ instance.platform,
90
+ instance.description,
91
+ ...instance.endpoints.map(endpoint => `${endpoint.label} ${endpoint.scope}`),
92
+ ].join(' ').toLocaleLowerCase();
93
+ }
94
+
95
+ function displayedInstances() {
96
+ const query = state.query.trim().toLocaleLowerCase();
97
+ const instances = [...state.registry.instances].sort((left, right) => {
98
+ if (left.pinned !== right.pinned) return left.pinned ? -1 : 1;
99
+ return left.name.localeCompare(right.name, language === 'zh' ? 'zh-CN' : 'en');
100
+ });
101
+ if (!query) return instances;
102
+ return instances.filter(instance => cardSearchText(instance).includes(query));
103
+ }
104
+
105
+ function initials(name) {
106
+ const parts = Array.from(String(name || '').trim());
107
+ return parts.slice(0, 2).join('').toUpperCase() || 'F';
108
+ }
109
+
110
+ function availabilityElement(instance) {
111
+ const status = state.availability.get(instance.id) || 'checking';
112
+ const element = document.createElement('span');
113
+ element.className = 'availability';
114
+ element.dataset.state = status;
115
+ element.dataset.testid = `net-status-${instance.id}`;
116
+
117
+ const dot = document.createElement('span');
118
+ dot.className = 'availability-dot';
119
+ dot.setAttribute('aria-hidden', 'true');
120
+ element.append(dot);
121
+
122
+ const label = document.createElement('span');
123
+ label.textContent = status === 'reachable' ? t.reachable : status === 'checking' ? t.checking : t.unknown;
124
+ element.append(label);
125
+ return element;
126
+ }
127
+
128
+ function endpointElement(instance, endpoint, endpointIndex) {
129
+ const link = document.createElement('a');
130
+ link.className = 'endpoint-link';
131
+ link.href = endpoint.launchUrl || endpoint.url;
132
+ link.target = '_blank';
133
+ link.rel = 'noopener noreferrer';
134
+ link.dataset.testid = endpointIndex === 0 ? `net-open-${instance.id}` : `net-open-${instance.id}-${endpointIndex + 1}`;
135
+
136
+ const label = document.createElement('span');
137
+ label.className = 'endpoint-label';
138
+ label.textContent = endpoint.label;
139
+ link.append(label);
140
+
141
+ const scope = document.createElement('span');
142
+ scope.className = 'endpoint-scope';
143
+ scope.textContent = t.scope[endpoint.scope] || t.scope.remote;
144
+ link.append(scope);
145
+
146
+ const arrow = document.createElement('span');
147
+ arrow.className = 'endpoint-arrow';
148
+ arrow.setAttribute('aria-hidden', 'true');
149
+ arrow.textContent = '↗';
150
+ link.append(arrow);
151
+ return link;
152
+ }
153
+
154
+ function instanceElement(instance) {
155
+ const card = document.createElement('article');
156
+ card.className = 'instance-card';
157
+ card.dataset.testid = `net-instance-card-${instance.id}`;
158
+
159
+ const topline = document.createElement('div');
160
+ topline.className = 'card-topline';
161
+ const avatar = document.createElement('div');
162
+ avatar.className = 'instance-avatar';
163
+ avatar.setAttribute('aria-hidden', 'true');
164
+ avatar.textContent = initials(instance.name);
165
+ topline.append(avatar, availabilityElement(instance));
166
+ card.append(topline);
167
+
168
+ const title = document.createElement('h3');
169
+ title.textContent = instance.name;
170
+ card.append(title);
171
+
172
+ const metadata = document.createElement('p');
173
+ metadata.className = 'instance-meta';
174
+ metadata.textContent = [instance.owner, instance.platform].filter(Boolean).join(' · ');
175
+ card.append(metadata);
176
+
177
+ const description = document.createElement('p');
178
+ description.className = 'instance-description';
179
+ description.textContent = instance.description || '\u00a0';
180
+ card.append(description);
181
+
182
+ const endpointList = document.createElement('div');
183
+ endpointList.className = 'endpoint-list';
184
+ instance.endpoints.forEach((endpoint, index) => endpointList.append(endpointElement(instance, endpoint, index)));
185
+ card.append(endpointList);
186
+ return card;
187
+ }
188
+
189
+ function render() {
190
+ const instances = displayedInstances();
191
+ elements.grid.replaceChildren(...instances.map(instanceElement));
192
+ elements.grid.setAttribute('aria-busy', 'false');
193
+ elements.empty.hidden = instances.length !== 0;
194
+ elements.count.textContent = t.count(instances.length);
195
+ }
196
+
197
+ async function probeInstance(instance) {
198
+ const endpoint = instance.endpoints.find(item => item.primary) || instance.endpoints[0];
199
+ if (!endpoint) return;
200
+ const controller = new window.AbortController();
201
+ const timeout = window.setTimeout(() => controller.abort(), 2500);
202
+ try {
203
+ const statusUrl = new URL('api/auth/status', endpoint.url);
204
+ await fetch(statusUrl, {
205
+ cache: 'no-store',
206
+ credentials: 'include',
207
+ mode: 'no-cors',
208
+ signal: controller.signal,
209
+ });
210
+ state.availability.set(instance.id, 'reachable');
211
+ } catch {
212
+ state.availability.set(instance.id, 'unknown');
213
+ } finally {
214
+ window.clearTimeout(timeout);
215
+ render();
216
+ }
217
+ }
218
+
219
+ async function loadRegistry() {
220
+ elements.notice.hidden = true;
221
+ elements.refresh.disabled = true;
222
+ elements.refresh.classList.add('is-loading');
223
+ elements.grid.setAttribute('aria-busy', 'true');
224
+ try {
225
+ const response = await fetch('api/instances', { cache: 'no-store', credentials: 'same-origin' });
226
+ if (!response.ok) throw new Error(`Registry request failed with ${response.status}`);
227
+ const registry = await response.json();
228
+ state.registry = registry;
229
+ state.availability = new Map(registry.instances.map(instance => [instance.id, 'checking']));
230
+ elements.title.textContent = registry.title || 'Farming Net';
231
+ elements.subtitle.textContent = registry.subtitle || t.defaultSubtitle;
232
+ document.title = registry.title || 'Farming Net';
233
+ elements.updated.textContent = t.justUpdated;
234
+ render();
235
+ registry.instances.forEach(instance => void probeInstance(instance));
236
+ } catch (error) {
237
+ console.error(error);
238
+ state.registry = { title: 'Farming Net', subtitle: '', instances: [] };
239
+ elements.notice.textContent = t.failed;
240
+ elements.notice.hidden = false;
241
+ render();
242
+ } finally {
243
+ elements.refresh.disabled = false;
244
+ elements.refresh.classList.remove('is-loading');
245
+ }
246
+ }
247
+
248
+ elements.search.addEventListener('input', event => {
249
+ state.query = event.target.value;
250
+ render();
251
+ });
252
+ elements.refresh.addEventListener('click', () => void loadRegistry());
253
+ document.addEventListener('keydown', event => {
254
+ if (event.key === '/' && document.activeElement !== elements.search) {
255
+ event.preventDefault();
256
+ elements.search.focus();
257
+ }
258
+ if (event.key === 'Escape' && document.activeElement === elements.search) {
259
+ elements.search.value = '';
260
+ state.query = '';
261
+ render();
262
+ elements.search.blur();
263
+ }
264
+ });
265
+
266
+ applyCopy();
267
+ void loadRegistry();
268
+ })();
@@ -0,0 +1,86 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="color-scheme" content="light dark">
7
+ <meta name="theme-color" content="#0d1712">
8
+ <meta name="description" content="A private index for deployed Farming workspaces.">
9
+ <link rel="icon" type="image/png" href="icon.png">
10
+ <link rel="stylesheet" href="app.css?v=font-spacing-1">
11
+ <script src="app.js?v=federated-pass-1" defer></script>
12
+ <title>Farming Net</title>
13
+ </head>
14
+ <body>
15
+ <div class="page-glow page-glow-one" aria-hidden="true"></div>
16
+ <div class="page-glow page-glow-two" aria-hidden="true"></div>
17
+
18
+ <div class="shell">
19
+ <header class="topbar">
20
+ <a class="brand" href="./" aria-label="Farming Net">
21
+ <img class="brand-icon" src="icon.png" alt="">
22
+ <span>Farming Net</span>
23
+ </a>
24
+ <div class="private-badge">
25
+ <span class="private-badge-dot" aria-hidden="true"></span>
26
+ <span data-copy="privateIndex">Private index</span>
27
+ </div>
28
+ </header>
29
+
30
+ <main>
31
+ <section class="hero" aria-labelledby="portal-title">
32
+ <p class="eyebrow" data-copy="eyebrow">YOUR FARMING NETWORK</p>
33
+ <h1 id="portal-title">Farming Net</h1>
34
+ <p id="portal-subtitle" class="subtitle" data-copy="defaultSubtitle">
35
+ All your deployed Farming workspaces, one click away.
36
+ </p>
37
+ </section>
38
+
39
+ <section class="toolbar" aria-label="Portal controls">
40
+ <label class="search-field" for="instance-search">
41
+ <svg viewBox="0 0 24 24" aria-hidden="true">
42
+ <path d="m21 21-4.35-4.35m2.35-5.15a7.5 7.5 0 1 1-15 0 7.5 7.5 0 0 1 15 0Z"></path>
43
+ </svg>
44
+ <input
45
+ id="instance-search"
46
+ data-testid="net-search"
47
+ type="search"
48
+ autocomplete="off"
49
+ data-copy-placeholder="searchPlaceholder"
50
+ placeholder="Search environments"
51
+ >
52
+ <kbd>/</kbd>
53
+ </label>
54
+ <button class="refresh-button" data-testid="net-refresh" type="button">
55
+ <svg viewBox="0 0 24 24" aria-hidden="true">
56
+ <path d="M20 11a8.1 8.1 0 0 0-15.5-2M4 4v5h5m-5 4a8.1 8.1 0 0 0 15.5 2M20 20v-5h-5"></path>
57
+ </svg>
58
+ <span data-copy="refresh">Refresh</span>
59
+ </button>
60
+ </section>
61
+
62
+ <section class="directory" aria-labelledby="directory-title">
63
+ <div class="directory-heading">
64
+ <h2 id="directory-title" data-copy="environments">Environments</h2>
65
+ <span id="instance-count" class="instance-count">—</span>
66
+ </div>
67
+
68
+ <div id="notice" class="notice" role="status" aria-live="polite" hidden></div>
69
+ <div id="instance-grid" class="instance-grid" aria-live="polite" aria-busy="true"></div>
70
+
71
+ <div id="empty-state" class="empty-state" hidden>
72
+ <div class="empty-mark" aria-hidden="true">⌘</div>
73
+ <h3 data-copy="emptyTitle">No matching environment</h3>
74
+ <p data-copy="emptyBody">Try another name, owner, or platform.</p>
75
+ </div>
76
+ </section>
77
+ </main>
78
+
79
+ <footer>
80
+ <span data-copy="footer">One portal token. Short-lived passes for enrolled Farms.</span>
81
+ <span class="footer-separator" aria-hidden="true"></span>
82
+ <span id="last-updated"></span>
83
+ </footer>
84
+ </div>
85
+ </body>
86
+ </html>
@@ -0,0 +1,198 @@
1
+ // A skin-neutral, per-tab reading-location protocol. Renderers own how they
2
+ // resolve an anchor; this module owns only its stable shape and transport-safe
3
+ // storage. Never put terminal text in an anchor: terminal locations use a
4
+ // short fingerprint of adjacent logical lines instead.
5
+ (function installFarmingReadingAnchors(global) {
6
+ 'use strict';
7
+
8
+ const VERSION = 1;
9
+ const STORAGE_PREFIX = 'farming.reading-anchor.v1:';
10
+ const SURFACES = new Set(['chat', 'terminal', 'file']);
11
+ const LIMITS = { id: 512, path: 2048, workspace: 2048, encoded: 1800 };
12
+
13
+ function boundedString(value, limit) {
14
+ const text = typeof value === 'string' ? value.trim() : '';
15
+ return text && text.length <= limit && !text.includes('\0') ? text : '';
16
+ }
17
+
18
+ function finiteNumber(value) {
19
+ const number = Number(value);
20
+ return Number.isFinite(number) ? number : null;
21
+ }
22
+
23
+ function normalizeAnchor(value) {
24
+ if (!value || typeof value !== 'object' || Number(value.version) !== VERSION) return null;
25
+ const surface = boundedString(value.surface, 16);
26
+ const resource = value.resource && typeof value.resource === 'object' ? value.resource : null;
27
+ const locator = value.locator && typeof value.locator === 'object' ? value.locator : null;
28
+ const position = value.position && typeof value.position === 'object' ? value.position : null;
29
+ if (!SURFACES.has(surface) || !resource || !locator || !position) return null;
30
+
31
+ if (surface === 'file') {
32
+ const workspace = boundedString(resource.workspace, LIMITS.workspace);
33
+ const path = boundedString(resource.path, LIMITS.path);
34
+ const line = finiteNumber(position.value);
35
+ const column = finiteNumber(position.column);
36
+ if (resource.kind !== 'file' || locator.kind !== 'file-line' || !workspace || !path || !Number.isInteger(line) || line < 1) return null;
37
+ return {
38
+ version: VERSION,
39
+ surface,
40
+ resource: { kind: 'file', workspace, path },
41
+ locator: { kind: 'file-line', id: path },
42
+ position: {
43
+ unit: 'line-column',
44
+ value: line,
45
+ ...(Number.isInteger(column) && column >= 1 ? { column } : {}),
46
+ },
47
+ };
48
+ }
49
+
50
+ const agentId = boundedString(resource.id, LIMITS.id);
51
+ const locatorId = boundedString(locator.id, LIMITS.id);
52
+ if (resource.kind !== 'agent' || !agentId || !locatorId) return null;
53
+ if (surface === 'chat') {
54
+ const fraction = finiteNumber(position.value);
55
+ const childId = boundedString(locator.childId, LIMITS.id);
56
+ if (locator.kind !== 'message' || position.unit !== 'fraction' || fraction === null || fraction < 0 || fraction > 1) return null;
57
+ return {
58
+ version: VERSION,
59
+ surface,
60
+ resource: { kind: 'agent', id: agentId },
61
+ locator: { kind: 'message', id: locatorId, ...(childId ? { childId } : {}) },
62
+ position: { unit: 'fraction', value: fraction },
63
+ };
64
+ }
65
+
66
+ const rowOffset = finiteNumber(position.value);
67
+ const lineCount = finiteNumber(locator.lineCount);
68
+ if (locator.kind !== 'terminal-lines' || position.unit !== 'row' || !Number.isInteger(rowOffset) || rowOffset < 0) return null;
69
+ return {
70
+ version: VERSION,
71
+ surface,
72
+ resource: { kind: 'agent', id: agentId },
73
+ locator: {
74
+ kind: 'terminal-lines',
75
+ id: locatorId,
76
+ ...(Number.isInteger(lineCount) && lineCount > 0 ? { lineCount } : {}),
77
+ },
78
+ position: { unit: 'row', value: rowOffset },
79
+ };
80
+ }
81
+
82
+ function keyFor(anchor) {
83
+ const normalized = normalizeAnchor(anchor);
84
+ if (!normalized) return '';
85
+ if (normalized.resource.kind === 'file') {
86
+ return `file:${normalized.resource.workspace}:${normalized.resource.path}`;
87
+ }
88
+ return `agent:${normalized.resource.id}:${normalized.surface}`;
89
+ }
90
+
91
+ function storageKey(key) {
92
+ return `${STORAGE_PREFIX}${key}`;
93
+ }
94
+
95
+ function save(anchor) {
96
+ const normalized = normalizeAnchor(anchor);
97
+ const key = normalized && keyFor(normalized);
98
+ if (!normalized || !key) return null;
99
+ try {
100
+ global.sessionStorage.setItem(storageKey(key), JSON.stringify(normalized));
101
+ } catch {
102
+ // Private browsing or an exhausted browser store must not break viewing.
103
+ }
104
+ return normalized;
105
+ }
106
+
107
+ function read(key) {
108
+ if (!key) return null;
109
+ try {
110
+ const parsed = JSON.parse(global.sessionStorage.getItem(storageKey(key)) || 'null');
111
+ const normalized = normalizeAnchor(parsed);
112
+ if (!normalized || keyFor(normalized) !== key) {
113
+ if (parsed) global.sessionStorage.removeItem(storageKey(key));
114
+ return null;
115
+ }
116
+ return normalized;
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ function remove(key) {
123
+ if (!key) return;
124
+ try {
125
+ global.sessionStorage.removeItem(storageKey(key));
126
+ } catch {
127
+ // Best-effort only.
128
+ }
129
+ }
130
+
131
+ function agentKey(agentId, surface) {
132
+ return `agent:${String(agentId || '').trim()}:${surface}`;
133
+ }
134
+
135
+ function fileKey(workspace, path) {
136
+ return `file:${String(workspace || '').trim()}:${String(path || '').trim()}`;
137
+ }
138
+
139
+ function fingerprint(parts) {
140
+ const value = (Array.isArray(parts) ? parts : [parts])
141
+ .map(part => String(part || '').slice(0, 2048))
142
+ .join('\u001f');
143
+ let hash = 0x811c9dc5;
144
+ for (let index = 0; index < value.length; index += 1) {
145
+ hash ^= value.charCodeAt(index);
146
+ hash = Math.imul(hash, 0x01000193);
147
+ }
148
+ return `fnv1a-${(hash >>> 0).toString(36)}-${value.length.toString(36)}`;
149
+ }
150
+
151
+ function encode(anchor) {
152
+ const normalized = normalizeAnchor(anchor);
153
+ if (!normalized) return '';
154
+ try {
155
+ const text = JSON.stringify(normalized);
156
+ const encoded = global.btoa(unescape(encodeURIComponent(text)))
157
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
158
+ return encoded.length <= LIMITS.encoded ? encoded : '';
159
+ } catch {
160
+ return '';
161
+ }
162
+ }
163
+
164
+ function decode(encoded) {
165
+ const compact = boundedString(encoded, LIMITS.encoded);
166
+ if (!compact || !/^[A-Za-z0-9_-]+$/.test(compact)) return null;
167
+ try {
168
+ const padded = compact.replace(/-/g, '+').replace(/_/g, '/') + '==='.slice((compact.length + 3) % 4);
169
+ return normalizeAnchor(JSON.parse(decodeURIComponent(escape(global.atob(padded)))));
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ function importEncoded(encoded) {
176
+ const anchor = decode(encoded);
177
+ return anchor ? save(anchor) : null;
178
+ }
179
+
180
+ function importFromSearch(search) {
181
+ const params = new global.URLSearchParams(search || global.location.search || '');
182
+ return importEncoded(params.get('fra') || '');
183
+ }
184
+
185
+ global.FarmingReadingAnchors = {
186
+ VERSION,
187
+ agentKey,
188
+ fileKey,
189
+ save,
190
+ read,
191
+ remove,
192
+ fingerprint,
193
+ encode,
194
+ decode,
195
+ importEncoded,
196
+ importFromSearch,
197
+ };
198
+ }(window));
@@ -23,7 +23,7 @@
23
23
  });
24
24
  },
25
25
 
26
- sendInput(agentId, input) {
26
+ sendTerminalInput(agentId, input) {
27
27
  return send({
28
28
  type: 'input',
29
29
  agentId,
@@ -56,6 +56,13 @@
56
56
  });
57
57
  },
58
58
 
59
+ clearTerminal(agentId) {
60
+ return send({
61
+ type: 'clear-terminal',
62
+ agentId,
63
+ });
64
+ },
65
+
59
66
  killAgent(agentId) {
60
67
  return send({
61
68
  type: 'kill-agent',
@@ -63,11 +70,13 @@
63
70
  });
64
71
  },
65
72
 
66
- async getSessionView(agentId) {
73
+ async getSessionView(agentId, options = {}) {
67
74
  const path = global.FarmingRuntimePaths
68
75
  ? global.FarmingRuntimePaths.apiPath(`/agents/${agentId}/session-view`)
69
76
  : `/api/agents/${agentId}/session-view`;
70
- const response = await fetchImpl(path);
77
+ const response = await fetchImpl(path, {
78
+ ...(options.signal ? { signal: options.signal } : {}),
79
+ });
71
80
  if (!response.ok) {
72
81
  throw new Error(`Failed to load session view: ${response.status}`);
73
82
  }
@@ -22,8 +22,8 @@
22
22
  };
23
23
  }
24
24
 
25
- function shouldPollSessionView(sessionSource) {
26
- return sessionSource === 'live-text';
25
+ function shouldPollSessionView(_sessionSource) {
26
+ return false;
27
27
  }
28
28
 
29
29
  function getDomState(documentRef) {
@@ -96,7 +96,7 @@
96
96
  return;
97
97
  }
98
98
 
99
- if (fitAddon && fitAddon.fit) {
99
+ if (fitAddon && fitAddon.fit && options.authoritativeGeometry !== true) {
100
100
  fitAddon.fit();
101
101
  }
102
102
 
@@ -284,15 +284,8 @@
284
284
 
285
285
  startPolling(context = {}) {
286
286
  this.stopPolling();
287
- if (!options.schedulePoll || !options.refreshSessionView) {
288
- return null;
289
- }
290
-
291
- poller = options.schedulePoll(() => {
292
- options.refreshSessionView(false, context.agentId, context.sessionToken);
293
- });
294
- syncPoller();
295
- return poller;
287
+ void context;
288
+ return null;
296
289
  },
297
290
 
298
291
  stopPolling() {