aegis-desktop 1.0.13 → 1.0.15
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/SOURCE-OFFER.txt +9 -0
- package/bin/aegis.js +1 -32
- package/lib/account-key.js +1 -56
- package/lib/auto.js +1 -430
- package/lib/avatar/companion.js +1 -698
- package/lib/avatar/cosmetics.js +1 -532
- package/lib/avatar/events.js +1 -344
- package/lib/avatar/holder-turn.js +1 -350
- package/lib/avatar/holders.js +1 -612
- package/lib/avatar/identity.js +1 -641
- package/lib/avatar/level.js +1 -368
- package/lib/avatar/mirror.js +1 -524
- package/lib/avatar/persona.js +1 -388
- package/lib/avatar/profile.js +1 -977
- package/lib/avatar/register.js +1 -307
- package/lib/avatar/store.js +1 -472
- package/lib/avatar/tiers.js +1 -38
- package/lib/avatar/transfer.js +1 -415
- package/lib/avatar/turn.js +1 -216
- package/lib/avatar/voice.js +1 -328
- package/lib/avatar/xp.js +1 -404
- package/lib/deep-link.js +1 -120
- package/lib/fingerprint.js +2 -347
- package/lib/google-connect.js +1 -169
- package/lib/local/agents.js +1 -105
- package/lib/local/autonomous.js +1 -1045
- package/lib/local/claude-pro.js +1 -993
- package/lib/local/context.js +1 -95
- package/lib/local/engine.js +1 -3228
- package/lib/local/entitlement.js +1 -654
- package/lib/local/git-scope.js +1 -228
- package/lib/local/local.js +1 -2135
- package/lib/local/prompt.js +1 -275
- package/lib/local/queue.js +1 -560
- package/lib/local/session-rounds.js +1 -208
- package/lib/local/shell.js +1 -680
- package/lib/local/telemetry.js +1 -470
- package/lib/local/tools.js +1 -1238
- package/lib/local/turn-guard.js +1 -300
- package/lib/local/worktree-lock.js +1 -235
- package/lib/provider-key-mirror.js +1 -252
- package/lib/quick-launcher.js +1 -84
- package/lib/settings.js +1 -890
- package/lib/sync/memory-queue.js +1 -60
- package/lib/sync/persist-gate.js +1 -148
- package/lib/sync/sessions.js +1 -106
- package/lib/window-state.js +1 -66
- package/main.js +3 -4390
- package/package.json +3 -5
- package/preload.js +3 -628
- package/renderer/app.js +1 -6560
- package/renderer/avatar/assemble.js +1 -335
- package/renderer/avatar/avatar.js +1 -380
- package/renderer/avatar/cards.js +1 -141
- package/renderer/avatar/hud.js +1 -206
- package/renderer/avatar/machine.js +1 -213
- package/renderer/avatar/pane.js +1 -439
- package/renderer/avatar/parts.js +1 -437
- package/renderer/budget.js +1 -168
- package/renderer/companion.js +1 -223
- package/renderer/diff.js +1 -359
- package/renderer/diffview.js +1 -336
- package/renderer/fingerprint.js +3 -720
- package/renderer/markdown.js +1 -168
- package/renderer/provider-toolbar.js +1 -134
- package/renderer/providers.js +1 -259
- package/renderer/quick.js +1 -191
- package/renderer/stream-policy.js +1 -306
- package/renderer/transcript-view.js +1 -323
- package/renderer/usage.js +1 -1239
- package/vendor/aegis.js +1 -2054
- package/vendor/attest.js +1 -442
- package/vendor/authorship-key.js +1 -410
- package/vendor/auto-provision.js +1 -134
- package/vendor/brain-catalog.js +1 -268
- package/vendor/byok-catalog.js +1 -230
- package/vendor/credentials.js +1 -494
- package/vendor/device-fingerprint.js +1 -141
- package/vendor/drive-connect.js +1 -85
- package/vendor/drive-upload.js +1 -174
- package/vendor/env-file.js +1 -1273
- package/vendor/fingerprint.js +1 -1064
- package/vendor/foreign-memory.js +1 -669
- package/vendor/google-signin.js +1 -125
- package/vendor/hooks.js +1 -430
- package/vendor/model-discovery.js +1 -330
- package/vendor/platform.js +1 -937
- package/vendor/prefs.js +1 -351
- package/vendor/presence-constants.js +1 -107
- package/vendor/presence.js +1 -438
- package/vendor/record-vault.js +3 -1055
- package/vendor/session-store.js +1 -548
- package/vendor/topup.js +1 -297
- package/vendor/touch.js +1 -458
- package/vendor/update.js +1 -704
- package/vendor/user-models.js +1 -381
package/renderer/companion.js
CHANGED
|
@@ -1,225 +1,3 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
|
|
3
|
-
|
|
4
|
-
// Conveying a modified copy, or running one as a network service, owes the
|
|
5
|
-
// corresponding source under this licence (§5/§13) — see LICENSING.md.
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* companion.js — the one greeting the app writes to you, once a day.
|
|
10
|
-
*
|
|
11
|
-
* WHAT THIS IS NOT. It is not a notification, a pop-up, a badge, or anything
|
|
12
|
-
* that interrupts. It is one line in the sidebar that changes at most once per
|
|
13
|
-
* local day and can be dismissed for that day. A "daily reminder" that steals
|
|
14
|
-
* focus is a nag; the whole value here is that it is quiet enough to read.
|
|
15
|
-
*
|
|
16
|
-
* THE RULE THAT SHAPES EVERY MESSAGE: it may only say things this process
|
|
17
|
-
* actually knows. There is no engagement metric in this file, no invented
|
|
18
|
-
* streak, no "you usually work on Tuesdays". Every candidate message reads its
|
|
19
|
-
* facts from the `facts` object the caller passes, and a message whose facts
|
|
20
|
-
* are absent is not a candidate at all — it is never shown with a guess in the
|
|
21
|
-
* blank. That is why `pickMessage` returns `{ kind, ... }` rather than a
|
|
22
|
-
* pre-baked string: the copy is chosen by which facts exist, and each branch
|
|
23
|
-
* says exactly which ones it used.
|
|
24
|
-
*
|
|
25
|
-
* Determinism matters for the daily behaviour: the same local day and the same
|
|
26
|
-
* facts must select the same message, or a re-render (which happens on every
|
|
27
|
-
* status read) would flip the suggestion while the user is looking at it. The
|
|
28
|
-
* rotation between equally-valid tips is therefore seeded from the day key,
|
|
29
|
-
* never from Math.random().
|
|
30
|
-
*
|
|
31
|
-
* Pure: no DOM, no Electron, no fs, no clock of its own — `now` is passed in.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The local calendar day, as YYYY-MM-DD.
|
|
36
|
-
*
|
|
37
|
-
* Local, not UTC, on purpose: "a daily reminder" means the user's day. A UTC
|
|
38
|
-
* key would roll the message over at 01:00 or 02:00 for most of Europe, which
|
|
39
|
-
* is the middle of the evening for the people most likely to be running this.
|
|
40
|
-
*/
|
|
41
|
-
function dayKey(now) {
|
|
42
|
-
const d = now instanceof Date ? now : new Date(now == null ? Date.now() : now);
|
|
43
|
-
if (Number.isNaN(d.getTime())) return '';
|
|
44
|
-
const p = (n) => String(n).padStart(2, '0');
|
|
45
|
-
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Whole local days between two timestamps; null when either is unusable. */
|
|
49
|
-
function daysBetween(a, b) {
|
|
50
|
-
const t1 = a instanceof Date ? a.getTime() : Number(a);
|
|
51
|
-
const t2 = b instanceof Date ? b.getTime() : Number(b);
|
|
52
|
-
if (!Number.isFinite(t1) || !Number.isFinite(t2)) return null;
|
|
53
|
-
const d1 = new Date(t1); d1.setHours(0, 0, 0, 0);
|
|
54
|
-
const d2 = new Date(t2); d2.setHours(0, 0, 0, 0);
|
|
55
|
-
return Math.round((d2.getTime() - d1.getTime()) / 86400000);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Stable small hash, so tip rotation is deterministic per day. */
|
|
59
|
-
function hash(s) {
|
|
60
|
-
let h = 2166136261;
|
|
61
|
-
const str = String(s);
|
|
62
|
-
for (let i = 0; i < str.length; i += 1) {
|
|
63
|
-
h ^= str.charCodeAt(i);
|
|
64
|
-
h = Math.imul(h, 16777619);
|
|
65
|
-
}
|
|
66
|
-
return h >>> 0;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Tips that are TRUE OF THIS BUILD — each one names a real affordance. They are
|
|
71
|
-
* the fallback rotation once nothing needs attention; none of them is a "come
|
|
72
|
-
* back and use the app more" nudge, because that would be the app asking for
|
|
73
|
-
* the user's time rather than giving them something back.
|
|
74
|
-
*/
|
|
75
|
-
const TIPS = Object.freeze([
|
|
76
|
-
{ id: 'effort', text: 'Effort sizes the call — low for quick edits, high when the answer has to be complete.' },
|
|
77
|
-
{ id: 'tools', text: 'Turn on "confirm before running tools" in Tool approvals to see every command before it runs.' },
|
|
78
|
-
{ id: 'quick', text: 'The Quick Launcher shortcut opens a prompt from anywhere — no need to switch windows.' },
|
|
79
|
-
{ id: 'sessions', text: 'Sessions → Export writes the thread out as a file you keep, not something held in the app.' },
|
|
80
|
-
{ id: 'queue', text: 'Queued tasks run on their own class — they can stay on Aegis Cloud while you work locally.' },
|
|
81
|
-
{ id: 'memory', text: 'Memory entries are yours: the inspector shows the source and tags for every one.' },
|
|
82
|
-
{ id: 'local', text: 'Local model needs no key and no network — useful for anything you would rather not send anywhere.' },
|
|
83
|
-
]);
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Choose today's message.
|
|
87
|
-
*
|
|
88
|
-
* `facts` (all optional; each one absent merely removes its branch):
|
|
89
|
-
* keyConfigured {boolean} an AEGIS API key exists on this machine
|
|
90
|
-
* modelCount {number} how many models the current class listed
|
|
91
|
-
* lastTurnAt {number} epoch ms of the last finished turn, if any
|
|
92
|
-
* firstSeenAt {number} epoch ms this install first ran
|
|
93
|
-
* queuePending {number} unfinished queued tasks
|
|
94
|
-
* memoryCount {number} memory entries visible in the inspector
|
|
95
|
-
* providerNeedsKey {boolean} byok class has models but no provider key
|
|
96
|
-
* updatePending {string} version string of a downloaded update, if any
|
|
97
|
-
*
|
|
98
|
-
* Returns `{ id, kind, text, usedFacts }` — `kind` is 'action' when the message
|
|
99
|
-
* points at something concrete to do and 'tip' when it is just the rotation.
|
|
100
|
-
*/
|
|
101
|
-
function pickMessage(facts, now) {
|
|
102
|
-
const f = facts && typeof facts === 'object' ? facts : {};
|
|
103
|
-
const day = dayKey(now);
|
|
104
|
-
const used = [];
|
|
105
|
-
const pick = (id, kind, text, keys) => ({
|
|
106
|
-
id,
|
|
107
|
-
kind,
|
|
108
|
-
text,
|
|
109
|
-
day,
|
|
110
|
-
usedFacts: keys.filter((k) => f[k] !== undefined && f[k] !== null),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// 1. An update that is downloaded and waiting is the single most actionable
|
|
114
|
-
// thing the app can know: it is finished work sitting idle until restart.
|
|
115
|
-
if (typeof f.updatePending === 'string' && f.updatePending) {
|
|
116
|
-
return pick(
|
|
117
|
-
'update-ready',
|
|
118
|
-
'action',
|
|
119
|
-
`Update ${f.updatePending} is downloaded — restart when it suits you, it installs on quit.`,
|
|
120
|
-
['updatePending'],
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 2. No key at all: the app can barely do anything, and the remedy is one
|
|
125
|
-
// paste. Not an error message — a reminder is allowed to be the first
|
|
126
|
-
// place the user hears it, so it is phrased as an invitation.
|
|
127
|
-
if (f.keyConfigured === false) {
|
|
128
|
-
return pick(
|
|
129
|
-
'connect',
|
|
130
|
-
'action',
|
|
131
|
-
'Paste your AEGIS API key in Status to run turns on Aegis Cloud — or pick Local model below for no key at all.',
|
|
132
|
-
['keyConfigured'],
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 3. A provider class chosen with models listed but no provider key: the user
|
|
137
|
-
// has already walked to the door and has not opened it.
|
|
138
|
-
if (f.providerNeedsKey === true) {
|
|
139
|
-
return pick(
|
|
140
|
-
'provider-key',
|
|
141
|
-
'action',
|
|
142
|
-
'Your provider class has models listed but no key saved — add one in Provider settings to use them.',
|
|
143
|
-
['providerNeedsKey'],
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 4. Queued work still waiting. Counted, never estimated: "3 queued" is a
|
|
148
|
-
// fact, "your queue is busy" would be a guess about progress.
|
|
149
|
-
if (typeof f.queuePending === 'number' && f.queuePending > 0) {
|
|
150
|
-
return pick(
|
|
151
|
-
'queue',
|
|
152
|
-
'action',
|
|
153
|
-
`${f.queuePending} queued task${f.queuePending === 1 ? '' : 's'} still waiting — the queue card runs ${f.queuePending === 1 ? 'it' : 'them'} on its own class.`,
|
|
154
|
-
['queuePending'],
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// 4b. Memory this install already holds. Documented in the facts list from
|
|
159
|
-
// the day this file was written, but with no branch behind it — so a user
|
|
160
|
-
// with twenty saved entries was told nothing about them and got a generic
|
|
161
|
-
// tip instead. Counted exactly, and it points at the inspector, which is
|
|
162
|
-
// the only place in the app where the provenance of each entry is shown.
|
|
163
|
-
if (typeof f.memoryCount === 'number' && f.memoryCount > 0) {
|
|
164
|
-
return pick(
|
|
165
|
-
'memory-held',
|
|
166
|
-
'action',
|
|
167
|
-
`${f.memoryCount} memory entr${f.memoryCount === 1 ? 'y is' : 'ies are'} already saved here — the inspector shows the source and tags for every one.`,
|
|
168
|
-
['memoryCount'],
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// 5. First-run, no key required path: say what the app is, once. Only on the
|
|
173
|
-
// FIRST local day, so it does not become a permanent fixture.
|
|
174
|
-
const age = daysBetween(f.firstSeenAt, now);
|
|
175
|
-
if (age === 0 && f.lastTurnAt == null) {
|
|
176
|
-
return pick(
|
|
177
|
-
'first-day',
|
|
178
|
-
'tip',
|
|
179
|
-
'First day. Pick a class and a model in the Model card, then send a turn — everything else here is optional.',
|
|
180
|
-
['firstSeenAt', 'lastTurnAt'],
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// 6. A gap after real use. Reported only when there is a genuine last turn,
|
|
185
|
-
// and only past a threshold where the observation is worth stating; below
|
|
186
|
-
// it, nothing is said at all (fall through to the rotation).
|
|
187
|
-
const since = daysBetween(f.lastTurnAt, now);
|
|
188
|
-
if (since != null && since >= 3) {
|
|
189
|
-
return pick(
|
|
190
|
-
'returning',
|
|
191
|
-
'tip',
|
|
192
|
-
'Last turn was a few days ago — your sessions and queued tasks are still here.',
|
|
193
|
-
['lastTurnAt'],
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 7. Nothing needs attention: rotate one true tip, seeded by the day so it is
|
|
198
|
-
// stable for the whole day and different tomorrow.
|
|
199
|
-
const tip = TIPS[hash(day) % TIPS.length];
|
|
200
|
-
return pick(`tip-${tip.id}`, 'tip', tip.text, ['tipRotation']);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** True when this message was already dismissed today. */
|
|
204
|
-
function dismissedToday(dismissed, now) {
|
|
205
|
-
const d = dismissed && typeof dismissed === 'object' ? dismissed : null;
|
|
206
|
-
if (!d) return false;
|
|
207
|
-
return d.day === dayKey(now);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
211
|
-
module.exports = { pickMessage, dayKey, daysBetween, dismissedToday, TIPS };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Also a page global, because index.html loads this file as a classic script
|
|
215
|
-
// (`<script src="companion.js">`) and app.js reads window.aegisCompanion. The
|
|
216
|
-
// module branch above is for Node (test/renderer-providers.test.mjs requires
|
|
217
|
-
// the file directly); the two are mutually exclusive in practice — a classic
|
|
218
|
-
// script has no `module` — so this cannot clobber the module export.
|
|
219
|
-
if (typeof window !== 'undefined') {
|
|
220
|
-
(window.aegisCompanion = window.aegisCompanion || {}).pickMessage = pickMessage;
|
|
221
|
-
window.aegisCompanion.dayKey = dayKey;
|
|
222
|
-
window.aegisCompanion.daysBetween = daysBetween;
|
|
223
|
-
window.aegisCompanion.dismissedToday = dismissedToday;
|
|
224
|
-
window.aegisCompanion.TIPS = TIPS;
|
|
225
|
-
}
|
|
3
|
+
"use strict";function dayKey(e){const t=e instanceof Date?e:new Date(null==e?Date.now():e);if(Number.isNaN(t.getTime()))return"";const n=e=>String(e).padStart(2,"0");return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())}`}function daysBetween(e,t){const n=e instanceof Date?e.getTime():Number(e),o=t instanceof Date?t.getTime():Number(t);if(!Number.isFinite(n)||!Number.isFinite(o))return null;const s=new Date(n);s.setHours(0,0,0,0);const i=new Date(o);return i.setHours(0,0,0,0),Math.round((i.getTime()-s.getTime())/864e5)}function hash(e){let t=2166136261;const n=String(e);for(let e=0;e<n.length;e+=1)t^=n.charCodeAt(e),t=Math.imul(t,16777619);return t>>>0}const TIPS=Object.freeze([{id:"effort",text:"Effort sizes the call — low for quick edits, high when the answer has to be complete."},{id:"tools",text:'Turn on "confirm before running tools" in Tool approvals to see every command before it runs.'},{id:"quick",text:"The Quick Launcher shortcut opens a prompt from anywhere — no need to switch windows."},{id:"sessions",text:"Sessions → Export writes the thread out as a file you keep, not something held in the app."},{id:"queue",text:"Queued tasks run on their own class — they can stay on Aegis Cloud while you work locally."},{id:"memory",text:"Memory entries are yours: the inspector shows the source and tags for every one."},{id:"local",text:"Local model needs no key and no network — useful for anything you would rather not send anywhere."}]);function pickMessage(e,t){const n=e&&"object"==typeof e?e:{},o=dayKey(t),s=(e,t,s,i)=>({id:e,kind:t,text:s,day:o,usedFacts:i.filter((e=>void 0!==n[e]&&null!==n[e]))});if("string"==typeof n.updatePending&&n.updatePending)return s("update-ready","action",`Update ${n.updatePending} is downloaded — restart when it suits you, it installs on quit.`,["updatePending"]);if(!1===n.keyConfigured)return s("connect","action","Paste your AEGIS API key in Status to run turns on Aegis Cloud — or pick Local model below for no key at all.",["keyConfigured"]);if(!0===n.providerNeedsKey)return s("provider-key","action","Your provider class has models listed but no key saved — add one in Provider settings to use them.",["providerNeedsKey"]);if("number"==typeof n.queuePending&&n.queuePending>0)return s("queue","action",`${n.queuePending} queued task${1===n.queuePending?"":"s"} still waiting — the queue card runs ${1===n.queuePending?"it":"them"} on its own class.`,["queuePending"]);if("number"==typeof n.memoryCount&&n.memoryCount>0)return s("memory-held","action",`${n.memoryCount} memory entr${1===n.memoryCount?"y is":"ies are"} already saved here — the inspector shows the source and tags for every one.`,["memoryCount"]);if(0===daysBetween(n.firstSeenAt,t)&&null==n.lastTurnAt)return s("first-day","tip","First day. Pick a class and a model in the Model card, then send a turn — everything else here is optional.",["firstSeenAt","lastTurnAt"]);const i=daysBetween(n.lastTurnAt,t);if(null!=i&&i>=3)return s("returning","tip","Last turn was a few days ago — your sessions and queued tasks are still here.",["lastTurnAt"]);const r=TIPS[hash(o)%TIPS.length];return s(`tip-${r.id}`,"tip",r.text,["tipRotation"])}function dismissedToday(e,t){const n=e&&"object"==typeof e?e:null;return!!n&&n.day===dayKey(t)}"undefined"!=typeof module&&module.exports&&(module.exports={pickMessage:pickMessage,dayKey:dayKey,daysBetween:daysBetween,dismissedToday:dismissedToday,TIPS:TIPS}),"undefined"!=typeof window&&((window.aegisCompanion=window.aegisCompanion||{}).pickMessage=pickMessage,window.aegisCompanion.dayKey=dayKey,window.aegisCompanion.daysBetween=daysBetween,window.aegisCompanion.dismissedToday=dismissedToday,window.aegisCompanion.TIPS=TIPS);
|
package/renderer/diff.js
CHANGED
|
@@ -1,361 +1,3 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
|
|
3
|
-
|
|
4
|
-
// Conveying a modified copy, or running one as a network service, owes the
|
|
5
|
-
// corresponding source under this licence (§5/§13) — see LICENSING.md.
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Line-level diff for the transcript's edit blocks.
|
|
10
|
-
*
|
|
11
|
-
* An `editFile`/`writeFile` call used to render as a single gray row
|
|
12
|
-
* (" ⎿ → editFile src/hello.js ✓"): you could see that a file changed and
|
|
13
|
-
* never *what* changed — the one thing a coding session is actually about.
|
|
14
|
-
* This module turns the tool call's arguments into a real unified-style diff,
|
|
15
|
-
* which diffview.js paints as a collapsible colored block.
|
|
16
|
-
*
|
|
17
|
-
* Where the two sides come from:
|
|
18
|
-
* editFile — args.old_string → args.new_string. No I/O at all.
|
|
19
|
-
* writeFile — the file as it was on disk *before* the call ran, against
|
|
20
|
-
* args.content. app.js builds the preview when the tool's
|
|
21
|
-
* `phase: 'run'` event arrives, which is strictly before the
|
|
22
|
-
* executor writes, so the read is the pre-edit state.
|
|
23
|
-
*
|
|
24
|
-
* Pure: strings in, plain row objects out — no spans, no colors, no globals —
|
|
25
|
-
* so the whole thing is unit-testable with no TTY. The DOM painting lives in
|
|
26
|
-
* diffview.js, exactly the split render.js already keeps between structure and
|
|
27
|
-
* paint.
|
|
28
|
-
*
|
|
29
|
-
* Ported from `cli/src/diff.js`; the engine's tool names are
|
|
30
|
-
* `Edit`/`Write`/`MultiEdit`, this host's are `editFile`/`writeFile`, so
|
|
31
|
-
* `editPreview` accepts both spellings. `MultiEdit` has no counterpart here
|
|
32
|
-
* and is kept only so a newer engine frame cannot silently render nothing.
|
|
33
|
-
*
|
|
34
|
-
* `fs`/`path` are Node-only and the renderer is sandboxed (main.js sets
|
|
35
|
-
* contextIsolation:true, nodeIntegration:false, sandbox:true), so `require`
|
|
36
|
-
* may not exist at all when this loads as a classic <script>. The require is
|
|
37
|
-
* therefore guarded and the pre-edit read is injectable (`opts.readFile`):
|
|
38
|
-
* without either a Node fs or an injected reader a `Write` degrades to a
|
|
39
|
-
* create-style diff instead of throwing — a preview is decoration, and
|
|
40
|
-
* decoration must never break a turn.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
// Guarded so the same file loads both as a Node module (tests, CLI reuse) and
|
|
44
|
-
// as a sandboxed classic script, where `require` is simply absent.
|
|
45
|
-
let fs = null;
|
|
46
|
-
let path = null;
|
|
47
|
-
try {
|
|
48
|
-
if (typeof require === 'function') {
|
|
49
|
-
fs = require('node:fs');
|
|
50
|
-
path = require('node:path');
|
|
51
|
-
}
|
|
52
|
-
} catch {
|
|
53
|
-
fs = null; // sandboxed renderer: fall back to an injected readFile
|
|
54
|
-
path = null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Budgets. The preview is built synchronously inside the tool event, on the
|
|
58
|
-
// render path, so a 200 MB writeFile must not stall the window — the read is
|
|
59
|
-
// capped and the LCS table is bounded so a pathological pair of long files
|
|
60
|
-
// cannot allocate hundreds of megabytes. Past a budget the middle collapses to
|
|
61
|
-
// "remove everything, add everything", which is honest about being incomplete
|
|
62
|
-
// (`truncated: true` on the preview, `… N unchanged lines` in the block).
|
|
63
|
-
const DIFF_LIMITS = {
|
|
64
|
-
fileBytes: 256 * 1024, // bytes read from disk for writeFile
|
|
65
|
-
lcsCells: 250000, // n*m cap for the LCS table (~1 MB of Int32)
|
|
66
|
-
rows: 400, // diff rows kept per preview
|
|
67
|
-
context: 3, // unchanged lines kept around each change
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Split text into display lines. A trailing newline is a terminator, not an
|
|
72
|
-
* empty last line — otherwise every normal file diff ends in a phantom
|
|
73
|
-
* "- " / "+ " pair. CRLF is normalized so a Windows checkout doesn't render as
|
|
74
|
-
* "every line changed".
|
|
75
|
-
*/
|
|
76
|
-
function splitLines(text) {
|
|
77
|
-
const s = String(text == null ? '' : text).replace(/\r\n?/g, '\n');
|
|
78
|
-
if (s === '') return [];
|
|
79
|
-
const lines = s.split('\n');
|
|
80
|
-
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
81
|
-
return lines;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Unified-style diff of two strings.
|
|
86
|
-
*
|
|
87
|
-
* @returns {{rows: Array<{sign: string, text: string, no: number|null,
|
|
88
|
-
* marker?: boolean, note?: string}>, adds: number, dels: number,
|
|
89
|
-
* truncated: boolean}} `rows` carries context rows around each change run
|
|
90
|
-
* (DIFF_LIMITS.context lines), collapsed runs of unchanged lines as `marker`
|
|
91
|
-
* rows, and every changed line with a `no` for the gutter (old number on
|
|
92
|
-
* '-', new number otherwise).
|
|
93
|
-
*/
|
|
94
|
-
function lineDiff(oldText, newText) {
|
|
95
|
-
const a = splitLines(oldText);
|
|
96
|
-
const b = splitLines(newText);
|
|
97
|
-
let pre = 0;
|
|
98
|
-
while (pre < a.length && pre < b.length && a[pre] === b[pre]) pre++;
|
|
99
|
-
let post = 0;
|
|
100
|
-
while (post < a.length - pre && post < b.length - pre && a[a.length - 1 - post] === b[b.length - 1 - post]) post++;
|
|
101
|
-
|
|
102
|
-
const midA = a.slice(pre, a.length - post);
|
|
103
|
-
const midB = b.slice(pre, b.length - post);
|
|
104
|
-
const ops = lcsOps(midA, midB);
|
|
105
|
-
const truncated = ops === null;
|
|
106
|
-
// Nothing changed: say so with no rows at all. The prefix/suffix trim ate the
|
|
107
|
-
// whole file (pre === a.length, post === 0), so the context arithmetic below
|
|
108
|
-
// would otherwise paint "… N unchanged lines" plus three orphans over an
|
|
109
|
-
// identical pair — a marker claiming an omission that never happened. This
|
|
110
|
-
// guard is what keeps `lineDiff(x, x)` from rendering as a fake edit.
|
|
111
|
-
|
|
112
|
-
const body = ops || [
|
|
113
|
-
...midA.map((text) => ({ sign: '-', text })),
|
|
114
|
-
...midB.map((text) => ({ sign: '+', text })),
|
|
115
|
-
];
|
|
116
|
-
if (!body.some((op) => op.sign !== ' ')) return { rows: [], adds: 0, dels: 0, truncated };
|
|
117
|
-
|
|
118
|
-
const ctx = DIFF_LIMITS.context;
|
|
119
|
-
|
|
120
|
-
const rows = [];
|
|
121
|
-
let oldNo = 1;
|
|
122
|
-
let newNo = 1;
|
|
123
|
-
// Leading context: keep *all* of it when it fits, otherwise the last `ctx`
|
|
124
|
-
// lines plus a marker for the ones actually dropped. The engine's original
|
|
125
|
-
// condition (`ctxBeforeStart < pre`) fires whenever the change is preceded by
|
|
126
|
-
// any line at all, so an ordinary three-line edit grew a bogus
|
|
127
|
-
// "… 1 unchanged line" marker and a change deep in a file reported `ctx` as
|
|
128
|
-
// the number hidden rather than the real count. A marker means "something was
|
|
129
|
-
// omitted", so it may only appear when something was.
|
|
130
|
-
const leadStart = pre > ctx ? pre - ctx : 0;
|
|
131
|
-
const hiddenLead = leadStart;
|
|
132
|
-
if (hiddenLead > 0) {
|
|
133
|
-
rows.push({ sign: ' ', text: '', marker: true, note: `… ${hiddenLead} unchanged line${hiddenLead === 1 ? '' : 's'}` });
|
|
134
|
-
}
|
|
135
|
-
for (let i = leadStart; i < pre; i++) {
|
|
136
|
-
rows.push({ sign: ' ', text: a[i], no: newNo });
|
|
137
|
-
newNo++;
|
|
138
|
-
oldNo++;
|
|
139
|
-
}
|
|
140
|
-
for (const op of body) {
|
|
141
|
-
if (op.sign === ' ') {
|
|
142
|
-
rows.push({ sign: ' ', text: op.text, no: newNo });
|
|
143
|
-
newNo++;
|
|
144
|
-
oldNo++;
|
|
145
|
-
} else if (op.sign === '-') {
|
|
146
|
-
rows.push({ sign: '-', text: op.text, no: oldNo });
|
|
147
|
-
oldNo++;
|
|
148
|
-
} else {
|
|
149
|
-
rows.push({ sign: '+', text: op.text, no: newNo });
|
|
150
|
-
newNo++;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// Suffix context: the first `ctx` lines of the untouched tail, then a marker
|
|
154
|
-
// for whatever unchanged tail is left over. Both the index into `b` and the
|
|
155
|
-
// gutter number come from `b`'s tail, not `a`'s: the engine read
|
|
156
|
-
// `b[a.length - post + i]`, which is the wrong line — and the wrong numbering
|
|
157
|
-
// — for every edit that changes the line count.
|
|
158
|
-
const tailStart = b.length - post;
|
|
159
|
-
const tailCount = Math.min(post, ctx);
|
|
160
|
-
newNo = tailStart + 1;
|
|
161
|
-
for (let i = 0; i < tailCount; i++) {
|
|
162
|
-
rows.push({ sign: ' ', text: b[tailStart + i], no: newNo });
|
|
163
|
-
newNo++;
|
|
164
|
-
oldNo++;
|
|
165
|
-
}
|
|
166
|
-
if (post > ctx) {
|
|
167
|
-
const n = post - ctx;
|
|
168
|
-
rows.push({ sign: ' ', text: '', marker: true, note: `… ${n} unchanged line${n === 1 ? '' : 's'}` });
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const adds = rows.filter((r) => r.sign === '+').length;
|
|
172
|
-
const dels = rows.filter((r) => r.sign === '-').length;
|
|
173
|
-
let out = rows;
|
|
174
|
-
let cut = false;
|
|
175
|
-
if (out.length > DIFF_LIMITS.rows) {
|
|
176
|
-
out = out.slice(0, DIFF_LIMITS.rows);
|
|
177
|
-
out.push({ sign: ' ', text: '', marker: true, note: `… ${rows.length - DIFF_LIMITS.rows} more diff lines` });
|
|
178
|
-
cut = true;
|
|
179
|
-
}
|
|
180
|
-
return { rows: out, adds, dels, truncated: truncated || cut };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Longest-common-subsequence walk over two line arrays.
|
|
185
|
-
* @returns {Array<{sign: string, text: string}>|null} ops, or null when the
|
|
186
|
-
* table would exceed DIFF_LIMITS.lcsCells (the caller then treats the middle
|
|
187
|
-
* as a full replacement).
|
|
188
|
-
*/
|
|
189
|
-
function lcsOps(a, b) {
|
|
190
|
-
const n = a.length;
|
|
191
|
-
const m = b.length;
|
|
192
|
-
if (!n || !m) {
|
|
193
|
-
return [
|
|
194
|
-
...a.map((text) => ({ sign: '-', text })),
|
|
195
|
-
...b.map((text) => ({ sign: '+', text })),
|
|
196
|
-
];
|
|
197
|
-
}
|
|
198
|
-
if (n * m > DIFF_LIMITS.lcsCells) return null;
|
|
199
|
-
const w = m + 1;
|
|
200
|
-
const dp = new Int32Array((n + 1) * w); // dp[i*w+j] = |LCS(a[i:], b[j:])|
|
|
201
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
202
|
-
for (let j = m - 1; j >= 0; j--) {
|
|
203
|
-
dp[i * w + j] = a[i] === b[j]
|
|
204
|
-
? dp[(i + 1) * w + (j + 1)] + 1
|
|
205
|
-
: Math.max(dp[(i + 1) * w + j], dp[i * w + (j + 1)]);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
const ops = [];
|
|
209
|
-
let i = 0;
|
|
210
|
-
let j = 0;
|
|
211
|
-
while (i < n && j < m) {
|
|
212
|
-
if (a[i] === b[j]) { ops.push({ sign: ' ', text: a[i] }); i++; j++; }
|
|
213
|
-
else if (dp[(i + 1) * w + j] >= dp[i * w + (j + 1)]) { ops.push({ sign: '-', text: a[i] }); i++; }
|
|
214
|
-
else { ops.push({ sign: '+', text: b[j] }); j++; }
|
|
215
|
-
}
|
|
216
|
-
while (i < n) ops.push({ sign: '-', text: a[i++] });
|
|
217
|
-
while (j < m) ops.push({ sign: '+', text: b[j++] });
|
|
218
|
-
return ops;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Capped read for the pre-edit state. Never throws; missing/unreadable → null. */
|
|
222
|
-
function readCapped(file, opts = {}) {
|
|
223
|
-
// No Node fs (sandboxed renderer) → report missing so a Write degrades to a
|
|
224
|
-
// create-style diff rather than throwing on the render path.
|
|
225
|
-
if (!fs) return { text: null, missing: true };
|
|
226
|
-
const bytes = opts.bytes == null ? DIFF_LIMITS.fileBytes : opts.bytes;
|
|
227
|
-
try {
|
|
228
|
-
const st = fs.statSync(file);
|
|
229
|
-
if (!st.isFile()) return { text: null, missing: true };
|
|
230
|
-
const text = fs.readFileSync(file, 'utf8');
|
|
231
|
-
if (st.size > bytes) return { text: text.slice(0, bytes), missing: false, truncated: true };
|
|
232
|
-
return { text, missing: false };
|
|
233
|
-
} catch {
|
|
234
|
-
return { text: null, missing: true };
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** Path as the user thinks of it: relative to the session cwd when it is inside it. */
|
|
239
|
-
function shortPath(file, cwd) {
|
|
240
|
-
const f = String(file || '');
|
|
241
|
-
if (!cwd || !path) return f; // no node:path in the sandbox → show the raw path
|
|
242
|
-
const rel = path.relative(cwd, f);
|
|
243
|
-
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) return rel;
|
|
244
|
-
const parts = f.split(path.sep).filter(Boolean);
|
|
245
|
-
const base = f.startsWith(path.sep) ? path.sep : '';
|
|
246
|
-
return parts.length > 2 ? base + '…' + path.sep + parts.slice(-2).join(path.sep) : f;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Build the diff preview for a tool call, or null when the call isn't an edit
|
|
251
|
-
* or its arguments don't carry enough to diff (a partial stream, a malformed
|
|
252
|
-
* call). Never throws — a preview is decoration, not the tool result, so a bad
|
|
253
|
-
* argument object must not be able to break a turn.
|
|
254
|
-
*
|
|
255
|
-
* @param {string} name Tool name — this host's `editFile`/`writeFile`, or the
|
|
256
|
-
* engine's `Edit`/`Write`/`MultiEdit` (a vendored frame can carry either).
|
|
257
|
-
* @param {object|string} args Tool arguments as parsed from the stream.
|
|
258
|
-
* @param {{cwd?: string, readFile?: Function}} [opts] `readFile` is injectable
|
|
259
|
-
* so tests never touch disk and the sandboxed renderer can inject a bridge.
|
|
260
|
-
*/
|
|
261
|
-
function editPreview(name, args, opts = {}) {
|
|
262
|
-
try {
|
|
263
|
-
const cwd = opts.cwd == null ? (typeof process !== 'undefined' ? process.cwd() : '') : opts.cwd;
|
|
264
|
-
const readFile = opts.readFile || readCapped;
|
|
265
|
-
if (!args || typeof args !== 'object') return null;
|
|
266
|
-
const file = typeof args.file_path === 'string' ? args.file_path
|
|
267
|
-
: typeof args.path === 'string' ? args.path : null;
|
|
268
|
-
if (!file) return null;
|
|
269
|
-
const base = { file, short: shortPath(file, cwd) };
|
|
270
|
-
|
|
271
|
-
if (name === 'Edit' || name === 'editFile') {
|
|
272
|
-
if (typeof args.old_string !== 'string' || typeof args.new_string !== 'string') return null;
|
|
273
|
-
return Object.assign(
|
|
274
|
-
{ kind: 'Update' },
|
|
275
|
-
base,
|
|
276
|
-
lineDiff(args.old_string, args.new_string),
|
|
277
|
-
{ replaceAll: !!args.replace_all },
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (name === 'Write' || name === 'writeFile') {
|
|
282
|
-
if (typeof args.content !== 'string') return null;
|
|
283
|
-
const prev = readFile(file) || { text: null, missing: true };
|
|
284
|
-
const d = lineDiff(prev.text == null ? '' : prev.text, args.content);
|
|
285
|
-
return Object.assign(
|
|
286
|
-
{ kind: prev.missing ? 'Create' : 'Write' },
|
|
287
|
-
base,
|
|
288
|
-
d,
|
|
289
|
-
{ truncated: d.truncated || !!prev.truncated },
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (name === 'MultiEdit') {
|
|
294
|
-
const edits = (Array.isArray(args.edits) ? args.edits : []).filter(
|
|
295
|
-
(e) => e && typeof e.old_string === 'string' && typeof e.new_string === 'string',
|
|
296
|
-
);
|
|
297
|
-
if (!edits.length) return null;
|
|
298
|
-
const prev = readFile(file) || { text: null, missing: true };
|
|
299
|
-
if (typeof prev.text === 'string') {
|
|
300
|
-
let cur = prev.text;
|
|
301
|
-
let misses = 0;
|
|
302
|
-
for (const e of edits) {
|
|
303
|
-
if (cur.indexOf(e.old_string) === -1) { misses++; continue; }
|
|
304
|
-
cur = e.replace_all
|
|
305
|
-
? cur.split(e.old_string).join(e.new_string)
|
|
306
|
-
: cur.replace(e.old_string, e.new_string);
|
|
307
|
-
}
|
|
308
|
-
const d = lineDiff(prev.text, cur);
|
|
309
|
-
return Object.assign(
|
|
310
|
-
{ kind: 'Update' },
|
|
311
|
-
base,
|
|
312
|
-
d,
|
|
313
|
-
{ misses, replaceAll: edits.some((e) => e.replace_all) },
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
// No base on disk: stack each edit's own diff rather than inventing one.
|
|
317
|
-
let rows = [];
|
|
318
|
-
let adds = 0;
|
|
319
|
-
let dels = 0;
|
|
320
|
-
let truncated = false;
|
|
321
|
-
edits.forEach((e, i) => {
|
|
322
|
-
const d = lineDiff(e.old_string, e.new_string);
|
|
323
|
-
if (i) rows.push({ sign: ' ', text: '', marker: true, note: `… edit ${i + 1} of ${edits.length}` });
|
|
324
|
-
rows = rows.concat(d.rows);
|
|
325
|
-
adds += d.adds;
|
|
326
|
-
dels += d.dels;
|
|
327
|
-
truncated = truncated || d.truncated;
|
|
328
|
-
});
|
|
329
|
-
return Object.assign(
|
|
330
|
-
{ kind: 'Update' },
|
|
331
|
-
base,
|
|
332
|
-
{ rows, adds, dels, truncated, stacked: edits.length },
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return null;
|
|
337
|
-
} catch {
|
|
338
|
-
return null; // decoration must never break a turn
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/** Human count line in the reference's wording: "Added 3 lines, removed 1 line". */
|
|
343
|
-
function changeSummary(preview) {
|
|
344
|
-
const parts = [];
|
|
345
|
-
if (preview.adds) parts.push(`Added ${preview.adds} line${preview.adds === 1 ? '' : 's'}`);
|
|
346
|
-
if (preview.dels) parts.push(`removed ${preview.dels} line${preview.dels === 1 ? '' : 's'}`);
|
|
347
|
-
return parts.length ? parts.join(', ') : 'no line changes';
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
351
|
-
module.exports = {
|
|
352
|
-
DIFF_LIMITS,
|
|
353
|
-
splitLines,
|
|
354
|
-
lineDiff,
|
|
355
|
-
lcsOps,
|
|
356
|
-
readCapped,
|
|
357
|
-
shortPath,
|
|
358
|
-
changeSummary,
|
|
359
|
-
editPreview,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
3
|
+
"use strict";let fs=null,path=null;try{"function"==typeof require&&(fs=require("node:fs"),path=require("node:path"))}catch{fs=null,path=null}const DIFF_LIMITS={fileBytes:262144,lcsCells:25e4,rows:400,context:3};function splitLines(t){const e=String(null==t?"":t).replace(/\r\n?/g,"\n");if(""===e)return[];const n=e.split("\n");return n.length&&""===n[n.length-1]&&n.pop(),n}function lineDiff(t,e){const n=splitLines(t),s=splitLines(e);let i=0;for(;i<n.length&&i<s.length&&n[i]===s[i];)i++;let l=0;for(;l<n.length-i&&l<s.length-i&&n[n.length-1-l]===s[s.length-1-l];)l++;const r=n.slice(i,n.length-l),o=s.slice(i,s.length-l),a=lcsOps(r,o),c=null===a,u=a||[...r.map((t=>({sign:"-",text:t}))),...o.map((t=>({sign:"+",text:t})))];if(!u.some((t=>" "!==t.sign)))return{rows:[],adds:0,dels:0,truncated:c};const p=DIFF_LIMITS.context,f=[];let g=1,d=1;const h=i>p?i-p:0,x=h;x>0&&f.push({sign:" ",text:"",marker:!0,note:`… ${x} unchanged line${1===x?"":"s"}`});for(let t=h;t<i;t++)f.push({sign:" ",text:n[t],no:d}),d++,g++;for(const t of u)" "===t.sign?(f.push({sign:" ",text:t.text,no:d}),d++,g++):"-"===t.sign?(f.push({sign:"-",text:t.text,no:g}),g++):(f.push({sign:"+",text:t.text,no:d}),d++);const m=s.length-l,I=Math.min(l,p);d=m+1;for(let t=0;t<I;t++)f.push({sign:" ",text:s[m+t],no:d}),d++,g++;if(l>p){const t=l-p;f.push({sign:" ",text:"",marker:!0,note:`… ${t} unchanged line${1===t?"":"s"}`})}const y=f.filter((t=>"+"===t.sign)).length,_=f.filter((t=>"-"===t.sign)).length;let F=f,w=!1;return F.length>DIFF_LIMITS.rows&&(F=F.slice(0,DIFF_LIMITS.rows),F.push({sign:" ",text:"",marker:!0,note:`… ${f.length-DIFF_LIMITS.rows} more diff lines`}),w=!0),{rows:F,adds:y,dels:_,truncated:c||w}}function lcsOps(t,e){const n=t.length,s=e.length;if(!n||!s)return[...t.map((t=>({sign:"-",text:t}))),...e.map((t=>({sign:"+",text:t})))];if(n*s>DIFF_LIMITS.lcsCells)return null;const i=s+1,l=new Int32Array((n+1)*i);for(let r=n-1;r>=0;r--)for(let n=s-1;n>=0;n--)l[r*i+n]=t[r]===e[n]?l[(r+1)*i+(n+1)]+1:Math.max(l[(r+1)*i+n],l[r*i+(n+1)]);const r=[];let o=0,a=0;for(;o<n&&a<s;)t[o]===e[a]?(r.push({sign:" ",text:t[o]}),o++,a++):l[(o+1)*i+a]>=l[o*i+(a+1)]?(r.push({sign:"-",text:t[o]}),o++):(r.push({sign:"+",text:e[a]}),a++);for(;o<n;)r.push({sign:"-",text:t[o++]});for(;a<s;)r.push({sign:"+",text:e[a++]});return r}function readCapped(t,e={}){if(!fs)return{text:null,missing:!0};const n=null==e.bytes?DIFF_LIMITS.fileBytes:e.bytes;try{const e=fs.statSync(t);if(!e.isFile())return{text:null,missing:!0};const s=fs.readFileSync(t,"utf8");return e.size>n?{text:s.slice(0,n),missing:!1,truncated:!0}:{text:s,missing:!1}}catch{return{text:null,missing:!0}}}function shortPath(t,e){const n=String(t||"");if(!e||!path)return n;const s=path.relative(e,n);if(s&&!s.startsWith("..")&&!path.isAbsolute(s))return s;const i=n.split(path.sep).filter(Boolean),l=n.startsWith(path.sep)?path.sep:"";return i.length>2?l+"…"+path.sep+i.slice(-2).join(path.sep):n}function editPreview(t,e,n={}){try{const s=null==n.cwd?"undefined"!=typeof process?process.cwd():"":n.cwd,i=n.readFile||readCapped;if(!e||"object"!=typeof e)return null;const l="string"==typeof e.file_path?e.file_path:"string"==typeof e.path?e.path:null;if(!l)return null;const r={file:l,short:shortPath(l,s)};if("Edit"===t||"editFile"===t)return"string"!=typeof e.old_string||"string"!=typeof e.new_string?null:Object.assign({kind:"Update"},r,lineDiff(e.old_string,e.new_string),{replaceAll:!!e.replace_all});if("Write"===t||"writeFile"===t){if("string"!=typeof e.content)return null;const t=i(l)||{text:null,missing:!0},n=lineDiff(null==t.text?"":t.text,e.content);return Object.assign({kind:t.missing?"Create":"Write"},r,n,{truncated:n.truncated||!!t.truncated})}if("MultiEdit"===t){const t=(Array.isArray(e.edits)?e.edits:[]).filter((t=>t&&"string"==typeof t.old_string&&"string"==typeof t.new_string));if(!t.length)return null;const n=i(l)||{text:null,missing:!0};if("string"==typeof n.text){let e=n.text,s=0;for(const n of t)-1!==e.indexOf(n.old_string)?e=n.replace_all?e.split(n.old_string).join(n.new_string):e.replace(n.old_string,n.new_string):s++;const i=lineDiff(n.text,e);return Object.assign({kind:"Update"},r,i,{misses:s,replaceAll:t.some((t=>t.replace_all))})}let s=[],o=0,a=0,c=!1;return t.forEach(((e,n)=>{const i=lineDiff(e.old_string,e.new_string);n&&s.push({sign:" ",text:"",marker:!0,note:`… edit ${n+1} of ${t.length}`}),s=s.concat(i.rows),o+=i.adds,a+=i.dels,c=c||i.truncated})),Object.assign({kind:"Update"},r,{rows:s,adds:o,dels:a,truncated:c,stacked:t.length})}return null}catch{return null}}function changeSummary(t){const e=[];return t.adds&&e.push(`Added ${t.adds} line${1===t.adds?"":"s"}`),t.dels&&e.push(`removed ${t.dels} line${1===t.dels?"":"s"}`),e.length?e.join(", "):"no line changes"}"undefined"!=typeof module&&module.exports&&(module.exports={DIFF_LIMITS:DIFF_LIMITS,splitLines:splitLines,lineDiff:lineDiff,lcsOps:lcsOps,readCapped:readCapped,shortPath:shortPath,changeSummary:changeSummary,editPreview:editPreview});
|