@qilin-agent/client-ui-deliverables 0.0.0-stage → 3.1.4
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/LICENSE +21 -0
- package/README.i18n.yaml +58 -0
- package/README.md +121 -2
- package/README.zh.md +122 -0
- package/lib/client.js +2234 -0
- package/lib/index.js +290 -0
- package/lib/types/changes.d.ts +122 -0
- package/lib/types/client/ChangedFiles.d.ts +19 -0
- package/lib/types/client/Deliverables.d.ts +50 -0
- package/lib/types/client/PresentRow.d.ts +12 -0
- package/lib/types/client/PresentedFileCard.d.ts +19 -0
- package/lib/types/client/ReviewTab.d.ts +77 -0
- package/lib/types/client/changes-diff.d.ts +22 -0
- package/lib/types/client/changes-summary.d.ts +18 -0
- package/lib/types/client/host-read-store.d.ts +42 -0
- package/lib/types/client/icons.d.ts +8 -0
- package/lib/types/client/index.d.ts +26 -0
- package/lib/types/client/locales.d.ts +79 -0
- package/lib/types/client/present-open.d.ts +46 -0
- package/lib/types/client/review-definition.d.ts +33 -0
- package/lib/types/client/review-store.d.ts +39 -0
- package/lib/types/client/turn-deliverables.d.ts +87 -0
- package/lib/types/index.d.ts +15 -0
- package/lib/types/present-open.d.ts +10 -0
- package/lib/types/presented.d.ts +60 -0
- package/package.json +90 -3
package/lib/client.js
ADDED
|
@@ -0,0 +1,2234 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@qilin-agent/client-ui-deliverables",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _qilin_agent_client_store = require("@qilin-agent/client-store");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let react = require("react");
|
|
10
|
+
let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
|
|
11
|
+
//#region lib/types/changes.js
|
|
12
|
+
/** Authenticated GET route serving one announced change summary while its Session lives. */
|
|
13
|
+
const CHANGED_FILES_PATH = "/api/changes.summary";
|
|
14
|
+
/** Authenticated GET route serving one listed file's turn-start and turn-end comparison while its Session lives. */
|
|
15
|
+
const CHANGES_DIFF_PATH = "/api/changes.diff";
|
|
16
|
+
/** Authenticated GET route serving the Session's whole changed-file list while its Session lives. */
|
|
17
|
+
const CHANGES_SESSION_PATH = "/api/changes.session";
|
|
18
|
+
/** Authenticated POST route for opening a changed file on the Host desktop. */
|
|
19
|
+
const CHANGES_OPEN_PATH = "/api/changes.open";
|
|
20
|
+
/**
|
|
21
|
+
* Browser-relative form of {@link CHANGED_FILES_PATH}; see
|
|
22
|
+
* .agents/notes/implemented/architecture/2026-09-14-web-document-relative-app-routes.md.
|
|
23
|
+
*/
|
|
24
|
+
const CHANGED_FILES_ROUTE = CHANGED_FILES_PATH.slice(1);
|
|
25
|
+
/** Browser-relative form of {@link CHANGES_DIFF_PATH}. */
|
|
26
|
+
const CHANGES_DIFF_ROUTE = CHANGES_DIFF_PATH.slice(1);
|
|
27
|
+
CHANGES_SESSION_PATH.slice(1);
|
|
28
|
+
/** Browser-relative form of {@link CHANGES_OPEN_PATH}. */
|
|
29
|
+
const CHANGES_OPEN_ROUTE = CHANGES_OPEN_PATH.slice(1);
|
|
30
|
+
/** Resource-address prefix of a turn's review tab in the right Sidebar. */
|
|
31
|
+
const CHANGES_REVIEW_ADDRESS = "qilin-resource://changes-review/session/";
|
|
32
|
+
function isRecord$1(value) {
|
|
33
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate one changed-file record read from the summary route.
|
|
37
|
+
* @param value - decoded JSON.
|
|
38
|
+
* @returns whether the record carries a path, a display path, and line counts.
|
|
39
|
+
*/
|
|
40
|
+
function isChangedFile(value) {
|
|
41
|
+
if (!isRecord$1(value)) return false;
|
|
42
|
+
const { path, display, added, deleted, binary, oversized } = value;
|
|
43
|
+
return typeof path === "string" && path.length > 0 && typeof display === "string" && display.length > 0 && Number.isSafeInteger(added) && Number.isSafeInteger(deleted) && (binary === void 0 || binary === true) && (oversized === void 0 || oversized === true);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate a summary read from the summary route.
|
|
47
|
+
* @param value - decoded JSON.
|
|
48
|
+
* @returns whether the value identifies a turn, a complete file list, the total count, and the line totals.
|
|
49
|
+
*/
|
|
50
|
+
function isChangesSummary(value) {
|
|
51
|
+
if (!isRecord$1(value)) return false;
|
|
52
|
+
const { turn, files, total, added, deleted } = value;
|
|
53
|
+
return Number.isSafeInteger(turn) && turn >= 1 && Number.isSafeInteger(total) && Number.isSafeInteger(added) && Number.isSafeInteger(deleted) && Array.isArray(files) && files.every(isChangedFile);
|
|
54
|
+
}
|
|
55
|
+
function isHunk(value) {
|
|
56
|
+
if (!isRecord$1(value)) return false;
|
|
57
|
+
const { oldStart, oldLines, newStart, newLines, lines } = value;
|
|
58
|
+
return [
|
|
59
|
+
oldStart,
|
|
60
|
+
oldLines,
|
|
61
|
+
newStart,
|
|
62
|
+
newLines
|
|
63
|
+
].every((field) => Number.isSafeInteger(field) && field >= 0) && Array.isArray(lines) && lines.every((line) => typeof line === "string" && /^[+ -]/.test(line));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Validate a comparison read from the comparison route.
|
|
67
|
+
* @param value - decoded JSON.
|
|
68
|
+
* @returns whether the value is a text comparison with well-formed hunks, or a binary or oversized refusal.
|
|
69
|
+
*/
|
|
70
|
+
function isChangesDiff(value) {
|
|
71
|
+
if (!isRecord$1(value)) return false;
|
|
72
|
+
const { kind, path, display } = value;
|
|
73
|
+
if (typeof path !== "string" || path.length === 0 || typeof display !== "string" || display.length === 0) return false;
|
|
74
|
+
if (kind === "binary" || kind === "oversized") return true;
|
|
75
|
+
if (kind !== "text") return false;
|
|
76
|
+
const { before, after, hunks, coarse } = value;
|
|
77
|
+
return typeof before === "boolean" && typeof after === "boolean" && typeof coarse === "boolean" && Array.isArray(hunks) && hunks.every(isHunk);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Validate the `workspace/changes` event data read from a Session log.
|
|
81
|
+
* @param value - decoded durable event data.
|
|
82
|
+
* @returns whether the event names a turn.
|
|
83
|
+
*/
|
|
84
|
+
function isChangesEvent(value) {
|
|
85
|
+
return isRecord$1(value) && Number.isSafeInteger(value.turn) && value.turn >= 1;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build authenticated coordinates for the summary one `workspace/changes` event announced.
|
|
89
|
+
* @param sessionId - owning Session.
|
|
90
|
+
* @param seq - event sequence.
|
|
91
|
+
* @returns document-relative summary route.
|
|
92
|
+
*/
|
|
93
|
+
function changesSummaryUrl(sessionId, seq) {
|
|
94
|
+
return `${CHANGED_FILES_ROUTE}?${new URLSearchParams({
|
|
95
|
+
sessionId,
|
|
96
|
+
seq: String(seq)
|
|
97
|
+
})}`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Build authenticated coordinates for one listed file's comparison.
|
|
101
|
+
* @param sessionId - owning Session.
|
|
102
|
+
* @param seq - workspace/changes event sequence.
|
|
103
|
+
* @param index - original index in the summary's files array.
|
|
104
|
+
* @returns document-relative comparison route.
|
|
105
|
+
*/
|
|
106
|
+
function changesDiffUrl(sessionId, seq, index) {
|
|
107
|
+
return `${CHANGES_DIFF_ROUTE}?${new URLSearchParams({
|
|
108
|
+
sessionId,
|
|
109
|
+
seq: String(seq),
|
|
110
|
+
index: String(index)
|
|
111
|
+
})}`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build authenticated coordinates for a changed file's native open.
|
|
115
|
+
* @param sessionId - owning Session.
|
|
116
|
+
* @param seq - workspace/changes event sequence.
|
|
117
|
+
* @param index - original index in the summary's files array.
|
|
118
|
+
* @returns document-relative action route.
|
|
119
|
+
*/
|
|
120
|
+
function changedFileUrl(sessionId, seq, index) {
|
|
121
|
+
return `${CHANGES_OPEN_ROUTE}?${new URLSearchParams({
|
|
122
|
+
sessionId,
|
|
123
|
+
seq: String(seq),
|
|
124
|
+
index: String(index)
|
|
125
|
+
})}`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The right-Sidebar address of one turn's review. The Session and the event
|
|
129
|
+
* sequence identify the content; the turn rides along for the tab title.
|
|
130
|
+
* @param coordinates - viewed Session, announcing event, and turn.
|
|
131
|
+
* @returns a `qilin-resource://changes-review/session/…` address.
|
|
132
|
+
*/
|
|
133
|
+
function changesReviewAddress(coordinates) {
|
|
134
|
+
const { sessionId, seq, turn } = coordinates;
|
|
135
|
+
return `${CHANGES_REVIEW_ADDRESS}${encodeURIComponent(sessionId)}/${seq}/${turn}`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Read the coordinates back out of a review address.
|
|
139
|
+
* @param address - a resource address.
|
|
140
|
+
* @returns the coordinates, or undefined for any other address.
|
|
141
|
+
*/
|
|
142
|
+
function parseChangesReviewAddress(address) {
|
|
143
|
+
if (!address.startsWith("qilin-resource://changes-review/session/")) return void 0;
|
|
144
|
+
const parts = address.slice(40).split("/");
|
|
145
|
+
if (parts.length !== 3) return void 0;
|
|
146
|
+
const [sessionId, seq, turn] = parts;
|
|
147
|
+
if (sessionId === "" || !/^\d+$/.test(seq) || !/^[1-9]\d*$/.test(turn)) return void 0;
|
|
148
|
+
try {
|
|
149
|
+
return {
|
|
150
|
+
sessionId: decodeURIComponent(sessionId),
|
|
151
|
+
seq: Number(seq),
|
|
152
|
+
turn: Number(turn)
|
|
153
|
+
};
|
|
154
|
+
} catch {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region lib/types/client/host-read-store.js
|
|
160
|
+
/**
|
|
161
|
+
* Fetch-once cache of Host-served records keyed by their authenticated URL:
|
|
162
|
+
* one read per URL while a state stands, cleared on connection replacement,
|
|
163
|
+
* cancelled on disposal. Each store decides what a response means and which
|
|
164
|
+
* states a later request reads again.
|
|
165
|
+
*/
|
|
166
|
+
/** One browser plugin's reads of one record kind. */
|
|
167
|
+
var HostReadStore = class {
|
|
168
|
+
policy;
|
|
169
|
+
/** Record URLs key the state across Sessions and turns. */
|
|
170
|
+
state = (0, _qilin_agent_client_store.createSnapshotStore)({});
|
|
171
|
+
lifetime = new AbortController();
|
|
172
|
+
/** The connection generation the current states belong to; a reset aborts it so no older read publishes. */
|
|
173
|
+
generation = new AbortController();
|
|
174
|
+
pending = /* @__PURE__ */ new Set();
|
|
175
|
+
constructor(policy) {
|
|
176
|
+
this.policy = policy;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Read one URL unless a state the policy keeps already stands for it.
|
|
180
|
+
* @param url - the record's authenticated URL.
|
|
181
|
+
* @returns after the state is published.
|
|
182
|
+
*/
|
|
183
|
+
async loadUrl(url) {
|
|
184
|
+
const current = this.state.getSnapshot()[url];
|
|
185
|
+
if (this.lifetime.signal.aborted || current !== void 0 && !this.policy.retryable(current)) return;
|
|
186
|
+
this.state.update((state) => {
|
|
187
|
+
state[url] = this.policy.loading;
|
|
188
|
+
});
|
|
189
|
+
const task = this.read(url, AbortSignal.any([this.lifetime.signal, this.generation.signal]));
|
|
190
|
+
this.pending.add(task);
|
|
191
|
+
try {
|
|
192
|
+
await task;
|
|
193
|
+
} finally {
|
|
194
|
+
this.pending.delete(task);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Forget every state and abandon in-flight reads; a replaced connection may reach a Host that no longer serves them. */
|
|
198
|
+
reset() {
|
|
199
|
+
this.generation.abort();
|
|
200
|
+
this.generation = new AbortController();
|
|
201
|
+
this.state.set({});
|
|
202
|
+
}
|
|
203
|
+
/** Cancel outstanding reads and wait until none can publish state. */
|
|
204
|
+
async dispose() {
|
|
205
|
+
this.lifetime.abort();
|
|
206
|
+
await Promise.all(this.pending);
|
|
207
|
+
}
|
|
208
|
+
async read(url, signal) {
|
|
209
|
+
let next;
|
|
210
|
+
try {
|
|
211
|
+
next = await this.policy.decode(await fetch(url, { signal }));
|
|
212
|
+
} catch {
|
|
213
|
+
next = this.policy.failed;
|
|
214
|
+
}
|
|
215
|
+
if (!signal.aborted) this.state.update((state) => {
|
|
216
|
+
state[url] = next;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region lib/types/client/changes-diff.js
|
|
222
|
+
/** One browser plugin's comparison reads; a failed read is the one state a later request replaces. */
|
|
223
|
+
var ChangesDiffStore = class extends HostReadStore {
|
|
224
|
+
constructor() {
|
|
225
|
+
super({
|
|
226
|
+
loading: "loading",
|
|
227
|
+
failed: "error",
|
|
228
|
+
retryable: (state) => state === "error",
|
|
229
|
+
decode: async (response) => {
|
|
230
|
+
if (response.status === 404) return "missing";
|
|
231
|
+
if (!response.ok) return "error";
|
|
232
|
+
const value = await response.json();
|
|
233
|
+
return isChangesDiff(value) ? value : "error";
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Read one comparison; a cached comparison or a missing one is kept, a failed one is read again.
|
|
239
|
+
* @param sessionId - viewed Session.
|
|
240
|
+
* @param seq - the announcing event's sequence.
|
|
241
|
+
* @param index - the file's index in the summary.
|
|
242
|
+
* @returns after the state is published.
|
|
243
|
+
*/
|
|
244
|
+
load(sessionId, seq, index) {
|
|
245
|
+
return this.loadUrl(changesDiffUrl(sessionId, seq, index));
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region lib/types/client/changes-summary.js
|
|
250
|
+
/** One browser plugin's summary reads; a summary or a missing answer is kept until the connection is replaced. */
|
|
251
|
+
var ChangesSummaryStore = class extends HostReadStore {
|
|
252
|
+
constructor() {
|
|
253
|
+
super({
|
|
254
|
+
loading: "loading",
|
|
255
|
+
failed: "missing",
|
|
256
|
+
retryable: () => false,
|
|
257
|
+
decode: async (response) => {
|
|
258
|
+
if (!response.ok) return "missing";
|
|
259
|
+
const value = await response.json();
|
|
260
|
+
return isChangesSummary(value) ? value : "missing";
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Read one summary once; a later read of the same coordinates returns the cached state.
|
|
266
|
+
* @param sessionId - viewed Session.
|
|
267
|
+
* @param seq - the announcing event's sequence.
|
|
268
|
+
* @returns after the state is published.
|
|
269
|
+
*/
|
|
270
|
+
load(sessionId, seq) {
|
|
271
|
+
return this.loadUrl(changesSummaryUrl(sessionId, seq));
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region lib/types/presented.js
|
|
276
|
+
/** Authenticated POST route for opening a workspace file on the Host desktop. */
|
|
277
|
+
const PRESENT_OPEN_PATH = "/api/present.open";
|
|
278
|
+
/** Authenticated desktop availability and destination metadata. */
|
|
279
|
+
const PRESENT_HOST_PATH = "/api/present.host";
|
|
280
|
+
/**
|
|
281
|
+
* Browser-relative form of {@link PRESENT_OPEN_PATH}; see
|
|
282
|
+
* .agents/notes/implemented/architecture/2026-09-14-web-document-relative-app-routes.md.
|
|
283
|
+
*/
|
|
284
|
+
const PRESENT_OPEN_ROUTE = PRESENT_OPEN_PATH.slice(1);
|
|
285
|
+
/** Browser-relative form of {@link PRESENT_HOST_PATH}. */
|
|
286
|
+
const PRESENT_HOST_ROUTE = PRESENT_HOST_PATH.slice(1);
|
|
287
|
+
/**
|
|
288
|
+
* Validate desktop metadata received over HTTP.
|
|
289
|
+
* @param value - decoded response.
|
|
290
|
+
* @returns whether all displayed and actionable fields are supported.
|
|
291
|
+
*/
|
|
292
|
+
function isPresentedHost(value) {
|
|
293
|
+
if (typeof value !== "object" || value === null) return false;
|
|
294
|
+
const host = value;
|
|
295
|
+
return typeof host.name === "string" && typeof host.available === "boolean" && (host.fileManager === null || host.fileManager === "finder" || host.fileManager === "explorer" || host.fileManager === "directory");
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Validate a file declaration read from a Session log.
|
|
299
|
+
* @param value - decoded durable data.
|
|
300
|
+
* @returns whether the declaration contains a path and optional description.
|
|
301
|
+
*/
|
|
302
|
+
function isPresentedFile(value) {
|
|
303
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
304
|
+
const { path, description } = value;
|
|
305
|
+
return typeof path === "string" && path.trim().length > 0 && (description === void 0 || typeof description === "string");
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Build authenticated coordinates for a declared file.
|
|
309
|
+
* @param sessionId - owning Session.
|
|
310
|
+
* @param seq - deliverables/presented event sequence.
|
|
311
|
+
* @param index - original index in the event's files array.
|
|
312
|
+
* @returns document-relative file action route.
|
|
313
|
+
*/
|
|
314
|
+
function presentedFileUrl(sessionId, seq, index) {
|
|
315
|
+
return `${PRESENT_OPEN_ROUTE}?${new URLSearchParams({
|
|
316
|
+
sessionId,
|
|
317
|
+
seq: String(seq),
|
|
318
|
+
index: String(index)
|
|
319
|
+
})}`;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Validate a delivery event before reading its turn or file declarations.
|
|
323
|
+
* @param value - decoded durable event data.
|
|
324
|
+
* @returns whether the event identifies a turn, call, and file list.
|
|
325
|
+
*/
|
|
326
|
+
function isPresentedData(value) {
|
|
327
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
328
|
+
const { turn, callId, files } = value;
|
|
329
|
+
return typeof turn === "number" && Number.isSafeInteger(turn) && turn >= 1 && typeof callId === "string" && callId.length > 0 && Array.isArray(files);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Trailing path segment, the part that identifies the file at a glance.
|
|
333
|
+
* @param path - Slash- or backslash-separated path.
|
|
334
|
+
* @returns The final segment, or the whole string when separator-free.
|
|
335
|
+
*/
|
|
336
|
+
function basename(path) {
|
|
337
|
+
const at = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
338
|
+
return at === -1 ? path : path.slice(at + 1);
|
|
339
|
+
}
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region lib/types/client/present-open.js
|
|
342
|
+
/** Shared native-open status for delivery cards, the changed-files card, and closing-message file mentions. */
|
|
343
|
+
/** One browser plugin's file-open requests, cancelled when that plugin is disposed. */
|
|
344
|
+
var PresentedOpenController = class {
|
|
345
|
+
/** File action URLs key the state across Sessions, turns, and both clickable surfaces. */
|
|
346
|
+
state = (0, _qilin_agent_client_store.createSnapshotStore)({});
|
|
347
|
+
/** Native destination metadata, or a retryable read failure. */
|
|
348
|
+
host = (0, _qilin_agent_client_store.createSnapshotStore)(null);
|
|
349
|
+
loading;
|
|
350
|
+
metadata = new AbortController();
|
|
351
|
+
lifetime = new AbortController();
|
|
352
|
+
pending = /* @__PURE__ */ new Set();
|
|
353
|
+
/**
|
|
354
|
+
* Open a declared file once while a request for the same coordinates is pending.
|
|
355
|
+
* Failures remain visible on the card and a later gesture retries them.
|
|
356
|
+
* @param sessionId - viewed Session, including a fork's own identity.
|
|
357
|
+
* @param seq - durable delivery event sequence.
|
|
358
|
+
* @param index - original file index within that event.
|
|
359
|
+
* @param action - default application open or file-manager reveal.
|
|
360
|
+
* @returns after the Host acknowledges opening or the error state is published.
|
|
361
|
+
*/
|
|
362
|
+
open(sessionId, seq, index, action = "open") {
|
|
363
|
+
return this.openUrl(presentedFileUrl(sessionId, seq, index), action);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Open one recorded changed file in the Host's default application.
|
|
367
|
+
* @param sessionId - viewed Session.
|
|
368
|
+
* @param seq - durable workspace/changes event sequence.
|
|
369
|
+
* @param index - original file index within that event.
|
|
370
|
+
* @returns after the Host acknowledges opening or the error state is published.
|
|
371
|
+
*/
|
|
372
|
+
openChanged(sessionId, seq, index) {
|
|
373
|
+
return this.openUrl(changedFileUrl(sessionId, seq, index), "open");
|
|
374
|
+
}
|
|
375
|
+
async openUrl(url, action) {
|
|
376
|
+
const phase = this.state.getSnapshot()[url];
|
|
377
|
+
if (this.lifetime.signal.aborted || phase === "opening" || phase === "revealing") return;
|
|
378
|
+
this.state.update((state) => {
|
|
379
|
+
state[url] = action === "open" ? "opening" : "revealing";
|
|
380
|
+
});
|
|
381
|
+
const task = this.request(url, action);
|
|
382
|
+
this.pending.add(task);
|
|
383
|
+
try {
|
|
384
|
+
await task;
|
|
385
|
+
} finally {
|
|
386
|
+
this.pending.delete(task);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Read the serving desktop metadata, coalescing concurrent reads; a later call retries failure.
|
|
391
|
+
* @returns after metadata or a retryable error is published.
|
|
392
|
+
*/
|
|
393
|
+
async loadHost() {
|
|
394
|
+
if (this.lifetime.signal.aborted) return;
|
|
395
|
+
if (this.loading !== void 0) return this.loading;
|
|
396
|
+
this.host.set(null);
|
|
397
|
+
const task = this.readHost(AbortSignal.any([this.lifetime.signal, this.metadata.signal]));
|
|
398
|
+
this.loading = task;
|
|
399
|
+
this.pending.add(task);
|
|
400
|
+
try {
|
|
401
|
+
await task;
|
|
402
|
+
} finally {
|
|
403
|
+
if (this.loading === task) this.loading = void 0;
|
|
404
|
+
this.pending.delete(task);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/** Invalidate desktop metadata on connection replacement; mounted cards request the new Host. */
|
|
408
|
+
resetHost() {
|
|
409
|
+
const wasLoading = this.loading !== void 0;
|
|
410
|
+
this.metadata.abort();
|
|
411
|
+
this.metadata = new AbortController();
|
|
412
|
+
this.loading = void 0;
|
|
413
|
+
this.host.set(null);
|
|
414
|
+
if (wasLoading) this.loadHost();
|
|
415
|
+
}
|
|
416
|
+
async readHost(signal) {
|
|
417
|
+
let host = "error";
|
|
418
|
+
try {
|
|
419
|
+
const response = await fetch(PRESENT_HOST_ROUTE, { signal });
|
|
420
|
+
if (response.ok) {
|
|
421
|
+
const value = await response.json();
|
|
422
|
+
if (isPresentedHost(value)) host = value;
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
host = "error";
|
|
426
|
+
}
|
|
427
|
+
if (!signal.aborted) this.host.set(host);
|
|
428
|
+
}
|
|
429
|
+
/** Cancel outstanding requests and wait until no request can publish state. */
|
|
430
|
+
async dispose() {
|
|
431
|
+
this.lifetime.abort();
|
|
432
|
+
await Promise.all(this.pending);
|
|
433
|
+
}
|
|
434
|
+
async request(url, action) {
|
|
435
|
+
const failure = action === "open" ? "error" : "revealError";
|
|
436
|
+
let phase = action === "open" ? "opened" : "revealed";
|
|
437
|
+
try {
|
|
438
|
+
const response = await fetch(action === "open" ? url : `${url}&action=reveal`, {
|
|
439
|
+
method: "POST",
|
|
440
|
+
signal: this.lifetime.signal
|
|
441
|
+
});
|
|
442
|
+
if (!response.ok) phase = response.status === 422 ? "nativeUnavailable" : failure;
|
|
443
|
+
} catch {
|
|
444
|
+
phase = failure;
|
|
445
|
+
}
|
|
446
|
+
if (!this.lifetime.signal.aborted) this.state.update((state) => {
|
|
447
|
+
state[url] = phase;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-deliverables/src/client/PresentRow.module.css.mjs
|
|
453
|
+
const css$3 = ".eTe-sW_summary{min-width:0;color:var(--qilin-alias-label-secondary);align-items:center;gap:8px;margin-left:8px;font-size:12px;display:flex}.eTe-sW_summary>:first-child{flex-shrink:0}.eTe-sW_paths{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.eTe-sW_output{background:var(--qilin-alias-bg-layer-1);color:var(--qilin-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere;border-radius:8px;margin:8px 0;padding:12px;font-size:12px}.eTe-sW_inspect{color:var(--qilin-alias-link);font:inherit;cursor:pointer;background:0 0;border:none;align-self:flex-start;padding:4px 0;font-size:12px}";
|
|
454
|
+
const tagId$3 = "@qilin-agent/client-ui-deliverables/PresentRow.module.css";
|
|
455
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
|
|
456
|
+
const tag = document.createElement("style");
|
|
457
|
+
tag.dataset.plugin = "@qilin-agent/client-ui-deliverables";
|
|
458
|
+
tag.dataset.pluginCss = tagId$3;
|
|
459
|
+
tag.textContent = css$3;
|
|
460
|
+
document.head.appendChild(tag);
|
|
461
|
+
}
|
|
462
|
+
var PresentRow_module_css_default = {
|
|
463
|
+
"inspect": "eTe-sW_inspect",
|
|
464
|
+
"output": "eTe-sW_output",
|
|
465
|
+
"paths": "eTe-sW_paths",
|
|
466
|
+
"summary": "eTe-sW_summary"
|
|
467
|
+
};
|
|
468
|
+
//#endregion
|
|
469
|
+
//#region lib/types/client/PresentRow.js
|
|
470
|
+
/** Present call status and expandable durable result text. */
|
|
471
|
+
/* v8 ignore next -- Non-expandable rows never invoke DisclosureRow's required toggle callback. */
|
|
472
|
+
const noop = () => void 0;
|
|
473
|
+
/** Raw arguments can be partial while a call is streaming. */
|
|
474
|
+
function fileNames(raw) {
|
|
475
|
+
let args;
|
|
476
|
+
try {
|
|
477
|
+
args = JSON.parse(raw);
|
|
478
|
+
} catch {
|
|
479
|
+
return raw;
|
|
480
|
+
}
|
|
481
|
+
if (typeof args !== "object" || args === null || !("files" in args) || !Array.isArray(args.files)) return raw;
|
|
482
|
+
return args.files.flatMap((file) => typeof file === "object" && file !== null && "path" in file && typeof file.path === "string" ? [file.path] : []).join(", ");
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Render a present call using its recorded arguments and result.
|
|
486
|
+
* @param props - tool call and localized status copy.
|
|
487
|
+
* @returns a status row with a result disclosure.
|
|
488
|
+
*/
|
|
489
|
+
function PresentRow(props) {
|
|
490
|
+
return props.phase === "preparing" ? (0, react_jsx_runtime.jsx)(PreparingPresentRow, { ...props }) : (0, react_jsx_runtime.jsx)(StartedPresentRow, { ...props });
|
|
491
|
+
}
|
|
492
|
+
function PreparingPresentRow({ t }) {
|
|
493
|
+
return (0, react_jsx_runtime.jsx)("div", {
|
|
494
|
+
"data-tool": "present",
|
|
495
|
+
"data-state": "preparing",
|
|
496
|
+
"aria-label": t("row.preparing"),
|
|
497
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.DisclosureRow, {
|
|
498
|
+
title: t("row.title"),
|
|
499
|
+
icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.StateDot, { state: "ongoing" }),
|
|
500
|
+
open: false,
|
|
501
|
+
expandable: false,
|
|
502
|
+
onToggle: noop
|
|
503
|
+
})
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
function StartedPresentRow({ block, inspect, t }) {
|
|
507
|
+
const settled = "kind" in block;
|
|
508
|
+
const state = !settled ? "running" : block.error?.code === "interrupted" ? "stopped" : block.isError ? "error" : "ok";
|
|
509
|
+
const args = (settled ? block.call?.argsRaw : block.argsRaw) ?? "";
|
|
510
|
+
const details = (settled ? block.content.map((item) => item.type === "text" ? item.text : JSON.stringify(item)).join("\n") : "") || (settled && block.error ? `${block.error.name}: ${block.error.code}` : "");
|
|
511
|
+
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
512
|
+
return (0, react_jsx_runtime.jsx)("div", {
|
|
513
|
+
"data-tool": "present",
|
|
514
|
+
"data-state": state,
|
|
515
|
+
children: (0, react_jsx_runtime.jsxs)(_qilin_agent_client_ui_primitives.DisclosureRow, {
|
|
516
|
+
title: t("row.title"),
|
|
517
|
+
icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.StateDot, { state: state === "running" ? "ongoing" : state === "ok" ? "done" : state === "stopped" ? "warning" : "error" }),
|
|
518
|
+
open: expanded && details !== "",
|
|
519
|
+
expandable: details !== "",
|
|
520
|
+
expandOnRowClick: true,
|
|
521
|
+
keepContentWhenOpen: true,
|
|
522
|
+
onToggle: () => {
|
|
523
|
+
setExpanded((value) => !value);
|
|
524
|
+
},
|
|
525
|
+
collapsedContent: (0, react_jsx_runtime.jsxs)("span", {
|
|
526
|
+
className: PresentRow_module_css_default.summary,
|
|
527
|
+
children: [(0, react_jsx_runtime.jsx)("span", { children: t(`row.${state}`) }), (0, react_jsx_runtime.jsx)("span", {
|
|
528
|
+
className: PresentRow_module_css_default.paths,
|
|
529
|
+
children: fileNames(args)
|
|
530
|
+
})]
|
|
531
|
+
}),
|
|
532
|
+
children: [(0, react_jsx_runtime.jsx)("pre", {
|
|
533
|
+
className: PresentRow_module_css_default.output,
|
|
534
|
+
children: details
|
|
535
|
+
}), inspect && (0, react_jsx_runtime.jsx)("button", {
|
|
536
|
+
type: "button",
|
|
537
|
+
className: PresentRow_module_css_default.inspect,
|
|
538
|
+
onClick: inspect,
|
|
539
|
+
children: t("row.inspect")
|
|
540
|
+
})]
|
|
541
|
+
})
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
//#endregion
|
|
545
|
+
//#region ../../util/workspace-path/lib/index.js
|
|
546
|
+
/**
|
|
547
|
+
* The `qilin-resource://file/…` address grammar: how a file is named across the
|
|
548
|
+
* Sidebar and the resource model, built and parsed without touching a
|
|
549
|
+
* filesystem.
|
|
550
|
+
* @module
|
|
551
|
+
*/
|
|
552
|
+
/** The scheme and type every file address opens with. */
|
|
553
|
+
const FILE_ADDRESS_PREFIX = "qilin-resource://file/";
|
|
554
|
+
/** Component-encode one id or path segment, keeping `:` literal for drive letters. */
|
|
555
|
+
function encodeSegment(segment) {
|
|
556
|
+
return encodeURIComponent(segment).replace(/%3A/gi, ":");
|
|
557
|
+
}
|
|
558
|
+
/** Encode a `/`-separated path segment by segment. */
|
|
559
|
+
function encodePath(path) {
|
|
560
|
+
return path.split("/").map(encodeSegment).join("/");
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Build the address of a file read through one Session.
|
|
564
|
+
* @param sessionId - the Session whose Host workspace resolves the path.
|
|
565
|
+
* @param path - absolute or workspace-relative path; backslashes are normalized to `/`, and leading `./` prefixes are dropped.
|
|
566
|
+
* @returns the `qilin-resource://file/session/<sessionId>/<path>` address.
|
|
567
|
+
*/
|
|
568
|
+
function sessionFileAddress(sessionId, path) {
|
|
569
|
+
const normalized = path.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "");
|
|
570
|
+
return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Browser-safe Workspace path and display helpers.
|
|
574
|
+
* @module @qilin-agent/util-workspace-path
|
|
575
|
+
*/
|
|
576
|
+
/** Whether a path uses a Windows drive or UNC prefix. */
|
|
577
|
+
function isWindowsStylePath(value) {
|
|
578
|
+
return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Whether a path is absolute in either spelling the Host accepts: POSIX (`/a/b`) or Windows drive or UNC.
|
|
582
|
+
* @param path - the path to classify.
|
|
583
|
+
* @returns `true` for an absolute path; `false` for a Workspace-relative one.
|
|
584
|
+
*/
|
|
585
|
+
function isAbsoluteWorkspacePath(path) {
|
|
586
|
+
return path.startsWith("/") || isWindowsStylePath(path);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Resolve a Workspace-relative path into the Host-facing spelling used by path operations.
|
|
590
|
+
* @param cwd - Session Workspace root, when known.
|
|
591
|
+
* @param path - Absolute or Workspace-relative path.
|
|
592
|
+
* @returns an absolute path when a Workspace root is available, otherwise the original path.
|
|
593
|
+
*/
|
|
594
|
+
function resolveWorkspacePath(cwd, path) {
|
|
595
|
+
if (isAbsoluteWorkspacePath(path)) return path;
|
|
596
|
+
if (cwd === void 0 || cwd === "") return path;
|
|
597
|
+
const separator = isWindowsStylePath(cwd) && cwd.includes("\\") ? "\\" : "/";
|
|
598
|
+
return `${cwd.replace(/[/\\]+$/, "")}${separator}${path.replace(/^[/\\]+/, "")}`;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* The address for a path as a caller holds it: a relative path, or an absolute
|
|
602
|
+
* path inside the Session's workspace, becomes a `session`-scoped address; an
|
|
603
|
+
* absolute path outside it, or one whose workspace root is unknown, keeps its
|
|
604
|
+
* absolute path in that Session's address.
|
|
605
|
+
* @param sessionId - the Session the path is read in.
|
|
606
|
+
* @param cwd - that Session's workspace root, when known.
|
|
607
|
+
* @param path - absolute or workspace-relative path, in either separator spelling.
|
|
608
|
+
* @returns the `qilin-resource://file/…` address.
|
|
609
|
+
*/
|
|
610
|
+
function fileAddressFor(sessionId, cwd, path) {
|
|
611
|
+
const normalized = path.replace(/\\/g, "/");
|
|
612
|
+
if (!isAbsoluteWorkspacePath(normalized)) return sessionFileAddress(sessionId, normalized);
|
|
613
|
+
const root = cwd === void 0 ? "" : cwd.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
614
|
+
if (root !== "" && normalized === root) return sessionFileAddress(sessionId, "");
|
|
615
|
+
if (root !== "" && normalized.startsWith(`${root}/`)) return sessionFileAddress(sessionId, normalized.slice(root.length + 1));
|
|
616
|
+
return sessionFileAddress(sessionId, normalized);
|
|
617
|
+
}
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region lib/types/client/icons.js
|
|
620
|
+
/** Angle brackets, the code mark the card's header tile shows. */
|
|
621
|
+
const IconCodeBracketsOutline16 = ({ size = 16, className }) => (0, react_jsx_runtime.jsx)("svg", {
|
|
622
|
+
width: size,
|
|
623
|
+
height: size,
|
|
624
|
+
className,
|
|
625
|
+
viewBox: "0 0 16 16",
|
|
626
|
+
fill: "none",
|
|
627
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
628
|
+
"aria-hidden": "true",
|
|
629
|
+
children: (0, react_jsx_runtime.jsx)("path", {
|
|
630
|
+
d: "M5.6 3.4 1 8l4.6 4.6M10.4 3.4 15 8l-4.6 4.6",
|
|
631
|
+
stroke: "currentColor",
|
|
632
|
+
strokeWidth: "1.8",
|
|
633
|
+
strokeLinecap: "round",
|
|
634
|
+
strokeLinejoin: "round"
|
|
635
|
+
})
|
|
636
|
+
});
|
|
637
|
+
//#endregion
|
|
638
|
+
//#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-deliverables/src/client/ChangedFiles.module.css.mjs
|
|
639
|
+
const css$2 = ".TY0rha_card{--changes-fill:var(--qilin-static-neutral-50);--changes-hover:var(--qilin-static-neutral-100);border:.5px solid var(--qilin-alias-border-l1);min-width:0;color:var(--qilin-alias-label-primary);border-radius:16px;flex-direction:column;margin-top:4px;display:flex;overflow:hidden}body[data-ds-dark-theme] .TY0rha_card{--changes-fill:var(--qilin-static-neutral-850);--changes-hover:var(--qilin-static-neutral-800)}.TY0rha_header{box-sizing:border-box;background:var(--changes-fill);width:100%;min-width:0;color:inherit;font:inherit;text-align:left;border:0;align-items:center;gap:12px;margin:0;padding:12px 16px;display:flex}button.TY0rha_header{cursor:pointer;transition:background-color .12s}button.TY0rha_header:hover:not(:disabled),button.TY0rha_header:focus-visible{background:var(--changes-hover)}button.TY0rha_header:focus-visible{box-shadow:inset 0 0 0 2px var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));outline:none}button.TY0rha_header:disabled{cursor:progress}.TY0rha_tile{background:var(--qilin-alias-link);width:36px;height:36px;color:var(--qilin-static-neutral-00);border-radius:10px;flex:none;place-items:center;display:grid}.TY0rha_titles{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.TY0rha_title{text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:500;line-height:22px;overflow:hidden}.TY0rha_stat{font-family:var(--ds-font-family-code);color:var(--qilin-alias-label-tertiary);gap:6px;font-size:12px;line-height:18px;display:inline-flex}.TY0rha_stat[data-error=true],.TY0rha_counts[data-error=true]{color:var(--qilin-alias-state-error-primary)}.TY0rha_added{color:var(--qilin-alias-state-success-primary)}.TY0rha_deleted{color:var(--qilin-alias-state-error-primary)}.TY0rha_list{border-top:.5px solid var(--qilin-alias-border-l1);margin:0;padding:4px 0;list-style:none}.TY0rha_row{box-sizing:border-box;width:100%;min-width:0;color:var(--qilin-alias-label-secondary);cursor:pointer;font-family:var(--ds-font-family-code);text-align:left;background:0 0;border:0;justify-content:space-between;align-items:center;gap:12px;margin:0;padding:6px 16px;font-size:12px;line-height:18px;display:flex}.TY0rha_row:hover:not(:disabled),.TY0rha_row:focus-visible{background:var(--qilin-alias-interactive-bg-hover)}.TY0rha_row:focus-visible{box-shadow:inset 0 0 0 2px var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));outline:none}.TY0rha_row:disabled{cursor:progress}.TY0rha_path{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.TY0rha_leading{flex:1;align-items:center;gap:8px;min-width:0;display:inline-flex}.TY0rha_fileIcon{box-sizing:border-box;border:.5px solid var(--qilin-alias-border-l1);background:var(--changes-fill);width:22px;height:22px;color:var(--qilin-alias-link);border-radius:6px;flex:none;place-items:center;display:grid;overflow:hidden}.TY0rha_row .TY0rha_path{color:var(--qilin-alias-label-primary);font-weight:500}.TY0rha_counts{white-space:nowrap;color:var(--qilin-alias-label-tertiary);flex:none;gap:6px;display:inline-flex}.TY0rha_toggle{box-sizing:border-box;border:0;border-top:.5px solid var(--qilin-alias-border-l1);width:100%;color:var(--qilin-alias-label-tertiary);cursor:pointer;font:inherit;text-align:left;background:0 0;align-items:center;gap:4px;margin:0;padding:8px 16px;font-size:12px;line-height:18px;display:inline-flex}.TY0rha_toggle:hover{background:var(--qilin-alias-interactive-bg-hover)}.TY0rha_toggle svg{flex:none;width:14px;height:14px}@media (pointer:coarse){.TY0rha_row,.TY0rha_toggle{min-height:44px}}";
|
|
640
|
+
const tagId$2 = "@qilin-agent/client-ui-deliverables/ChangedFiles.module.css";
|
|
641
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
|
|
642
|
+
const tag = document.createElement("style");
|
|
643
|
+
tag.dataset.plugin = "@qilin-agent/client-ui-deliverables";
|
|
644
|
+
tag.dataset.pluginCss = tagId$2;
|
|
645
|
+
tag.textContent = css$2;
|
|
646
|
+
document.head.appendChild(tag);
|
|
647
|
+
}
|
|
648
|
+
var ChangedFiles_module_css_default = {
|
|
649
|
+
"added": "TY0rha_added",
|
|
650
|
+
"card": "TY0rha_card",
|
|
651
|
+
"counts": "TY0rha_counts",
|
|
652
|
+
"deleted": "TY0rha_deleted",
|
|
653
|
+
"fileIcon": "TY0rha_fileIcon",
|
|
654
|
+
"header": "TY0rha_header",
|
|
655
|
+
"leading": "TY0rha_leading",
|
|
656
|
+
"list": "TY0rha_list",
|
|
657
|
+
"path": "TY0rha_path",
|
|
658
|
+
"row": "TY0rha_row",
|
|
659
|
+
"stat": "TY0rha_stat",
|
|
660
|
+
"tile": "TY0rha_tile",
|
|
661
|
+
"title": "TY0rha_title",
|
|
662
|
+
"titles": "TY0rha_titles",
|
|
663
|
+
"toggle": "TY0rha_toggle"
|
|
664
|
+
};
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region lib/types/client/ChangedFiles.js
|
|
667
|
+
/** The changed-files card: a header and per-file rows that open the turn's review, and a three-row fold. */
|
|
668
|
+
/** Rows shown before the fold; the design's summary height for a closing message. */
|
|
669
|
+
const COLLAPSED_ROWS = 3;
|
|
670
|
+
const GROUPED$1 = new Intl.NumberFormat("en-US");
|
|
671
|
+
/** Added and deleted line counts in the card's colors. */
|
|
672
|
+
function Counts$1({ added, deleted, t }) {
|
|
673
|
+
return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("span", {
|
|
674
|
+
className: ChangedFiles_module_css_default.added,
|
|
675
|
+
children: t("changes.added", { count: GROUPED$1.format(added) })
|
|
676
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
677
|
+
className: ChangedFiles_module_css_default.deleted,
|
|
678
|
+
children: t("changes.deleted", { count: GROUPED$1.format(deleted) })
|
|
679
|
+
})] });
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Render one turn's changed files. The header opens the turn's review in the
|
|
683
|
+
* right Sidebar on its first file; each row opens it on that row's file.
|
|
684
|
+
* @param props - the recorded summary, the review opener, and localized copy.
|
|
685
|
+
* @returns the card.
|
|
686
|
+
*/
|
|
687
|
+
function ChangedFiles({ changes, cwd, openReview, t }) {
|
|
688
|
+
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
689
|
+
const foldable = changes.files.length > COLLAPSED_ROWS;
|
|
690
|
+
const rows = foldable && !expanded ? changes.files.slice(0, COLLAPSED_ROWS) : changes.files;
|
|
691
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
692
|
+
className: ChangedFiles_module_css_default.card,
|
|
693
|
+
"data-changed-files": true,
|
|
694
|
+
children: [
|
|
695
|
+
(0, react_jsx_runtime.jsxs)("button", {
|
|
696
|
+
type: "button",
|
|
697
|
+
className: ChangedFiles_module_css_default.header,
|
|
698
|
+
"aria-label": t("changes.openReview"),
|
|
699
|
+
onClick: () => {
|
|
700
|
+
openReview(0);
|
|
701
|
+
},
|
|
702
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
703
|
+
className: ChangedFiles_module_css_default.tile,
|
|
704
|
+
children: (0, react_jsx_runtime.jsx)(IconCodeBracketsOutline16, { size: 18 })
|
|
705
|
+
}), (0, react_jsx_runtime.jsxs)("span", {
|
|
706
|
+
className: ChangedFiles_module_css_default.titles,
|
|
707
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
708
|
+
className: ChangedFiles_module_css_default.title,
|
|
709
|
+
children: t("changes.title", { count: String(changes.total) })
|
|
710
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
711
|
+
className: ChangedFiles_module_css_default.stat,
|
|
712
|
+
children: (0, react_jsx_runtime.jsx)(Counts$1, {
|
|
713
|
+
t,
|
|
714
|
+
added: changes.added,
|
|
715
|
+
deleted: changes.deleted
|
|
716
|
+
})
|
|
717
|
+
})]
|
|
718
|
+
})]
|
|
719
|
+
}),
|
|
720
|
+
(0, react_jsx_runtime.jsx)("ul", {
|
|
721
|
+
className: ChangedFiles_module_css_default.list,
|
|
722
|
+
children: rows.map((file, index) => (0, react_jsx_runtime.jsx)("li", { children: (0, react_jsx_runtime.jsxs)("button", {
|
|
723
|
+
type: "button",
|
|
724
|
+
className: ChangedFiles_module_css_default.row,
|
|
725
|
+
title: resolveWorkspacePath(cwd, file.path),
|
|
726
|
+
"aria-label": t("changes.viewDiff", { name: file.display }),
|
|
727
|
+
onClick: () => {
|
|
728
|
+
openReview(index);
|
|
729
|
+
},
|
|
730
|
+
children: [(0, react_jsx_runtime.jsxs)("span", {
|
|
731
|
+
className: ChangedFiles_module_css_default.leading,
|
|
732
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
733
|
+
className: ChangedFiles_module_css_default.fileIcon,
|
|
734
|
+
"aria-hidden": true,
|
|
735
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.FileTypeIcon, {
|
|
736
|
+
path: file.path,
|
|
737
|
+
size: 16
|
|
738
|
+
})
|
|
739
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
740
|
+
className: ChangedFiles_module_css_default.path,
|
|
741
|
+
children: file.display
|
|
742
|
+
})]
|
|
743
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
744
|
+
className: ChangedFiles_module_css_default.counts,
|
|
745
|
+
children: file.binary === true ? t("changes.binary") : file.oversized === true ? t("changes.oversized") : (0, react_jsx_runtime.jsx)(Counts$1, {
|
|
746
|
+
t,
|
|
747
|
+
added: file.added,
|
|
748
|
+
deleted: file.deleted
|
|
749
|
+
})
|
|
750
|
+
})]
|
|
751
|
+
}) }, file.display))
|
|
752
|
+
}),
|
|
753
|
+
foldable && (0, react_jsx_runtime.jsxs)("button", {
|
|
754
|
+
type: "button",
|
|
755
|
+
className: ChangedFiles_module_css_default.toggle,
|
|
756
|
+
"aria-expanded": expanded,
|
|
757
|
+
"aria-label": t(expanded ? "changes.collapseAria" : "changes.expandAria", { count: String(changes.files.length) }),
|
|
758
|
+
onClick: () => {
|
|
759
|
+
setExpanded((value) => !value);
|
|
760
|
+
},
|
|
761
|
+
children: [(0, react_jsx_runtime.jsx)("span", { children: t(expanded ? "changes.collapse" : "changes.all", { count: String(changes.files.length) }) }), expanded ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronUpOutline14, {}) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, {})]
|
|
762
|
+
})
|
|
763
|
+
]
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region ../../core/session/lib/types/surface.js
|
|
768
|
+
/** Runtime counterpart of the message-producing event union. */
|
|
769
|
+
const SURFACE_EVENT_TYPES = new Set([
|
|
770
|
+
"system/message",
|
|
771
|
+
"developer/message",
|
|
772
|
+
"user/message",
|
|
773
|
+
"assistant/message",
|
|
774
|
+
"tool/result"
|
|
775
|
+
]);
|
|
776
|
+
/**
|
|
777
|
+
* Narrow an event to a surface-eligible event carrying its required marker.
|
|
778
|
+
* @param event - event to test.
|
|
779
|
+
* @returns true when both the type and marker identify a surface event.
|
|
780
|
+
*/
|
|
781
|
+
function isSurfaceEvent(event) {
|
|
782
|
+
if (!SURFACE_EVENT_TYPES.has(event.type)) return false;
|
|
783
|
+
return event.surfaceOp !== void 0;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Narrow an event to an append-origin surface event: one that entered the
|
|
787
|
+
* surface at its own log position and was never itself a replacement copy.
|
|
788
|
+
*
|
|
789
|
+
* The model-visible surface deliberately shadows replaced ranges, so it is the
|
|
790
|
+
* wrong source for a human transcript — a landed replacement would erase
|
|
791
|
+
* conversation the user already saw. Append-origin events are that transcript's
|
|
792
|
+
* durable source material; replacement copies stay model-only.
|
|
793
|
+
* @param event - event to test.
|
|
794
|
+
* @returns true when the event appended to the surface tail.
|
|
795
|
+
*/
|
|
796
|
+
function isAppendSurfaceEvent(event) {
|
|
797
|
+
return isSurfaceEvent(event) && event.surfaceOp === "append";
|
|
798
|
+
}
|
|
799
|
+
//#endregion
|
|
800
|
+
//#region lib/types/client/turn-deliverables.js
|
|
801
|
+
/**
|
|
802
|
+
* Turn-scoped produced-file Definition and readers. Client-only and
|
|
803
|
+
* model-free: produced paths come from successful first-party mutation calls and
|
|
804
|
+
* their recorded file targets, changed files from the Host's recorded git
|
|
805
|
+
* summary, and deliveries from `present`; never from presentation data or
|
|
806
|
+
* the closing prose.
|
|
807
|
+
*/
|
|
808
|
+
/**
|
|
809
|
+
* Extract the path from a supported first-party mutation call. Session
|
|
810
|
+
* `tool/call` events are root calls; PTC dispatch children do not enter this
|
|
811
|
+
* Definition independently.
|
|
812
|
+
* @param name - wire tool name.
|
|
813
|
+
* @param argsRaw - model-produced JSON arguments.
|
|
814
|
+
* @returns the mutation path, or null when the call is not a supported mutation.
|
|
815
|
+
*/
|
|
816
|
+
function mutationPath(name, argsRaw) {
|
|
817
|
+
let args;
|
|
818
|
+
try {
|
|
819
|
+
args = JSON.parse(argsRaw);
|
|
820
|
+
} catch {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
if (!isRecord(args)) return null;
|
|
824
|
+
switch (name) {
|
|
825
|
+
case "write": return typeof args.content === "string" ? pathValue(args.file_path) : null;
|
|
826
|
+
case "edit": return validEditArgs(args) ? pathValue(args.file_path) : null;
|
|
827
|
+
case "str_replace_editor": return editorMutationPath(args);
|
|
828
|
+
default: return null;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/** Validate the fields that an `edit` execution requires. */
|
|
832
|
+
function validEditArgs(args) {
|
|
833
|
+
return typeof args.old_string === "string" && args.old_string.length > 0 && typeof args.new_string === "string" && args.old_string !== args.new_string && (args.replace_all === void 0 || typeof args.replace_all === "boolean");
|
|
834
|
+
}
|
|
835
|
+
/** Extract a path only from a complete mutating editor command. */
|
|
836
|
+
function editorMutationPath(args) {
|
|
837
|
+
const path = pathValue(args.path);
|
|
838
|
+
if (path === null) return null;
|
|
839
|
+
switch (args.command) {
|
|
840
|
+
case "create": return typeof args.file_text === "string" ? path : null;
|
|
841
|
+
case "str_replace": return typeof args.old_str === "string" && args.old_str.length > 0 && (args.new_str === void 0 || typeof args.new_str === "string") ? path : null;
|
|
842
|
+
case "insert": return typeof args.insert_line === "number" && Number.isInteger(args.insert_line) && args.insert_line >= 0 && typeof args.new_str === "string" ? path : null;
|
|
843
|
+
default: return null;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/** A non-blank path preserves the exact spelling supplied to the tool. */
|
|
847
|
+
function pathValue(value) {
|
|
848
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
849
|
+
}
|
|
850
|
+
/** Resolve a settled mutation from recorded metadata, retaining legacy argument paths. */
|
|
851
|
+
function mutationResultPath(argumentPath, meta) {
|
|
852
|
+
if (!isRecord(meta) || !Object.hasOwn(meta, "path")) return argumentPath;
|
|
853
|
+
if (typeof meta.path === "string" && isAbsoluteWorkspacePath(meta.path)) return meta.path;
|
|
854
|
+
return isAbsoluteWorkspacePath(argumentPath) ? argumentPath : null;
|
|
855
|
+
}
|
|
856
|
+
/** Narrow parsed JSON to an argument object. */
|
|
857
|
+
function isRecord(value) {
|
|
858
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Files produced by one Turn data value.
|
|
862
|
+
*
|
|
863
|
+
* The source is successful `write`, `edit`, and mutating
|
|
864
|
+
* `str_replace_editor` calls and their recorded absolute targets; legacy
|
|
865
|
+
* results use arguments. A produced file must be listed whether or not the
|
|
866
|
+
* model remembered to name it. Reads, unsupported
|
|
867
|
+
* tools, malformed calls, and failed results contribute nothing. Paths keep
|
|
868
|
+
* first-seen order and appear once, so a file written and then edited in the
|
|
869
|
+
* same turn is one entry.
|
|
870
|
+
*
|
|
871
|
+
* The Conversation Location index owns turn membership before this function
|
|
872
|
+
* runs, so paths cannot spill across turns and this derivation does not infer
|
|
873
|
+
* boundaries from neighboring presentation Nodes.
|
|
874
|
+
* @param data - engine-published Deliverables data for one Turn.
|
|
875
|
+
* @param seq - closing Assistant seq; later Tool settlements are excluded.
|
|
876
|
+
* @returns Produced paths in first-seen order; empty when the turn wrote nothing.
|
|
877
|
+
*/
|
|
878
|
+
function producedForClosing(data, seq = Number.POSITIVE_INFINITY) {
|
|
879
|
+
if (data === void 0) return [];
|
|
880
|
+
const paths = [];
|
|
881
|
+
const seen = /* @__PURE__ */ new Set();
|
|
882
|
+
for (const produced of data.produced) {
|
|
883
|
+
if (produced.seq > seq || seen.has(produced.path)) continue;
|
|
884
|
+
seen.add(produced.path);
|
|
885
|
+
paths.push(produced.path);
|
|
886
|
+
}
|
|
887
|
+
return paths;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Claim the turn-tail chain only when its closing turn produced files.
|
|
891
|
+
* @param owner - Turn-tail owner currency for the closing assistant.
|
|
892
|
+
* @returns Produced paths as the component's match, or null to decline before mount.
|
|
893
|
+
*/
|
|
894
|
+
function selectProducedFiles(owner) {
|
|
895
|
+
const paths = producedForClosing(owner.turn.data.get("deliverables"), owner.seq);
|
|
896
|
+
return paths.length === 0 ? null : paths;
|
|
897
|
+
}
|
|
898
|
+
/** Turn-local successful mutation accumulator; it publishes no view Node. */
|
|
899
|
+
const deliverablesDefinition = {
|
|
900
|
+
kind: "deliverables",
|
|
901
|
+
match: (event) => {
|
|
902
|
+
if (event.type === "turn/start") return {
|
|
903
|
+
id: String(event.data.turn),
|
|
904
|
+
role: "start"
|
|
905
|
+
};
|
|
906
|
+
if (event.type === "tool/call") return {
|
|
907
|
+
id: String(event.data.turn),
|
|
908
|
+
role: "update"
|
|
909
|
+
};
|
|
910
|
+
if (event.type === "deliverables/presented") return isPresentedData(event.data) ? {
|
|
911
|
+
id: String(event.data.turn),
|
|
912
|
+
role: "update"
|
|
913
|
+
} : null;
|
|
914
|
+
if (event.type === "workspace/changes") return isChangesEvent(event.data) ? {
|
|
915
|
+
id: String(event.data.turn),
|
|
916
|
+
role: "update"
|
|
917
|
+
} : null;
|
|
918
|
+
if (event.type === "tool/result" && isAppendSurfaceEvent(event)) return {
|
|
919
|
+
id: String(event.data.turn),
|
|
920
|
+
role: "update"
|
|
921
|
+
};
|
|
922
|
+
return null;
|
|
923
|
+
},
|
|
924
|
+
start: (_context, match) => {
|
|
925
|
+
if (match.event.type !== "turn/start") throw new Error("deliverables start requires turn/start");
|
|
926
|
+
return {
|
|
927
|
+
turn: match.event.data.turn,
|
|
928
|
+
calls: /* @__PURE__ */ new Map(),
|
|
929
|
+
produced: []
|
|
930
|
+
};
|
|
931
|
+
},
|
|
932
|
+
update: (context, match) => {
|
|
933
|
+
if (match.event.type === "workspace/changes") return {
|
|
934
|
+
...context.state,
|
|
935
|
+
changes: { seq: match.event.seq }
|
|
936
|
+
};
|
|
937
|
+
if (match.event.type === "deliverables/presented") {
|
|
938
|
+
const { files } = match.event.data;
|
|
939
|
+
const seq = match.event.seq;
|
|
940
|
+
const presented = [];
|
|
941
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
942
|
+
const file = files[index];
|
|
943
|
+
if (isPresentedFile(file)) presented.push({
|
|
944
|
+
...file,
|
|
945
|
+
seq,
|
|
946
|
+
index
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
if (presented.length === 0) return context.state;
|
|
950
|
+
return {
|
|
951
|
+
...context.state,
|
|
952
|
+
presented: [...context.state.presented ?? [], ...presented]
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
if (match.event.type === "tool/call") {
|
|
956
|
+
const calls = new Map(context.state.calls);
|
|
957
|
+
calls.set(String(match.event.data.callId), mutationPath(match.event.data.name, match.event.data.arguments));
|
|
958
|
+
return {
|
|
959
|
+
...context.state,
|
|
960
|
+
calls
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
if (match.event.type !== "tool/result") return context.state;
|
|
964
|
+
if (match.event.data.message.isError === true) return context.state;
|
|
965
|
+
const callId = String(match.event.data.message.source.callId);
|
|
966
|
+
const argumentPath = context.state.calls.get(callId);
|
|
967
|
+
if (argumentPath === null || argumentPath === void 0) return context.state;
|
|
968
|
+
const path = mutationResultPath(argumentPath, match.event.data.meta);
|
|
969
|
+
return path === null ? context.state : {
|
|
970
|
+
...context.state,
|
|
971
|
+
produced: [...context.state.produced, {
|
|
972
|
+
seq: match.event.seq,
|
|
973
|
+
path,
|
|
974
|
+
...path === argumentPath ? {} : { argumentPath }
|
|
975
|
+
}]
|
|
976
|
+
};
|
|
977
|
+
},
|
|
978
|
+
buildLocationData: (context, scope, previous) => {
|
|
979
|
+
if (scope !== "turn" || context.state === void 0) return null;
|
|
980
|
+
if (previous?.kind === "turn" && previous.turn === context.state.turn && previous.key === "deliverables" && previous.value.produced === context.state.produced && previous.value.presented === context.state.presented && previous.value.changes === context.state.changes) return previous;
|
|
981
|
+
return {
|
|
982
|
+
kind: "turn",
|
|
983
|
+
turn: context.state.turn,
|
|
984
|
+
key: "deliverables",
|
|
985
|
+
value: {
|
|
986
|
+
produced: context.state.produced,
|
|
987
|
+
...context.state.presented === void 0 ? {} : { presented: context.state.presented },
|
|
988
|
+
...context.state.changes === void 0 ? {} : { changes: context.state.changes }
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
/**
|
|
994
|
+
* The turn's latest change announcement.
|
|
995
|
+
* @param owner - closing turn.
|
|
996
|
+
* @returns the announcement, or null when the Host recorded none.
|
|
997
|
+
*/
|
|
998
|
+
function changesForClosing(owner) {
|
|
999
|
+
return owner.turn.data.get("deliverables")?.changes ?? null;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Select the latest declaration of each path before the closing reply.
|
|
1003
|
+
* @param owner - closing turn and sequence.
|
|
1004
|
+
* @returns replayable deliveries in first-seen path order.
|
|
1005
|
+
*/
|
|
1006
|
+
function presentedForClosing(owner) {
|
|
1007
|
+
const files = /* @__PURE__ */ new Map();
|
|
1008
|
+
for (const file of owner.turn.data.get("deliverables")?.presented ?? []) if (file.seq < owner.seq) files.set(file.path, file);
|
|
1009
|
+
return [...files.values()];
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Resolves inline-code references against one turn's produced or delivered
|
|
1013
|
+
* paths and explicit mutation arguments. A reference resolves only when every
|
|
1014
|
+
* exact or basename match identifies one target. Ambiguous and unknown tokens stay inert.
|
|
1015
|
+
* @param paths - The turn's produced or delivered paths, already deduplicated.
|
|
1016
|
+
* @param openFile - The chat view's file opener.
|
|
1017
|
+
* @param label - Localizes the accessible open-label for a resolved path.
|
|
1018
|
+
* @param operations - successful mutations whose literal arguments may name the same canonical targets.
|
|
1019
|
+
* @returns The resolver MarkdownText consumes; the full path rides `title`,
|
|
1020
|
+
* the same disambiguator the row's chips carry.
|
|
1021
|
+
*/
|
|
1022
|
+
function producedFileMentions(paths, openFile, label, operations = []) {
|
|
1023
|
+
return { resolve(value) {
|
|
1024
|
+
const matches = new Set(paths.filter((path) => path === value || basename(path) === value));
|
|
1025
|
+
for (const operation of operations) if (operation.argumentPath === value) matches.add(operation.path);
|
|
1026
|
+
const path = matches.size === 1 ? matches.values().next().value : void 0;
|
|
1027
|
+
if (path === void 0) return void 0;
|
|
1028
|
+
return {
|
|
1029
|
+
open: () => {
|
|
1030
|
+
openFile(path);
|
|
1031
|
+
},
|
|
1032
|
+
label: label(path),
|
|
1033
|
+
title: path
|
|
1034
|
+
};
|
|
1035
|
+
} };
|
|
1036
|
+
}
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-deliverables/src/client/Deliverables.module.css.mjs
|
|
1039
|
+
const css$1 = "._2Q7zSW_root{--deliverable-fill:var(--qilin-static-neutral-50);--deliverable-hover:var(--qilin-static-neutral-100);flex-direction:column;gap:16px;min-width:0;margin-top:4px;display:flex;container-type:inline-size}._2Q7zSW_root[data-after-changes=true]{margin-top:0}body[data-ds-dark-theme] ._2Q7zSW_root{--deliverable-fill:var(--qilin-static-neutral-850);--deliverable-hover:var(--qilin-static-neutral-800)}._2Q7zSW_hostStatus{color:var(--qilin-alias-label-secondary);align-items:center;gap:8px;font-size:12px;line-height:18px;display:flex}._2Q7zSW_presented{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;min-width:0;display:grid}._2Q7zSW_presented[data-single=true]{grid-template-columns:minmax(0,1fr)}._2Q7zSW_file{box-sizing:border-box;border:.5px solid var(--qilin-alias-border-l1);background:var(--deliverable-fill);min-width:0;height:60px;color:var(--qilin-alias-label-primary);border-radius:18px;align-items:center;gap:10px;padding:8px 10px;transition:background-color .12s;display:flex;position:relative;overflow:hidden}._2Q7zSW_file:hover{background:var(--deliverable-hover)}._2Q7zSW_cardPreview{z-index:1;border-radius:inherit;cursor:pointer;background:0 0;border:0;width:100%;padding:0;position:absolute;inset:0}._2Q7zSW_cardPreview:focus-visible{box-shadow:inset 0 0 0 2px var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));outline:none}._2Q7zSW_fileIcon{z-index:2;box-sizing:border-box;pointer-events:none;border:.5px solid var(--qilin-alias-border-l1);background:var(--deliverable-fill);width:40px;height:40px;color:var(--qilin-alias-link);border-radius:10px;flex:none;place-items:center;display:grid;position:relative;overflow:hidden}._2Q7zSW_fileBody{z-index:2;pointer-events:none;flex:1;justify-content:space-between;align-items:center;gap:12px;min-width:0;display:flex;position:relative}._2Q7zSW_details{flex-direction:column;flex:1;justify-content:center;gap:2px;min-width:0;display:flex}._2Q7zSW_fileName{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}._2Q7zSW_description{color:var(--qilin-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:10px;font-weight:400;line-height:16px;overflow:hidden}._2Q7zSW_description[data-error=true]{color:var(--qilin-alias-state-error-primary)}._2Q7zSW_previewHint,._2Q7zSW_file:hover ._2Q7zSW_secondaryText{display:none}._2Q7zSW_file:hover ._2Q7zSW_previewHint{display:inline}._2Q7zSW_split{box-sizing:border-box;pointer-events:auto;border:.5px solid var(--qilin-alias-border-l3);background:var(--qilin-alias-button-floating-fill);border-radius:10px;flex:none;align-items:stretch;height:28px;display:inline-flex;overflow:hidden}._2Q7zSW_menuAnchor{align-self:stretch}._2Q7zSW_open,._2Q7zSW_chevron{color:var(--qilin-alias-label-primary);cursor:pointer;font-family:var(--qilin-font-family);background:0 0;border:0;justify-content:center;align-items:center;display:inline-flex}._2Q7zSW_open{padding:4px 8px;font-size:12px;line-height:18px}._2Q7zSW_chevron{border-left:.5px solid var(--qilin-alias-border-l3);color:var(--qilin-alias-label-secondary);padding:4px 5px}._2Q7zSW_open:hover,._2Q7zSW_open:focus-visible,._2Q7zSW_chevron:hover:not(:disabled),._2Q7zSW_chevron:focus-visible{background:var(--qilin-alias-interactive-bg-hover)}._2Q7zSW_chevron:disabled{color:var(--qilin-alias-label-dimmed);cursor:not-allowed}._2Q7zSW_menuActionIcon{width:16px;height:16px;display:block}._2Q7zSW_toggle{min-width:0;color:var(--qilin-alias-label-tertiary);cursor:pointer;font:inherit;background:0 0;border:0;border-radius:8px;align-self:center;align-items:center;gap:4px;padding:1px 11px;font-size:12px;line-height:18px;display:inline-flex}._2Q7zSW_toggle:hover{background:var(--qilin-alias-interactive-bg-hover)}._2Q7zSW_toggle svg{flex:none;width:14px;height:14px}@container (width<=620px){._2Q7zSW_presented{grid-template-columns:minmax(0,1fr)}}@media (pointer:coarse){._2Q7zSW_split{min-height:44px}._2Q7zSW_open,._2Q7zSW_chevron{min-width:44px}}";
|
|
1040
|
+
const tagId$1 = "@qilin-agent/client-ui-deliverables/Deliverables.module.css";
|
|
1041
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
1042
|
+
const tag = document.createElement("style");
|
|
1043
|
+
tag.dataset.plugin = "@qilin-agent/client-ui-deliverables";
|
|
1044
|
+
tag.dataset.pluginCss = tagId$1;
|
|
1045
|
+
tag.textContent = css$1;
|
|
1046
|
+
document.head.appendChild(tag);
|
|
1047
|
+
}
|
|
1048
|
+
var Deliverables_module_css_default = {
|
|
1049
|
+
"cardPreview": "_2Q7zSW_cardPreview",
|
|
1050
|
+
"chevron": "_2Q7zSW_chevron",
|
|
1051
|
+
"description": "_2Q7zSW_description",
|
|
1052
|
+
"details": "_2Q7zSW_details",
|
|
1053
|
+
"file": "_2Q7zSW_file",
|
|
1054
|
+
"fileBody": "_2Q7zSW_fileBody",
|
|
1055
|
+
"fileIcon": "_2Q7zSW_fileIcon",
|
|
1056
|
+
"fileName": "_2Q7zSW_fileName",
|
|
1057
|
+
"hostStatus": "_2Q7zSW_hostStatus",
|
|
1058
|
+
"menuActionIcon": "_2Q7zSW_menuActionIcon",
|
|
1059
|
+
"menuAnchor": "_2Q7zSW_menuAnchor",
|
|
1060
|
+
"open": "_2Q7zSW_open",
|
|
1061
|
+
"presented": "_2Q7zSW_presented",
|
|
1062
|
+
"previewHint": "_2Q7zSW_previewHint",
|
|
1063
|
+
"root": "_2Q7zSW_root",
|
|
1064
|
+
"secondaryText": "_2Q7zSW_secondaryText",
|
|
1065
|
+
"split": "_2Q7zSW_split",
|
|
1066
|
+
"toggle": "_2Q7zSW_toggle"
|
|
1067
|
+
};
|
|
1068
|
+
//#endregion
|
|
1069
|
+
//#region lib/types/client/PresentedFileCard.js
|
|
1070
|
+
/** File identity and explicit default-app or file-manager actions for one delivery. */
|
|
1071
|
+
function cardDescription(description, fallback) {
|
|
1072
|
+
const trimmed = description?.replace(/\s*(?:\([^()]*\)|([^()]*))\s*$/u, "").trim();
|
|
1073
|
+
return trimmed === void 0 || trimmed === "" ? fallback : trimmed;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Render independent file actions without nesting buttons inside a clickable card.
|
|
1077
|
+
* @param props - durable file metadata, Sidebar preview, Host capabilities, gesture status, and localized copy.
|
|
1078
|
+
* @returns the file card and its anchored action menu.
|
|
1079
|
+
*/
|
|
1080
|
+
function PresentedFileCard({ file, cwd, phase, host, onPreview, onAction, t }) {
|
|
1081
|
+
const [menuOpen, setMenuOpen] = (0, react.useState)(false);
|
|
1082
|
+
const previewRef = (0, react.useRef)(null);
|
|
1083
|
+
const menuDisabled = phase === "opening" || phase === "revealing" || host === null || !host.available;
|
|
1084
|
+
if (menuDisabled && menuOpen) setMenuOpen(false);
|
|
1085
|
+
const reveal = host?.fileManager ?? "directory";
|
|
1086
|
+
const act = (action) => {
|
|
1087
|
+
setMenuOpen(false);
|
|
1088
|
+
previewRef.current?.focus();
|
|
1089
|
+
onAction(action);
|
|
1090
|
+
};
|
|
1091
|
+
const name = basename(file.path);
|
|
1092
|
+
const metadata = (0, _qilin_agent_client_ui_primitives.fileExtension)(name).toUpperCase() || t("presented.file");
|
|
1093
|
+
const status = phase === void 0 ? cardDescription(file.description, metadata) : t(reveal === "directory" && phase === "revealed" ? "presented.directoryOpened" : reveal === "directory" && phase === "revealing" ? "presented.directoryOpening" : reveal === "directory" && phase === "revealError" ? "presented.directoryError" : `presented.${phase}`);
|
|
1094
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
1095
|
+
className: Deliverables_module_css_default.file,
|
|
1096
|
+
"data-presented-file": true,
|
|
1097
|
+
children: [
|
|
1098
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1099
|
+
type: "button",
|
|
1100
|
+
className: Deliverables_module_css_default.cardPreview,
|
|
1101
|
+
title: resolveWorkspacePath(cwd, file.path),
|
|
1102
|
+
"aria-label": t("presented.previewCard", { name: file.path }),
|
|
1103
|
+
onClick: onPreview
|
|
1104
|
+
}),
|
|
1105
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1106
|
+
className: Deliverables_module_css_default.fileIcon,
|
|
1107
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.FileTypeIcon, {
|
|
1108
|
+
path: file.path,
|
|
1109
|
+
size: 20
|
|
1110
|
+
})
|
|
1111
|
+
}),
|
|
1112
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1113
|
+
className: Deliverables_module_css_default.fileBody,
|
|
1114
|
+
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
1115
|
+
className: Deliverables_module_css_default.details,
|
|
1116
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1117
|
+
className: Deliverables_module_css_default.fileName,
|
|
1118
|
+
children: name
|
|
1119
|
+
}), (0, react_jsx_runtime.jsxs)("span", {
|
|
1120
|
+
className: Deliverables_module_css_default.description,
|
|
1121
|
+
role: phase === void 0 ? void 0 : "status",
|
|
1122
|
+
"data-error": phase === "error" || phase === "revealError" || phase === "nativeUnavailable" ? true : void 0,
|
|
1123
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1124
|
+
className: Deliverables_module_css_default.secondaryText,
|
|
1125
|
+
children: status
|
|
1126
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
1127
|
+
className: Deliverables_module_css_default.previewHint,
|
|
1128
|
+
children: t("presented.preview")
|
|
1129
|
+
})]
|
|
1130
|
+
})]
|
|
1131
|
+
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
1132
|
+
className: Deliverables_module_css_default.split,
|
|
1133
|
+
children: [(0, react_jsx_runtime.jsx)("button", {
|
|
1134
|
+
ref: previewRef,
|
|
1135
|
+
type: "button",
|
|
1136
|
+
className: Deliverables_module_css_default.open,
|
|
1137
|
+
"aria-label": t("presented.previewButton", { name: file.path }),
|
|
1138
|
+
onClick: onPreview,
|
|
1139
|
+
children: t("presented.action")
|
|
1140
|
+
}), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Menu, {
|
|
1141
|
+
className: Deliverables_module_css_default.menuAnchor,
|
|
1142
|
+
open: menuOpen && !menuDisabled,
|
|
1143
|
+
autoFocus: true,
|
|
1144
|
+
portal: true,
|
|
1145
|
+
align: "end",
|
|
1146
|
+
onClose: () => {
|
|
1147
|
+
setMenuOpen(false);
|
|
1148
|
+
},
|
|
1149
|
+
anchor: (0, react_jsx_runtime.jsx)("button", {
|
|
1150
|
+
type: "button",
|
|
1151
|
+
className: Deliverables_module_css_default.chevron,
|
|
1152
|
+
disabled: menuDisabled,
|
|
1153
|
+
"aria-haspopup": "menu",
|
|
1154
|
+
"aria-expanded": menuOpen && !menuDisabled,
|
|
1155
|
+
"aria-label": t("presented.more", { name: file.path }),
|
|
1156
|
+
onClick: () => {
|
|
1157
|
+
setMenuOpen((value) => !value);
|
|
1158
|
+
},
|
|
1159
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, { size: 11 })
|
|
1160
|
+
}),
|
|
1161
|
+
items: [{
|
|
1162
|
+
id: "open",
|
|
1163
|
+
icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconRightUpOutline16, {
|
|
1164
|
+
size: 16,
|
|
1165
|
+
className: Deliverables_module_css_default.menuActionIcon
|
|
1166
|
+
}),
|
|
1167
|
+
label: t("presented.defaultApp")
|
|
1168
|
+
}, {
|
|
1169
|
+
id: "reveal",
|
|
1170
|
+
icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconFolderOpenOutline16, {}),
|
|
1171
|
+
label: t(`presented.${reveal}`)
|
|
1172
|
+
}],
|
|
1173
|
+
onSelect: (id) => {
|
|
1174
|
+
act(id === "reveal" ? "reveal" : "open");
|
|
1175
|
+
}
|
|
1176
|
+
})]
|
|
1177
|
+
})]
|
|
1178
|
+
})
|
|
1179
|
+
]
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
//#endregion
|
|
1183
|
+
//#region lib/types/client/Deliverables.js
|
|
1184
|
+
/** The changed-files card, shown only while the Host serves the turn's summary, and explicitly declared files for a closing turn. */
|
|
1185
|
+
const COLLAPSED_PRESENTED_COUNT = 4;
|
|
1186
|
+
/**
|
|
1187
|
+
* Claim turns with a change announcement or declared files.
|
|
1188
|
+
* @param owner - closing turn.
|
|
1189
|
+
* @returns matched announcement and deliveries, or null for a turn with neither.
|
|
1190
|
+
*/
|
|
1191
|
+
function selectDeliverables(owner) {
|
|
1192
|
+
const changes = changesForClosing(owner);
|
|
1193
|
+
const presented = presentedForClosing(owner);
|
|
1194
|
+
return changes === null && presented.length === 0 ? null : {
|
|
1195
|
+
changes,
|
|
1196
|
+
presented
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Contribute file deliveries alongside other completed-Turn artifacts.
|
|
1201
|
+
* @param props - closing Turn, file actions, and localized copy.
|
|
1202
|
+
* @returns file rows, or null when the Turn declares none.
|
|
1203
|
+
*/
|
|
1204
|
+
function DeliverablesTail(props) {
|
|
1205
|
+
const matched = selectDeliverables(props);
|
|
1206
|
+
return matched === null ? null : (0, react_jsx_runtime.jsx)(Deliverables, {
|
|
1207
|
+
...props,
|
|
1208
|
+
matched
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Render the changed-files card, once the Host has served the announced
|
|
1213
|
+
* summary and it lists a file, and default-application buttons for declared
|
|
1214
|
+
* files. A summary the Host no longer serves leaves no card.
|
|
1215
|
+
* @param props - matched announcement and files, workspace opener, and localized copy.
|
|
1216
|
+
* @returns the closing turn's file rows.
|
|
1217
|
+
*/
|
|
1218
|
+
function Deliverables({ matched, openFile, t, sessionId, useSessions, openPresented, openChangesReview, usePresentedOpen, usePresentedHost, useChangesSummary, reloadPresentedHost, loadChangesSummary }) {
|
|
1219
|
+
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
1220
|
+
const cwd = useSessions((state) => state.byId[sessionId]?.cwd);
|
|
1221
|
+
const states = usePresentedOpen((value) => value);
|
|
1222
|
+
const host = usePresentedHost((value) => value);
|
|
1223
|
+
const announced = matched.changes;
|
|
1224
|
+
const summary = useChangesSummary((value) => announced === null ? void 0 : value[changesSummaryUrl(sessionId, announced.seq)]);
|
|
1225
|
+
(0, react.useEffect)(() => {
|
|
1226
|
+
if (announced !== null && summary === void 0) loadChangesSummary(sessionId, announced.seq);
|
|
1227
|
+
}, [
|
|
1228
|
+
announced,
|
|
1229
|
+
summary,
|
|
1230
|
+
sessionId,
|
|
1231
|
+
loadChangesSummary
|
|
1232
|
+
]);
|
|
1233
|
+
const changes = announced !== null && typeof summary === "object" && summary.files.length > 0 ? {
|
|
1234
|
+
seq: announced.seq,
|
|
1235
|
+
...summary
|
|
1236
|
+
} : null;
|
|
1237
|
+
const collapsible = matched.presented.length > COLLAPSED_PRESENTED_COUNT;
|
|
1238
|
+
const presented = collapsible && !expanded ? matched.presented.slice(0, COLLAPSED_PRESENTED_COUNT) : matched.presented;
|
|
1239
|
+
(0, react.useEffect)(() => {
|
|
1240
|
+
if (host === null) reloadPresentedHost();
|
|
1241
|
+
}, [host, reloadPresentedHost]);
|
|
1242
|
+
return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [changes !== null && (0, react_jsx_runtime.jsx)(ChangedFiles, {
|
|
1243
|
+
changes,
|
|
1244
|
+
cwd,
|
|
1245
|
+
t,
|
|
1246
|
+
openReview: (index) => {
|
|
1247
|
+
openChangesReview({
|
|
1248
|
+
sessionId,
|
|
1249
|
+
seq: changes.seq,
|
|
1250
|
+
turn: changes.turn
|
|
1251
|
+
}, index);
|
|
1252
|
+
}
|
|
1253
|
+
}), matched.presented.length > 0 && (0, react_jsx_runtime.jsxs)("div", {
|
|
1254
|
+
className: Deliverables_module_css_default.root,
|
|
1255
|
+
"data-after-changes": changes !== null || void 0,
|
|
1256
|
+
children: [
|
|
1257
|
+
host === "error" && (0, react_jsx_runtime.jsxs)("div", {
|
|
1258
|
+
className: Deliverables_module_css_default.hostStatus,
|
|
1259
|
+
children: [(0, react_jsx_runtime.jsx)("span", { children: t("presented.hostError") }), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
|
|
1260
|
+
size: "sm",
|
|
1261
|
+
onClick: () => {
|
|
1262
|
+
reloadPresentedHost();
|
|
1263
|
+
},
|
|
1264
|
+
children: t("presented.retry")
|
|
1265
|
+
})]
|
|
1266
|
+
}),
|
|
1267
|
+
host !== null && host !== "error" && !host.available && (0, react_jsx_runtime.jsx)("span", {
|
|
1268
|
+
className: Deliverables_module_css_default.hostStatus,
|
|
1269
|
+
children: t("presented.unavailable")
|
|
1270
|
+
}),
|
|
1271
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
1272
|
+
className: Deliverables_module_css_default.presented,
|
|
1273
|
+
"data-presented-files-row": true,
|
|
1274
|
+
"data-single": matched.presented.length === 1 ? true : void 0,
|
|
1275
|
+
children: presented.map((file) => (0, react_jsx_runtime.jsx)(PresentedFileCard, {
|
|
1276
|
+
file,
|
|
1277
|
+
cwd,
|
|
1278
|
+
phase: states[presentedFileUrl(sessionId, file.seq, file.index)],
|
|
1279
|
+
host: host === "error" ? null : host,
|
|
1280
|
+
t,
|
|
1281
|
+
onPreview: () => {
|
|
1282
|
+
openFile(file.path);
|
|
1283
|
+
},
|
|
1284
|
+
onAction: (action) => {
|
|
1285
|
+
openPresented(sessionId, file.seq, file.index, action);
|
|
1286
|
+
}
|
|
1287
|
+
}, `${file.seq}:${file.index}`))
|
|
1288
|
+
}),
|
|
1289
|
+
collapsible && (0, react_jsx_runtime.jsxs)("button", {
|
|
1290
|
+
type: "button",
|
|
1291
|
+
className: Deliverables_module_css_default.toggle,
|
|
1292
|
+
"aria-expanded": expanded,
|
|
1293
|
+
"aria-label": t(expanded ? "presented.collapseAria" : "presented.expandAria", { count: matched.presented.length }),
|
|
1294
|
+
onClick: () => {
|
|
1295
|
+
setExpanded((value) => !value);
|
|
1296
|
+
},
|
|
1297
|
+
children: [(0, react_jsx_runtime.jsx)("span", { children: t(expanded ? "presented.collapse" : "presented.all", { count: matched.presented.length }) }), expanded ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronUpOutline14, {}) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, {})]
|
|
1298
|
+
})
|
|
1299
|
+
]
|
|
1300
|
+
})] });
|
|
1301
|
+
}
|
|
1302
|
+
//#endregion
|
|
1303
|
+
//#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-deliverables/src/client/ReviewTab.module.css.mjs
|
|
1304
|
+
const css = ".RTyLca_root{box-sizing:border-box;width:100%;height:100%;min-height:0;color:var(--qilin-alias-label-primary);flex-direction:column;display:flex}.RTyLca_header{box-sizing:border-box;border-bottom:.5px solid var(--qilin-alias-border-l3);flex:none;align-items:center;gap:6px;height:38px;padding:0 6px 0 8px;display:flex}.RTyLca_selector{flex:0 auto;min-width:0}.RTyLca_selectorLabel{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;font-size:12px;overflow:hidden}.RTyLca_selectorButton{box-sizing:border-box;max-width:100%;height:28px;color:var(--qilin-alias-label-primary);cursor:pointer;font:inherit;background:0 0;border:0;border-radius:8px;align-items:center;gap:4px;padding:0 6px 0 8px;font-size:12px;display:inline-flex}.RTyLca_selectorButton:hover,.RTyLca_selectorButton[aria-expanded=true]{background:var(--qilin-alias-interactive-bg-hover)}.RTyLca_selectorText{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.RTyLca_item{justify-content:space-between;align-items:center;gap:12px;min-width:0;display:flex}.RTyLca_itemPath{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.RTyLca_itemCounts,.RTyLca_counts{font-family:var(--ds-font-family-code);color:var(--qilin-alias-label-tertiary);flex:none;gap:6px;font-size:12px;display:inline-flex}.RTyLca_counts{min-width:0;margin-right:auto}.RTyLca_added{color:var(--qilin-alias-state-success-primary)}.RTyLca_deleted{color:var(--qilin-alias-state-error-primary)}.RTyLca_label{color:var(--qilin-alias-label-tertiary)}.RTyLca_tools{flex:none;align-items:center;gap:2px;margin-left:auto;display:inline-flex}.RTyLca_tool{width:28px;height:28px;color:var(--qilin-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:8px;flex:none;justify-content:center;align-items:center;padding:6px;display:inline-flex}.RTyLca_tool svg{width:15px;height:15px}.RTyLca_tool:hover:not(:disabled),.RTyLca_tool[aria-pressed=true]{color:var(--qilin-alias-label-primary);background:var(--qilin-alias-interactive-bg-hover)}.RTyLca_tool:disabled{cursor:progress}.RTyLca_tool[data-error]{color:var(--qilin-alias-state-error-primary)}.RTyLca_status{color:var(--qilin-alias-label-secondary);align-items:center;gap:12px;margin:0;padding:16px;font-size:13px;display:flex}.RTyLca_body{min-height:0;font:var(--qilin-font-markdown-code-block);flex-direction:column;flex:auto;padding:8px 0 16px;display:flex;overflow:auto}.RTyLca_columns{flex:auto;grid-template-columns:minmax(0,1fr) minmax(0,1fr);min-height:0;display:grid}.RTyLca_column{min-width:0;overflow-x:auto}.RTyLca_column+.RTyLca_column{border-left:.5px solid var(--qilin-alias-border-l3)}.RTyLca_sideLine{box-sizing:border-box;white-space:pre;grid-template-columns:3.5em max-content;width:max-content;min-width:100%;min-height:22px;line-height:22px;display:grid}.RTyLca_note{font:var(--qilin-font-xs-13);color:var(--qilin-alias-label-tertiary);margin:0;padding:4px 16px 8px}.RTyLca_hunk{margin-bottom:8px}.RTyLca_hunkHeader{color:var(--qilin-alias-label-tertiary);white-space:pre;padding:4px 16px}.RTyLca_line{white-space:pre;grid-template-columns:3.5em 3.5em 1.2em minmax(0,1fr);min-height:22px;line-height:22px;display:grid}.RTyLca_splitLine{white-space:pre;grid-template-columns:minmax(0,1fr) minmax(0,1fr);min-height:22px;line-height:22px;display:grid}.RTyLca_cell{grid-template-columns:3.5em minmax(0,1fr);min-width:0;display:grid}.RTyLca_cell+.RTyLca_cell{border-left:.5px solid var(--qilin-alias-border-l3)}.RTyLca_number{color:var(--qilin-alias-label-tertiary);text-align:right;user-select:none;padding-right:8px}.RTyLca_sign{text-align:center;user-select:none}.RTyLca_text{padding-right:16px}.RTyLca_body[data-review-wrap] .RTyLca_line,.RTyLca_body[data-review-wrap] .RTyLca_splitLine{white-space:pre-wrap}.RTyLca_body[data-review-wrap] .RTyLca_text{overflow-wrap:anywhere}.RTyLca_add{background:color-mix(in srgb, var(--qilin-alias-state-success-primary) 12%, transparent)}.RTyLca_add .RTyLca_sign,.RTyLca_add .RTyLca_text{color:var(--qilin-alias-state-success-primary)}.RTyLca_del{background:color-mix(in srgb, var(--qilin-alias-state-error-primary) 12%, transparent)}.RTyLca_del .RTyLca_sign,.RTyLca_del .RTyLca_text{color:var(--qilin-alias-state-error-primary)}.RTyLca_context .RTyLca_text{color:var(--qilin-alias-label-secondary)}.RTyLca_empty{background:var(--qilin-alias-interactive-bg-hover)}";
|
|
1305
|
+
const tagId = "@qilin-agent/client-ui-deliverables/ReviewTab.module.css";
|
|
1306
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1307
|
+
const tag = document.createElement("style");
|
|
1308
|
+
tag.dataset.plugin = "@qilin-agent/client-ui-deliverables";
|
|
1309
|
+
tag.dataset.pluginCss = tagId;
|
|
1310
|
+
tag.textContent = css;
|
|
1311
|
+
document.head.appendChild(tag);
|
|
1312
|
+
}
|
|
1313
|
+
var ReviewTab_module_css_default = {
|
|
1314
|
+
"add": "RTyLca_add",
|
|
1315
|
+
"added": "RTyLca_added",
|
|
1316
|
+
"body": "RTyLca_body",
|
|
1317
|
+
"cell": "RTyLca_cell",
|
|
1318
|
+
"column": "RTyLca_column",
|
|
1319
|
+
"columns": "RTyLca_columns",
|
|
1320
|
+
"context": "RTyLca_context",
|
|
1321
|
+
"counts": "RTyLca_counts",
|
|
1322
|
+
"del": "RTyLca_del",
|
|
1323
|
+
"deleted": "RTyLca_deleted",
|
|
1324
|
+
"empty": "RTyLca_empty",
|
|
1325
|
+
"header": "RTyLca_header",
|
|
1326
|
+
"hunk": "RTyLca_hunk",
|
|
1327
|
+
"hunkHeader": "RTyLca_hunkHeader",
|
|
1328
|
+
"item": "RTyLca_item",
|
|
1329
|
+
"itemCounts": "RTyLca_itemCounts",
|
|
1330
|
+
"itemPath": "RTyLca_itemPath",
|
|
1331
|
+
"label": "RTyLca_label",
|
|
1332
|
+
"line": "RTyLca_line",
|
|
1333
|
+
"note": "RTyLca_note",
|
|
1334
|
+
"number": "RTyLca_number",
|
|
1335
|
+
"root": "RTyLca_root",
|
|
1336
|
+
"selector": "RTyLca_selector",
|
|
1337
|
+
"selectorButton": "RTyLca_selectorButton",
|
|
1338
|
+
"selectorLabel": "RTyLca_selectorLabel",
|
|
1339
|
+
"selectorText": "RTyLca_selectorText",
|
|
1340
|
+
"sideLine": "RTyLca_sideLine",
|
|
1341
|
+
"sign": "RTyLca_sign",
|
|
1342
|
+
"splitLine": "RTyLca_splitLine",
|
|
1343
|
+
"status": "RTyLca_status",
|
|
1344
|
+
"text": "RTyLca_text",
|
|
1345
|
+
"tool": "RTyLca_tool",
|
|
1346
|
+
"tools": "RTyLca_tools"
|
|
1347
|
+
};
|
|
1348
|
+
//#endregion
|
|
1349
|
+
//#region lib/types/client/ReviewTab.js
|
|
1350
|
+
/**
|
|
1351
|
+
* The review tab: one turn's changed files behind a file selector, with the
|
|
1352
|
+
* selected file's turn-start and turn-end comparison drawn unified or side by
|
|
1353
|
+
* side, wrapped or scrolling, and controls to open the file itself.
|
|
1354
|
+
*/
|
|
1355
|
+
/** Lines drawn before the tab stops; a coarse comparison of a file near the byte cap would otherwise draw every line. */
|
|
1356
|
+
const MAX_RENDERED_LINES = 5e3;
|
|
1357
|
+
const GROUPED = new Intl.NumberFormat("en-US");
|
|
1358
|
+
/**
|
|
1359
|
+
* Number a hunk's lines: context lines count on both sides, deletions on the
|
|
1360
|
+
* old side, additions on the new side.
|
|
1361
|
+
* @param hunk - a served hunk.
|
|
1362
|
+
* @returns the rows in order.
|
|
1363
|
+
*/
|
|
1364
|
+
function hunkRows(hunk) {
|
|
1365
|
+
let oldNo = hunk.oldStart;
|
|
1366
|
+
let newNo = hunk.newStart;
|
|
1367
|
+
return hunk.lines.map((line) => {
|
|
1368
|
+
const text = line.slice(1);
|
|
1369
|
+
switch (line[0]) {
|
|
1370
|
+
case "+": return {
|
|
1371
|
+
kind: "add",
|
|
1372
|
+
old: void 0,
|
|
1373
|
+
new: newNo++,
|
|
1374
|
+
text
|
|
1375
|
+
};
|
|
1376
|
+
case "-": return {
|
|
1377
|
+
kind: "del",
|
|
1378
|
+
old: oldNo++,
|
|
1379
|
+
new: void 0,
|
|
1380
|
+
text
|
|
1381
|
+
};
|
|
1382
|
+
default: return {
|
|
1383
|
+
kind: "context",
|
|
1384
|
+
old: oldNo++,
|
|
1385
|
+
new: newNo++,
|
|
1386
|
+
text
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Pair a hunk's lines for the side-by-side view: each run of deletions is
|
|
1393
|
+
* aligned with the run of additions that follows it, row by row, and context
|
|
1394
|
+
* lines sit on both sides.
|
|
1395
|
+
* @param hunk - a served hunk.
|
|
1396
|
+
* @returns the rows in order.
|
|
1397
|
+
*/
|
|
1398
|
+
function splitRows(hunk) {
|
|
1399
|
+
const rows = [];
|
|
1400
|
+
let dels = [];
|
|
1401
|
+
let adds = [];
|
|
1402
|
+
const flush = () => {
|
|
1403
|
+
for (let at = 0; at < Math.max(dels.length, adds.length); at += 1) {
|
|
1404
|
+
const left = dels[at];
|
|
1405
|
+
const right = adds[at];
|
|
1406
|
+
rows.push({
|
|
1407
|
+
...left === void 0 ? {} : { left },
|
|
1408
|
+
...right === void 0 ? {} : { right }
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
dels = [];
|
|
1412
|
+
adds = [];
|
|
1413
|
+
};
|
|
1414
|
+
for (const row of hunkRows(hunk)) if (row.kind === "del") dels.push({
|
|
1415
|
+
no: row.old,
|
|
1416
|
+
text: row.text,
|
|
1417
|
+
kind: "del"
|
|
1418
|
+
});
|
|
1419
|
+
else if (row.kind === "add") adds.push({
|
|
1420
|
+
no: row.new,
|
|
1421
|
+
text: row.text,
|
|
1422
|
+
kind: "add"
|
|
1423
|
+
});
|
|
1424
|
+
else {
|
|
1425
|
+
flush();
|
|
1426
|
+
rows.push({
|
|
1427
|
+
left: {
|
|
1428
|
+
no: row.old,
|
|
1429
|
+
text: row.text,
|
|
1430
|
+
kind: "context"
|
|
1431
|
+
},
|
|
1432
|
+
right: {
|
|
1433
|
+
no: row.new,
|
|
1434
|
+
text: row.text,
|
|
1435
|
+
kind: "context"
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
flush();
|
|
1440
|
+
return rows;
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* The hunks to draw, cut at {@link MAX_RENDERED_LINES} lines in total.
|
|
1444
|
+
* @param hunks - served hunks.
|
|
1445
|
+
* @returns the hunks with the last one shortened as needed, and whether anything was cut.
|
|
1446
|
+
*/
|
|
1447
|
+
function renderedHunks(hunks) {
|
|
1448
|
+
let budget = MAX_RENDERED_LINES;
|
|
1449
|
+
const kept = [];
|
|
1450
|
+
for (const hunk of hunks) {
|
|
1451
|
+
if (budget === 0) return {
|
|
1452
|
+
hunks: kept,
|
|
1453
|
+
truncated: true
|
|
1454
|
+
};
|
|
1455
|
+
kept.push(hunk.lines.length <= budget ? hunk : {
|
|
1456
|
+
...hunk,
|
|
1457
|
+
lines: hunk.lines.slice(0, budget)
|
|
1458
|
+
});
|
|
1459
|
+
budget -= Math.min(budget, hunk.lines.length);
|
|
1460
|
+
}
|
|
1461
|
+
return {
|
|
1462
|
+
hunks: kept,
|
|
1463
|
+
truncated: hunks.some((hunk, at) => kept[at] !== hunk)
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
/** The one-line fact about a text comparison worth stating above its hunks, if any. */
|
|
1467
|
+
function noteOf(diff) {
|
|
1468
|
+
if (!diff.before) return "diff.created";
|
|
1469
|
+
if (!diff.after) return "diff.deleted";
|
|
1470
|
+
if (diff.hunks.length === 0) return "diff.unchanged";
|
|
1471
|
+
}
|
|
1472
|
+
/** The file index a navigation names, when it names one. */
|
|
1473
|
+
function navigatedIndex(params) {
|
|
1474
|
+
const index = params?.index;
|
|
1475
|
+
return typeof index === "number" && Number.isSafeInteger(index) && index >= 0 ? index : void 0;
|
|
1476
|
+
}
|
|
1477
|
+
/** Added and deleted line counts in the card's colors. */
|
|
1478
|
+
function Counts({ file, t }) {
|
|
1479
|
+
if (file.binary === true) return (0, react_jsx_runtime.jsx)("span", {
|
|
1480
|
+
className: ReviewTab_module_css_default.label,
|
|
1481
|
+
children: t("changes.binary")
|
|
1482
|
+
});
|
|
1483
|
+
if (file.oversized === true) return (0, react_jsx_runtime.jsx)("span", {
|
|
1484
|
+
className: ReviewTab_module_css_default.label,
|
|
1485
|
+
children: t("changes.oversized")
|
|
1486
|
+
});
|
|
1487
|
+
return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1488
|
+
className: ReviewTab_module_css_default.added,
|
|
1489
|
+
children: t("changes.added", { count: GROUPED.format(file.added) })
|
|
1490
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
1491
|
+
className: ReviewTab_module_css_default.deleted,
|
|
1492
|
+
children: t("changes.deleted", { count: GROUPED.format(file.deleted) })
|
|
1493
|
+
})] });
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* The review type's body, registered under `sidebar.right.pane.tab` as `changes-review`.
|
|
1497
|
+
* @param props - composed slot props.
|
|
1498
|
+
* @returns the selected file's comparison behind the file selector, or the state that stands in for it.
|
|
1499
|
+
*/
|
|
1500
|
+
function ReviewTab({ useTabInfo, sessionId, useSessions, useStore, actions, useChangesSummary, useChangesDiff, usePresentedOpen, usePresentedHost, loadChangesSummary, loadChangesDiff, reloadPresentedHost, openChanged, t }) {
|
|
1501
|
+
const { tab } = useTabInfo();
|
|
1502
|
+
const { navigation, signal } = tab;
|
|
1503
|
+
const coordinates = (0, react.useMemo)(() => parseChangesReviewAddress(tab.contentId), [tab.contentId]);
|
|
1504
|
+
if (coordinates === void 0) throw new Error(`ui-deliverables: not a review address "${tab.contentId}"`);
|
|
1505
|
+
const { seq } = coordinates;
|
|
1506
|
+
const cwd = useSessions((sessions) => sessions.byId[sessionId]?.cwd);
|
|
1507
|
+
const summary = useChangesSummary((value) => value[changesSummaryUrl(sessionId, seq)]);
|
|
1508
|
+
const state = useStore((store) => store.byTab[tab.id]);
|
|
1509
|
+
const host = usePresentedHost((value) => value);
|
|
1510
|
+
(0, react.useEffect)(() => {
|
|
1511
|
+
if (state?.navigated === navigation.revision) return;
|
|
1512
|
+
actions.navigated(tab.id, navigation.revision, navigatedIndex(navigation.params) ?? state?.index ?? 0);
|
|
1513
|
+
}, [
|
|
1514
|
+
state,
|
|
1515
|
+
navigation.revision,
|
|
1516
|
+
navigation.params,
|
|
1517
|
+
actions,
|
|
1518
|
+
tab.id
|
|
1519
|
+
]);
|
|
1520
|
+
(0, react.useEffect)(() => {
|
|
1521
|
+
const forget = () => {
|
|
1522
|
+
actions.forget(tab.id);
|
|
1523
|
+
};
|
|
1524
|
+
signal.addEventListener("abort", forget, { once: true });
|
|
1525
|
+
return () => {
|
|
1526
|
+
signal.removeEventListener("abort", forget);
|
|
1527
|
+
};
|
|
1528
|
+
}, [
|
|
1529
|
+
signal,
|
|
1530
|
+
actions,
|
|
1531
|
+
tab.id
|
|
1532
|
+
]);
|
|
1533
|
+
(0, react.useEffect)(() => {
|
|
1534
|
+
if (summary === void 0) loadChangesSummary(sessionId, seq);
|
|
1535
|
+
}, [
|
|
1536
|
+
summary,
|
|
1537
|
+
sessionId,
|
|
1538
|
+
seq,
|
|
1539
|
+
loadChangesSummary
|
|
1540
|
+
]);
|
|
1541
|
+
(0, react.useEffect)(() => {
|
|
1542
|
+
if (host === null) reloadPresentedHost();
|
|
1543
|
+
}, [host, reloadPresentedHost]);
|
|
1544
|
+
const files = typeof summary === "object" ? summary.files : [];
|
|
1545
|
+
const index = state !== void 0 && files[state.index] !== void 0 ? state.index : 0;
|
|
1546
|
+
const file = files[index];
|
|
1547
|
+
const diffState = useChangesDiff((value) => file === void 0 ? void 0 : value[changesDiffUrl(sessionId, seq, index)]);
|
|
1548
|
+
(0, react.useEffect)(() => {
|
|
1549
|
+
if (file !== void 0 && diffState === void 0) loadChangesDiff(sessionId, seq, index);
|
|
1550
|
+
}, [
|
|
1551
|
+
file,
|
|
1552
|
+
diffState,
|
|
1553
|
+
sessionId,
|
|
1554
|
+
seq,
|
|
1555
|
+
index,
|
|
1556
|
+
loadChangesDiff
|
|
1557
|
+
]);
|
|
1558
|
+
const phase = usePresentedOpen((value) => file === void 0 ? void 0 : value[changedFileUrl(sessionId, seq, index)]);
|
|
1559
|
+
const [menuOpen, setMenuOpen] = (0, react.useState)(false);
|
|
1560
|
+
const split = state?.split === true;
|
|
1561
|
+
const wrap = state?.wrap === true;
|
|
1562
|
+
const native = host !== null && host !== "error" && host.available && phase !== "nativeUnavailable";
|
|
1563
|
+
const summaryState = summary === void 0 || summary === "loading" ? "loading" : summary === "missing" ? "missing" : "ready";
|
|
1564
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
1565
|
+
className: ReviewTab_module_css_default.root,
|
|
1566
|
+
"data-changes-review": true,
|
|
1567
|
+
"data-review-state": summaryState,
|
|
1568
|
+
children: [
|
|
1569
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1570
|
+
className: ReviewTab_module_css_default.header,
|
|
1571
|
+
children: [
|
|
1572
|
+
file === void 0 ? (0, react_jsx_runtime.jsx)("span", {
|
|
1573
|
+
className: ReviewTab_module_css_default.selectorLabel,
|
|
1574
|
+
children: t("review.title", { turn: String(coordinates.turn) })
|
|
1575
|
+
}) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Menu, {
|
|
1576
|
+
className: ReviewTab_module_css_default.selector,
|
|
1577
|
+
open: menuOpen,
|
|
1578
|
+
autoFocus: true,
|
|
1579
|
+
portal: true,
|
|
1580
|
+
align: "start",
|
|
1581
|
+
dense: true,
|
|
1582
|
+
onClose: () => {
|
|
1583
|
+
setMenuOpen(false);
|
|
1584
|
+
},
|
|
1585
|
+
anchor: (0, react_jsx_runtime.jsxs)("button", {
|
|
1586
|
+
type: "button",
|
|
1587
|
+
className: ReviewTab_module_css_default.selectorButton,
|
|
1588
|
+
"aria-haspopup": "menu",
|
|
1589
|
+
"aria-expanded": menuOpen,
|
|
1590
|
+
"aria-label": t("review.selectFile"),
|
|
1591
|
+
title: file.display,
|
|
1592
|
+
"data-review-file": file.path,
|
|
1593
|
+
onClick: () => {
|
|
1594
|
+
setMenuOpen((value) => !value);
|
|
1595
|
+
},
|
|
1596
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1597
|
+
className: ReviewTab_module_css_default.selectorText,
|
|
1598
|
+
children: file.display
|
|
1599
|
+
}), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, { size: 12 })]
|
|
1600
|
+
}),
|
|
1601
|
+
items: files.map((entry, at) => ({
|
|
1602
|
+
id: String(at),
|
|
1603
|
+
label: (0, react_jsx_runtime.jsxs)("span", {
|
|
1604
|
+
className: ReviewTab_module_css_default.item,
|
|
1605
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1606
|
+
className: ReviewTab_module_css_default.itemPath,
|
|
1607
|
+
children: entry.display
|
|
1608
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
1609
|
+
className: ReviewTab_module_css_default.itemCounts,
|
|
1610
|
+
children: (0, react_jsx_runtime.jsx)(Counts, {
|
|
1611
|
+
file: entry,
|
|
1612
|
+
t
|
|
1613
|
+
})
|
|
1614
|
+
})]
|
|
1615
|
+
})
|
|
1616
|
+
})),
|
|
1617
|
+
selectedId: String(index),
|
|
1618
|
+
onSelect: (id) => {
|
|
1619
|
+
actions.selected(tab.id, Number(id));
|
|
1620
|
+
setMenuOpen(false);
|
|
1621
|
+
}
|
|
1622
|
+
}),
|
|
1623
|
+
file !== void 0 && (0, react_jsx_runtime.jsx)("span", {
|
|
1624
|
+
className: ReviewTab_module_css_default.counts,
|
|
1625
|
+
children: (0, react_jsx_runtime.jsx)(Counts, {
|
|
1626
|
+
file,
|
|
1627
|
+
t
|
|
1628
|
+
})
|
|
1629
|
+
}),
|
|
1630
|
+
(0, react_jsx_runtime.jsxs)("span", {
|
|
1631
|
+
className: ReviewTab_module_css_default.tools,
|
|
1632
|
+
children: [
|
|
1633
|
+
(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
|
|
1634
|
+
label: t(split ? "review.unified" : "review.split"),
|
|
1635
|
+
side: "bottom",
|
|
1636
|
+
delayMs: 500,
|
|
1637
|
+
children: (0, react_jsx_runtime.jsx)("button", {
|
|
1638
|
+
type: "button",
|
|
1639
|
+
className: ReviewTab_module_css_default.tool,
|
|
1640
|
+
"aria-pressed": split,
|
|
1641
|
+
"aria-label": t("review.splitAria"),
|
|
1642
|
+
"data-review-tool": "split",
|
|
1643
|
+
onClick: () => {
|
|
1644
|
+
actions.toggledSplit(tab.id);
|
|
1645
|
+
},
|
|
1646
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPanelLeftOutline16, {})
|
|
1647
|
+
})
|
|
1648
|
+
}),
|
|
1649
|
+
(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
|
|
1650
|
+
label: t(wrap ? "review.nowrap" : "review.wrap"),
|
|
1651
|
+
side: "bottom",
|
|
1652
|
+
delayMs: 500,
|
|
1653
|
+
children: (0, react_jsx_runtime.jsx)("button", {
|
|
1654
|
+
type: "button",
|
|
1655
|
+
className: ReviewTab_module_css_default.tool,
|
|
1656
|
+
"aria-pressed": wrap,
|
|
1657
|
+
"aria-label": t("review.wrapAria"),
|
|
1658
|
+
"data-review-tool": "wrap",
|
|
1659
|
+
onClick: () => {
|
|
1660
|
+
actions.toggledWrap(tab.id);
|
|
1661
|
+
},
|
|
1662
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconWrapLinesOutline16, {})
|
|
1663
|
+
})
|
|
1664
|
+
}),
|
|
1665
|
+
file !== void 0 && (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
|
|
1666
|
+
label: t("review.openFile"),
|
|
1667
|
+
side: "bottom",
|
|
1668
|
+
delayMs: 500,
|
|
1669
|
+
children: (0, react_jsx_runtime.jsx)("button", {
|
|
1670
|
+
type: "button",
|
|
1671
|
+
className: ReviewTab_module_css_default.tool,
|
|
1672
|
+
"aria-label": t("review.openFileAria", { name: file.display }),
|
|
1673
|
+
"data-review-tool": "open-file",
|
|
1674
|
+
onClick: () => {
|
|
1675
|
+
tab.actions.openResource(fileAddressFor(sessionId, cwd, file.path));
|
|
1676
|
+
},
|
|
1677
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCodeOutline16, {})
|
|
1678
|
+
})
|
|
1679
|
+
}),
|
|
1680
|
+
file !== void 0 && native && (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
|
|
1681
|
+
label: t(phase === "error" ? "diff.openNativeError" : "diff.openNative"),
|
|
1682
|
+
side: "bottom",
|
|
1683
|
+
delayMs: 500,
|
|
1684
|
+
children: (0, react_jsx_runtime.jsx)("button", {
|
|
1685
|
+
type: "button",
|
|
1686
|
+
className: ReviewTab_module_css_default.tool,
|
|
1687
|
+
disabled: phase === "opening",
|
|
1688
|
+
"data-review-tool": "open-native",
|
|
1689
|
+
"aria-label": t("diff.openNativeAria", { name: file.display }),
|
|
1690
|
+
"data-error": phase === "error" || void 0,
|
|
1691
|
+
onClick: () => {
|
|
1692
|
+
openChanged(sessionId, seq, index);
|
|
1693
|
+
},
|
|
1694
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconRightUpOutline16, {})
|
|
1695
|
+
})
|
|
1696
|
+
})
|
|
1697
|
+
]
|
|
1698
|
+
})
|
|
1699
|
+
]
|
|
1700
|
+
}),
|
|
1701
|
+
summaryState === "loading" && (0, react_jsx_runtime.jsx)("p", {
|
|
1702
|
+
className: ReviewTab_module_css_default.status,
|
|
1703
|
+
role: "status",
|
|
1704
|
+
children: t("diff.loading")
|
|
1705
|
+
}),
|
|
1706
|
+
summaryState === "missing" && (0, react_jsx_runtime.jsx)("p", {
|
|
1707
|
+
className: ReviewTab_module_css_default.status,
|
|
1708
|
+
children: t("diff.missing")
|
|
1709
|
+
}),
|
|
1710
|
+
file !== void 0 && (0, react_jsx_runtime.jsx)(FileBody, {
|
|
1711
|
+
state: diffState,
|
|
1712
|
+
split,
|
|
1713
|
+
wrap,
|
|
1714
|
+
t,
|
|
1715
|
+
retry: () => {
|
|
1716
|
+
loadChangesDiff(sessionId, seq, index);
|
|
1717
|
+
}
|
|
1718
|
+
})
|
|
1719
|
+
]
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
/** The selected file's comparison, or the state that stands in for it. */
|
|
1723
|
+
function FileBody({ state, split, wrap, retry, t }) {
|
|
1724
|
+
if (state === void 0 || state === "loading") return (0, react_jsx_runtime.jsx)("p", {
|
|
1725
|
+
className: ReviewTab_module_css_default.status,
|
|
1726
|
+
role: "status",
|
|
1727
|
+
children: t("diff.loading")
|
|
1728
|
+
});
|
|
1729
|
+
if (state === "missing") return (0, react_jsx_runtime.jsx)("p", {
|
|
1730
|
+
className: ReviewTab_module_css_default.status,
|
|
1731
|
+
children: t("diff.missing")
|
|
1732
|
+
});
|
|
1733
|
+
if (state === "error") return (0, react_jsx_runtime.jsxs)("div", {
|
|
1734
|
+
className: ReviewTab_module_css_default.status,
|
|
1735
|
+
children: [(0, react_jsx_runtime.jsx)("span", { children: t("diff.error") }), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
|
|
1736
|
+
size: "sm",
|
|
1737
|
+
onClick: retry,
|
|
1738
|
+
children: t("presented.retry")
|
|
1739
|
+
})]
|
|
1740
|
+
});
|
|
1741
|
+
if (state.kind === "binary") return (0, react_jsx_runtime.jsx)("p", {
|
|
1742
|
+
className: ReviewTab_module_css_default.status,
|
|
1743
|
+
children: t("diff.binary")
|
|
1744
|
+
});
|
|
1745
|
+
if (state.kind === "oversized") return (0, react_jsx_runtime.jsx)("p", {
|
|
1746
|
+
className: ReviewTab_module_css_default.status,
|
|
1747
|
+
children: t("diff.oversized")
|
|
1748
|
+
});
|
|
1749
|
+
return (0, react_jsx_runtime.jsx)(TextDiff, {
|
|
1750
|
+
diff: state,
|
|
1751
|
+
split,
|
|
1752
|
+
wrap,
|
|
1753
|
+
t
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
/** The kind a paired row carries: a deletion or addition on either side, otherwise context. */
|
|
1757
|
+
function splitRowKind(row) {
|
|
1758
|
+
return row.left?.kind === "del" ? "del" : row.right?.kind === "add" ? "add" : "context";
|
|
1759
|
+
}
|
|
1760
|
+
function hunkHeader(hunk) {
|
|
1761
|
+
return `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* The side-by-side view without wrapping: two columns that clip their long
|
|
1765
|
+
* lines and scroll sideways together, so a long line on one side never runs
|
|
1766
|
+
* under the other and both sides show the same columns of text. Every line is
|
|
1767
|
+
* one fixed-height row, which keeps the sides aligned.
|
|
1768
|
+
*/
|
|
1769
|
+
function SplitColumns({ hunks }) {
|
|
1770
|
+
const paired = (0, react.useMemo)(() => hunks.map((hunk) => ({
|
|
1771
|
+
header: hunkHeader(hunk),
|
|
1772
|
+
rows: splitRows(hunk)
|
|
1773
|
+
})), [hunks]);
|
|
1774
|
+
const columns = (0, react.useRef)({
|
|
1775
|
+
left: null,
|
|
1776
|
+
right: null
|
|
1777
|
+
});
|
|
1778
|
+
const follow = (side) => (event) => {
|
|
1779
|
+
const other = columns.current[side === "left" ? "right" : "left"];
|
|
1780
|
+
if (other !== null && other.scrollLeft !== event.currentTarget.scrollLeft) other.scrollLeft = event.currentTarget.scrollLeft;
|
|
1781
|
+
};
|
|
1782
|
+
return (0, react_jsx_runtime.jsx)("div", {
|
|
1783
|
+
className: ReviewTab_module_css_default.columns,
|
|
1784
|
+
children: ["left", "right"].map((side) => (0, react_jsx_runtime.jsx)("div", {
|
|
1785
|
+
className: ReviewTab_module_css_default.column,
|
|
1786
|
+
"data-diff-side": side,
|
|
1787
|
+
ref: (element) => {
|
|
1788
|
+
columns.current[side] = element;
|
|
1789
|
+
},
|
|
1790
|
+
onScroll: follow(side),
|
|
1791
|
+
children: paired.map((hunk, position) => (0, react_jsx_runtime.jsxs)("section", {
|
|
1792
|
+
className: ReviewTab_module_css_default.hunk,
|
|
1793
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
1794
|
+
className: ReviewTab_module_css_default.hunkHeader,
|
|
1795
|
+
children: hunk.header
|
|
1796
|
+
}), hunk.rows.map((row, at) => {
|
|
1797
|
+
const cell = row[side];
|
|
1798
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
1799
|
+
className: `${ReviewTab_module_css_default.sideLine} ${cell === void 0 ? ReviewTab_module_css_default.empty : ReviewTab_module_css_default[cell.kind]}`,
|
|
1800
|
+
"data-diff-line": splitRowKind(row),
|
|
1801
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1802
|
+
className: ReviewTab_module_css_default.number,
|
|
1803
|
+
children: cell?.no ?? ""
|
|
1804
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
1805
|
+
className: ReviewTab_module_css_default.text,
|
|
1806
|
+
children: cell?.text ?? ""
|
|
1807
|
+
})]
|
|
1808
|
+
}, at);
|
|
1809
|
+
})]
|
|
1810
|
+
}, position))
|
|
1811
|
+
}, side))
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
/** The hunks of a text comparison with their line numbers, unified or side by side. */
|
|
1815
|
+
function TextDiff({ diff, split, wrap, t }) {
|
|
1816
|
+
const note = noteOf(diff);
|
|
1817
|
+
const { hunks, truncated } = (0, react.useMemo)(() => renderedHunks(diff.hunks), [diff.hunks]);
|
|
1818
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
1819
|
+
className: ReviewTab_module_css_default.body,
|
|
1820
|
+
"data-review-view": split ? "split" : "unified",
|
|
1821
|
+
"data-review-wrap": wrap || void 0,
|
|
1822
|
+
children: [
|
|
1823
|
+
note !== void 0 && (0, react_jsx_runtime.jsx)("p", {
|
|
1824
|
+
className: ReviewTab_module_css_default.note,
|
|
1825
|
+
children: t(note)
|
|
1826
|
+
}),
|
|
1827
|
+
diff.coarse && (0, react_jsx_runtime.jsx)("p", {
|
|
1828
|
+
className: ReviewTab_module_css_default.note,
|
|
1829
|
+
"data-diff-coarse": true,
|
|
1830
|
+
children: t("diff.coarse")
|
|
1831
|
+
}),
|
|
1832
|
+
truncated && (0, react_jsx_runtime.jsx)("p", {
|
|
1833
|
+
className: ReviewTab_module_css_default.note,
|
|
1834
|
+
"data-diff-truncated": true,
|
|
1835
|
+
children: t("diff.truncated", { count: String(5e3) })
|
|
1836
|
+
}),
|
|
1837
|
+
split && !wrap ? (0, react_jsx_runtime.jsx)(SplitColumns, { hunks }) : hunks.map((hunk, position) => (0, react_jsx_runtime.jsxs)("section", {
|
|
1838
|
+
className: ReviewTab_module_css_default.hunk,
|
|
1839
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
1840
|
+
className: ReviewTab_module_css_default.hunkHeader,
|
|
1841
|
+
children: hunkHeader(hunk)
|
|
1842
|
+
}), split ? splitRows(hunk).map((row, at) => (0, react_jsx_runtime.jsxs)("div", {
|
|
1843
|
+
className: ReviewTab_module_css_default.splitLine,
|
|
1844
|
+
"data-diff-line": splitRowKind(row),
|
|
1845
|
+
children: [(0, react_jsx_runtime.jsxs)("span", {
|
|
1846
|
+
className: `${ReviewTab_module_css_default.cell} ${row.left === void 0 ? ReviewTab_module_css_default.empty : ReviewTab_module_css_default[row.left.kind]}`,
|
|
1847
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1848
|
+
className: ReviewTab_module_css_default.number,
|
|
1849
|
+
children: row.left?.no ?? ""
|
|
1850
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
1851
|
+
className: ReviewTab_module_css_default.text,
|
|
1852
|
+
children: row.left?.text ?? ""
|
|
1853
|
+
})]
|
|
1854
|
+
}), (0, react_jsx_runtime.jsxs)("span", {
|
|
1855
|
+
className: `${ReviewTab_module_css_default.cell} ${row.right === void 0 ? ReviewTab_module_css_default.empty : ReviewTab_module_css_default[row.right.kind]}`,
|
|
1856
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1857
|
+
className: ReviewTab_module_css_default.number,
|
|
1858
|
+
children: row.right?.no ?? ""
|
|
1859
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
1860
|
+
className: ReviewTab_module_css_default.text,
|
|
1861
|
+
children: row.right?.text ?? ""
|
|
1862
|
+
})]
|
|
1863
|
+
})]
|
|
1864
|
+
}, at)) : hunkRows(hunk).map((row, at) => (0, react_jsx_runtime.jsxs)("div", {
|
|
1865
|
+
className: `${ReviewTab_module_css_default.line} ${ReviewTab_module_css_default[row.kind]}`,
|
|
1866
|
+
"data-diff-line": row.kind,
|
|
1867
|
+
children: [
|
|
1868
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1869
|
+
className: ReviewTab_module_css_default.number,
|
|
1870
|
+
children: row.old ?? ""
|
|
1871
|
+
}),
|
|
1872
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1873
|
+
className: ReviewTab_module_css_default.number,
|
|
1874
|
+
children: row.new ?? ""
|
|
1875
|
+
}),
|
|
1876
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1877
|
+
className: ReviewTab_module_css_default.sign,
|
|
1878
|
+
children: row.kind === "add" ? "+" : row.kind === "del" ? "-" : " "
|
|
1879
|
+
}),
|
|
1880
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1881
|
+
className: ReviewTab_module_css_default.text,
|
|
1882
|
+
children: row.text
|
|
1883
|
+
})
|
|
1884
|
+
]
|
|
1885
|
+
}, at))]
|
|
1886
|
+
}, position))
|
|
1887
|
+
]
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
//#endregion
|
|
1891
|
+
//#region lib/types/client/review-definition.js
|
|
1892
|
+
/** The tab kind this package owns. */
|
|
1893
|
+
const CHANGES_REVIEW_KIND = "changes-review";
|
|
1894
|
+
/** This implementation's identity in the tab system, and the key its body registers under. */
|
|
1895
|
+
const CHANGES_REVIEW_ID = "@qilin-agent/client-ui-deliverables";
|
|
1896
|
+
/**
|
|
1897
|
+
* The review type's registry definition.
|
|
1898
|
+
* @param t - namespace-bound translate, read fresh on every title call.
|
|
1899
|
+
* @returns the definition to register.
|
|
1900
|
+
*/
|
|
1901
|
+
function changesReviewDefinition(t) {
|
|
1902
|
+
return {
|
|
1903
|
+
id: CHANGES_REVIEW_ID,
|
|
1904
|
+
kind: CHANGES_REVIEW_KIND,
|
|
1905
|
+
patterns: ["qilin-resource://changes-review/**"],
|
|
1906
|
+
priority: "builtin",
|
|
1907
|
+
canOpen: (address) => parseChangesReviewAddress(address) !== void 0,
|
|
1908
|
+
title: (address) => {
|
|
1909
|
+
const turn = parseChangesReviewAddress(address)?.turn;
|
|
1910
|
+
return turn === void 0 ? address : t("review.title", { turn: String(turn) });
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
//#endregion
|
|
1915
|
+
//#region lib/types/client/review-store.js
|
|
1916
|
+
/**
|
|
1917
|
+
* The review tab's view state: which listed file is shown, whether hunks are
|
|
1918
|
+
* drawn side by side, and whether long lines wrap. One bucket per tab, so two
|
|
1919
|
+
* reviews in one session keep their own choices; the bucket ends with the
|
|
1920
|
+
* tab record's signal.
|
|
1921
|
+
*/
|
|
1922
|
+
function bucket(state, tabId) {
|
|
1923
|
+
const tab = state.byTab[tabId];
|
|
1924
|
+
if (tab === void 0) throw new Error(`ui-deliverables: no review state for tab "${tabId}"`);
|
|
1925
|
+
return tab;
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Declare the review tab's store; the registration declares it as an
|
|
1929
|
+
* exclusive store, so the framework mints one instance per session.
|
|
1930
|
+
* @returns the store handle to declare on the registration.
|
|
1931
|
+
*/
|
|
1932
|
+
function createReviewStore() {
|
|
1933
|
+
return (0, _qilin_agent_client_store.defineStore)({
|
|
1934
|
+
init: () => ({ byTab: {} }),
|
|
1935
|
+
actions: {
|
|
1936
|
+
/**
|
|
1937
|
+
* Apply a navigation: seed the tab on its first one, then show the navigated file.
|
|
1938
|
+
* @param d - draft state.
|
|
1939
|
+
* @param tabId - the tab being drawn.
|
|
1940
|
+
* @param revision - the navigation revision being applied.
|
|
1941
|
+
* @param index - the file index the navigation named, or the current one.
|
|
1942
|
+
*/
|
|
1943
|
+
navigated: (d, tabId, revision, index) => {
|
|
1944
|
+
const tab = d.byTab[tabId];
|
|
1945
|
+
if (tab === void 0) d.byTab[tabId] = {
|
|
1946
|
+
index,
|
|
1947
|
+
split: false,
|
|
1948
|
+
wrap: false,
|
|
1949
|
+
navigated: revision
|
|
1950
|
+
};
|
|
1951
|
+
else {
|
|
1952
|
+
tab.index = index;
|
|
1953
|
+
tab.navigated = revision;
|
|
1954
|
+
}
|
|
1955
|
+
},
|
|
1956
|
+
/**
|
|
1957
|
+
* Show another listed file.
|
|
1958
|
+
* @param d - draft state.
|
|
1959
|
+
* @param tabId - the tab being drawn.
|
|
1960
|
+
* @param index - original index in the summary's files array.
|
|
1961
|
+
*/
|
|
1962
|
+
selected: (d, tabId, index) => {
|
|
1963
|
+
bucket(d, tabId).index = index;
|
|
1964
|
+
},
|
|
1965
|
+
/**
|
|
1966
|
+
* Switch between the unified and the side-by-side view.
|
|
1967
|
+
* @param d - draft state.
|
|
1968
|
+
* @param tabId - the tab being drawn.
|
|
1969
|
+
*/
|
|
1970
|
+
toggledSplit: (d, tabId) => {
|
|
1971
|
+
const tab = bucket(d, tabId);
|
|
1972
|
+
tab.split = !tab.split;
|
|
1973
|
+
},
|
|
1974
|
+
/**
|
|
1975
|
+
* Switch line wrapping.
|
|
1976
|
+
* @param d - draft state.
|
|
1977
|
+
* @param tabId - the tab being drawn.
|
|
1978
|
+
*/
|
|
1979
|
+
toggledWrap: (d, tabId) => {
|
|
1980
|
+
const tab = bucket(d, tabId);
|
|
1981
|
+
tab.wrap = !tab.wrap;
|
|
1982
|
+
},
|
|
1983
|
+
/**
|
|
1984
|
+
* Drop a tab's bucket once its record is gone.
|
|
1985
|
+
* @param d - draft state.
|
|
1986
|
+
* @param tabId - the tab that ended.
|
|
1987
|
+
*/
|
|
1988
|
+
forget: (d, tabId) => {
|
|
1989
|
+
d.byTab = Object.fromEntries(Object.entries(d.byTab).filter(([id]) => id !== tabId));
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
//#endregion
|
|
1995
|
+
//#region lib/types/client/locales.js
|
|
1996
|
+
/** `deliverables` namespace dictionaries: cards, comparison tab, and file-mention copy. */
|
|
1997
|
+
/** Dictionary namespace owned by this plugin. */
|
|
1998
|
+
const NS = "deliverables";
|
|
1999
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
2000
|
+
const zh = {
|
|
2001
|
+
"presented.nativeUnavailable": "此文件没有可用的主机路径,请在侧边栏预览",
|
|
2002
|
+
"presented.revealError": "无法在文件管理器中显示,请重试",
|
|
2003
|
+
"presented.directoryError": "无法打开所在文件夹,请重试",
|
|
2004
|
+
"presented.directoryOpening": "正在打开所在文件夹…",
|
|
2005
|
+
"presented.directoryOpened": "已请求打开所在文件夹",
|
|
2006
|
+
"presented.revealed": "已请求在文件管理器中显示",
|
|
2007
|
+
"presented.revealing": "正在文件管理器中显示…",
|
|
2008
|
+
"presented.unavailable": "此主机没有可用的桌面,无法使用外部程序打开文件或文件夹;文件仍可在侧边栏预览",
|
|
2009
|
+
"presented.retry": "重试",
|
|
2010
|
+
"presented.hostError": "无法读取主机桌面信息",
|
|
2011
|
+
"presented.directory": "打开所在文件夹",
|
|
2012
|
+
"presented.explorer": "在文件资源管理器中显示",
|
|
2013
|
+
"presented.finder": "在 Finder 中显示",
|
|
2014
|
+
"presented.defaultApp": "用默认应用打开",
|
|
2015
|
+
"presented.more": "{name} 的更多文件操作",
|
|
2016
|
+
"presented.action": "打开",
|
|
2017
|
+
"presented.preview": "在侧边栏预览",
|
|
2018
|
+
"presented.previewButton": "在侧边栏打开 {name}",
|
|
2019
|
+
"presented.previewCard": "在侧边栏预览 {name}",
|
|
2020
|
+
"presented.all": "全部 {count} 个文件",
|
|
2021
|
+
"presented.expandAria": "展开全部 {count} 个交付文件",
|
|
2022
|
+
"presented.collapse": "收起",
|
|
2023
|
+
"presented.collapseAria": "收起交付文件列表",
|
|
2024
|
+
"presented.opening": "正在打开…",
|
|
2025
|
+
"presented.opened": "已在默认程序中打开",
|
|
2026
|
+
"presented.error": "打开失败,点击重试",
|
|
2027
|
+
"presented.file": "文件",
|
|
2028
|
+
"row.title": "交付文件",
|
|
2029
|
+
"row.running": "正在交付",
|
|
2030
|
+
"row.preparing": "准备交付",
|
|
2031
|
+
"row.ok": "已交付",
|
|
2032
|
+
"row.error": "交付失败",
|
|
2033
|
+
"row.stopped": "已中断",
|
|
2034
|
+
"row.inspect": "查看调用",
|
|
2035
|
+
"changes.title": "已编辑 {count} 个文件",
|
|
2036
|
+
"changes.added": "+{count}",
|
|
2037
|
+
"changes.deleted": "-{count}",
|
|
2038
|
+
"changes.binary": "二进制",
|
|
2039
|
+
"changes.openReview": "在侧边栏查看本轮改动",
|
|
2040
|
+
"changes.all": "全部 {count} 个文件",
|
|
2041
|
+
"changes.expandAria": "展开全部 {count} 个改动文件",
|
|
2042
|
+
"changes.collapse": "收起",
|
|
2043
|
+
"changes.collapseAria": "收起改动文件列表",
|
|
2044
|
+
"changes.oversized": "过大",
|
|
2045
|
+
"changes.viewDiff": "查看 {name} 的改动",
|
|
2046
|
+
"review.title": "第 {turn} 轮改动",
|
|
2047
|
+
"review.selectFile": "选择要查看的文件",
|
|
2048
|
+
"review.split": "切换为左右对比",
|
|
2049
|
+
"review.unified": "切换为单栏对比",
|
|
2050
|
+
"review.splitAria": "左右对比",
|
|
2051
|
+
"review.wrap": "开启自动换行",
|
|
2052
|
+
"review.nowrap": "关闭自动换行",
|
|
2053
|
+
"review.wrapAria": "自动换行",
|
|
2054
|
+
"review.openFile": "在侧边栏打开整个文件",
|
|
2055
|
+
"review.openFileAria": "在侧边栏打开 {name}",
|
|
2056
|
+
"diff.openNativeAria": "用默认应用打开 {name}",
|
|
2057
|
+
"diff.loading": "正在读取改动…",
|
|
2058
|
+
"diff.missing": "这轮改动的内容已不可用",
|
|
2059
|
+
"diff.error": "无法读取改动",
|
|
2060
|
+
"diff.binary": "二进制文件,无法显示改动",
|
|
2061
|
+
"diff.oversized": "文件过大,无法显示改动",
|
|
2062
|
+
"diff.created": "本轮新建的文件",
|
|
2063
|
+
"diff.deleted": "本轮删除的文件",
|
|
2064
|
+
"diff.unchanged": "两侧内容相同",
|
|
2065
|
+
"diff.coarse": "逐行对比超时,按整个文件替换显示",
|
|
2066
|
+
"diff.truncated": "只显示前 {count} 行",
|
|
2067
|
+
"diff.openNative": "用默认应用打开",
|
|
2068
|
+
"diff.openNativeError": "打开失败,点击重试"
|
|
2069
|
+
};
|
|
2070
|
+
/** English dictionary (same key set). */
|
|
2071
|
+
const en = {
|
|
2072
|
+
"presented.nativeUnavailable": "This file has no available Host path. Preview it in the sidebar.",
|
|
2073
|
+
"presented.revealError": "Could not show in file manager. Try again.",
|
|
2074
|
+
"presented.directoryError": "Could not open containing folder. Try again.",
|
|
2075
|
+
"presented.directoryOpening": "Opening containing folder…",
|
|
2076
|
+
"presented.directoryOpened": "Requested opening containing folder",
|
|
2077
|
+
"presented.revealed": "Requested display in file manager",
|
|
2078
|
+
"presented.revealing": "Showing in file manager…",
|
|
2079
|
+
"presented.unavailable": "This Host has no desktop available to open files or folders in external apps. Files can still be previewed in the sidebar.",
|
|
2080
|
+
"presented.retry": "Retry",
|
|
2081
|
+
"presented.hostError": "Could not read the Host desktop information",
|
|
2082
|
+
"presented.directory": "Open containing folder",
|
|
2083
|
+
"presented.explorer": "Show in File Explorer",
|
|
2084
|
+
"presented.finder": "Show in Finder",
|
|
2085
|
+
"presented.defaultApp": "Open in default app",
|
|
2086
|
+
"presented.more": "More file actions for {name}",
|
|
2087
|
+
"presented.action": "Open",
|
|
2088
|
+
"presented.preview": "Preview in sidebar",
|
|
2089
|
+
"presented.previewButton": "Open {name} in sidebar",
|
|
2090
|
+
"presented.previewCard": "Preview {name} in sidebar",
|
|
2091
|
+
"presented.all": "All {count} files",
|
|
2092
|
+
"presented.expandAria": "Show all {count} delivered files",
|
|
2093
|
+
"presented.collapse": "Collapse",
|
|
2094
|
+
"presented.collapseAria": "Collapse delivered files",
|
|
2095
|
+
"presented.opening": "Opening…",
|
|
2096
|
+
"presented.opened": "Opened in default app",
|
|
2097
|
+
"presented.error": "Could not open. Click to retry.",
|
|
2098
|
+
"presented.file": "File",
|
|
2099
|
+
"row.title": "Present files",
|
|
2100
|
+
"row.running": "Delivering",
|
|
2101
|
+
"row.preparing": "Preparing deliverables",
|
|
2102
|
+
"row.ok": "Delivered",
|
|
2103
|
+
"row.error": "Delivery failed",
|
|
2104
|
+
"row.stopped": "Interrupted",
|
|
2105
|
+
"row.inspect": "Inspect call",
|
|
2106
|
+
"changes.title": "Edited {count} files",
|
|
2107
|
+
"changes.added": "+{count}",
|
|
2108
|
+
"changes.deleted": "-{count}",
|
|
2109
|
+
"changes.binary": "binary",
|
|
2110
|
+
"changes.openReview": "Review this turn’s changes in the sidebar",
|
|
2111
|
+
"changes.all": "All {count} files",
|
|
2112
|
+
"changes.expandAria": "Show all {count} changed files",
|
|
2113
|
+
"changes.collapse": "Collapse",
|
|
2114
|
+
"changes.collapseAria": "Collapse changed files",
|
|
2115
|
+
"changes.oversized": "too large",
|
|
2116
|
+
"changes.viewDiff": "View changes to {name}",
|
|
2117
|
+
"review.title": "Review · turn {turn}",
|
|
2118
|
+
"review.selectFile": "Choose the file to review",
|
|
2119
|
+
"review.split": "Switch to split view",
|
|
2120
|
+
"review.unified": "Switch to unified view",
|
|
2121
|
+
"review.splitAria": "Split view",
|
|
2122
|
+
"review.wrap": "Enable line wrap",
|
|
2123
|
+
"review.nowrap": "Disable line wrap",
|
|
2124
|
+
"review.wrapAria": "Line wrap",
|
|
2125
|
+
"review.openFile": "Open the whole file in the sidebar",
|
|
2126
|
+
"review.openFileAria": "Open {name} in sidebar",
|
|
2127
|
+
"diff.openNativeAria": "Open {name} in default app",
|
|
2128
|
+
"diff.loading": "Reading changes…",
|
|
2129
|
+
"diff.missing": "The contents of this turn’s changes are no longer available",
|
|
2130
|
+
"diff.error": "Could not read the changes",
|
|
2131
|
+
"diff.binary": "Binary file; changes cannot be shown",
|
|
2132
|
+
"diff.oversized": "File too large; changes cannot be shown",
|
|
2133
|
+
"diff.created": "Created in this turn",
|
|
2134
|
+
"diff.deleted": "Deleted in this turn",
|
|
2135
|
+
"diff.unchanged": "Both sides hold the same lines",
|
|
2136
|
+
"diff.coarse": "Line comparison timed out; shown as a whole-file replacement",
|
|
2137
|
+
"diff.truncated": "Showing the first {count} lines",
|
|
2138
|
+
"diff.openNative": "Open in default app",
|
|
2139
|
+
"diff.openNativeError": "Could not open. Click to retry."
|
|
2140
|
+
};
|
|
2141
|
+
//#endregion
|
|
2142
|
+
//#region lib/types/client/index.js
|
|
2143
|
+
/** Required services for the tail-slot and tab-type registrations and their dictionaries. */
|
|
2144
|
+
const inject = [
|
|
2145
|
+
"slots",
|
|
2146
|
+
"locale",
|
|
2147
|
+
"uiConversation",
|
|
2148
|
+
"remote",
|
|
2149
|
+
"remote.session",
|
|
2150
|
+
"sidebarRightTabs",
|
|
2151
|
+
"sidebarRight"
|
|
2152
|
+
];
|
|
2153
|
+
/**
|
|
2154
|
+
* Client plugin body: register the dictionaries, the turn-tail entry, and the comparison tab type.
|
|
2155
|
+
* @param ctx - client root context.
|
|
2156
|
+
*/
|
|
2157
|
+
function apply(ctx) {
|
|
2158
|
+
const opener = new PresentedOpenController();
|
|
2159
|
+
const summaries = new ChangesSummaryStore();
|
|
2160
|
+
const diffs = new ChangesDiffStore();
|
|
2161
|
+
ctx.effect(() => () => Promise.all([
|
|
2162
|
+
opener.dispose(),
|
|
2163
|
+
summaries.dispose(),
|
|
2164
|
+
diffs.dispose()
|
|
2165
|
+
]));
|
|
2166
|
+
ctx.on("connection/reset", () => {
|
|
2167
|
+
opener.resetHost();
|
|
2168
|
+
summaries.reset();
|
|
2169
|
+
diffs.reset();
|
|
2170
|
+
});
|
|
2171
|
+
ctx.uiConversation.events.register(deliverablesDefinition);
|
|
2172
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
2173
|
+
zh,
|
|
2174
|
+
en
|
|
2175
|
+
}), "ui-deliverables: dictionaries");
|
|
2176
|
+
ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
|
|
2177
|
+
name: "conversation.chat.turnTail",
|
|
2178
|
+
id: "@qilin-agent/client-ui-deliverables",
|
|
2179
|
+
locale: NS,
|
|
2180
|
+
inject: () => ({
|
|
2181
|
+
hooks: {
|
|
2182
|
+
presentedOpen: opener.state,
|
|
2183
|
+
presentedHost: opener.host,
|
|
2184
|
+
changesSummary: summaries.state
|
|
2185
|
+
},
|
|
2186
|
+
reloadPresentedHost: () => opener.loadHost(),
|
|
2187
|
+
loadChangesSummary: (sessionId, seq) => summaries.load(sessionId, seq),
|
|
2188
|
+
openPresented: (sessionId, seq, index, action) => opener.open(sessionId, seq, index, action),
|
|
2189
|
+
openChanged: (sessionId, seq, index) => opener.openChanged(sessionId, seq, index),
|
|
2190
|
+
openChangesReview: (coordinates, index) => {
|
|
2191
|
+
ctx.sidebarRight.openResource(changesReviewAddress(coordinates), { params: { index } });
|
|
2192
|
+
}
|
|
2193
|
+
})
|
|
2194
|
+
}, DeliverablesTail));
|
|
2195
|
+
ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
2196
|
+
name: "tool.call.toolview",
|
|
2197
|
+
key: "present",
|
|
2198
|
+
locale: NS
|
|
2199
|
+
}, PresentRow));
|
|
2200
|
+
const t = ctx.locale.bind(NS);
|
|
2201
|
+
ctx.effect(() => ctx.sidebarRightTabs.register(changesReviewDefinition(t)), "ui-deliverables: changes-review type");
|
|
2202
|
+
ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
|
|
2203
|
+
name: "sidebar.right.pane.tab",
|
|
2204
|
+
key: CHANGES_REVIEW_ID,
|
|
2205
|
+
locale: NS,
|
|
2206
|
+
store: createReviewStore(),
|
|
2207
|
+
inject: () => ({
|
|
2208
|
+
hooks: {
|
|
2209
|
+
changesSummary: summaries.state,
|
|
2210
|
+
changesDiff: diffs.state,
|
|
2211
|
+
presentedOpen: opener.state,
|
|
2212
|
+
presentedHost: opener.host
|
|
2213
|
+
},
|
|
2214
|
+
loadChangesSummary: (sessionId, seq) => summaries.load(sessionId, seq),
|
|
2215
|
+
loadChangesDiff: (sessionId, seq, index) => diffs.load(sessionId, seq, index),
|
|
2216
|
+
reloadPresentedHost: () => opener.loadHost(),
|
|
2217
|
+
openChanged: (sessionId, seq, index) => opener.openChanged(sessionId, seq, index)
|
|
2218
|
+
})
|
|
2219
|
+
}, ReviewTab)), "ui-deliverables: changes-review body");
|
|
2220
|
+
ctx.provide("chatFileMentions", { forClosing(owner) {
|
|
2221
|
+
const paths = selectProducedFiles(owner);
|
|
2222
|
+
const presented = presentedForClosing(owner);
|
|
2223
|
+
if (paths === null && presented.length === 0) return void 0;
|
|
2224
|
+
return producedFileMentions([...new Set([...paths ?? [], ...presented.map((file) => file.path)])], owner.openFile, (path) => t("presented.previewButton", { name: path }), owner.turn.data.get("deliverables")?.produced.filter((operation) => operation.seq <= owner.seq));
|
|
2225
|
+
} });
|
|
2226
|
+
}
|
|
2227
|
+
//#endregion
|
|
2228
|
+
exports.apply = apply;
|
|
2229
|
+
exports.inject = inject;
|
|
2230
|
+
return module.exports;
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
//# sourceMappingURL=client.js.map
|