taskchef 7.14.1 → 7.14.2
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/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/dashboard/app.js +16 -11
- package/src/dashboard/github-links.js +160 -24
package/package.json
CHANGED
package/src/dashboard/app.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
turnPresentation,
|
|
17
17
|
} from "./state.js";
|
|
18
18
|
import { openTaskFromControl } from "./actions.js";
|
|
19
|
-
import {
|
|
19
|
+
import { referenceSegments } from "./github-links.js";
|
|
20
20
|
import { formatRelativeTime, RelativeTimeController, parsedTimestamp } from "./time.js";
|
|
21
21
|
|
|
22
22
|
const state = {
|
|
@@ -61,28 +61,33 @@ const elements = {
|
|
|
61
61
|
};
|
|
62
62
|
let notificationDescriptionSerial = 0;
|
|
63
63
|
|
|
64
|
-
function
|
|
64
|
+
function referenceLink(link, { compact = false } = {}) {
|
|
65
65
|
const anchor = document.createElement("a");
|
|
66
66
|
anchor.className = compact ? "github-link github-link-compact" : "github-link";
|
|
67
67
|
anchor.href = link.url;
|
|
68
68
|
anchor.target = "_blank";
|
|
69
69
|
anchor.rel = "noopener noreferrer";
|
|
70
70
|
anchor.textContent = link.label ?? link.text;
|
|
71
|
+
const githubKind = link.type === "issue"
|
|
72
|
+
? ", GitHub issue"
|
|
73
|
+
: link.type === "pull"
|
|
74
|
+
? ", GitHub pull request"
|
|
75
|
+
: link.provider === "github" || link.owner ? " on GitHub" : "";
|
|
71
76
|
anchor.setAttribute(
|
|
72
77
|
"aria-label",
|
|
73
|
-
`${link.label ?? link.text}
|
|
78
|
+
`${link.label ?? link.text}${githubKind} (opens in a new tab)`,
|
|
74
79
|
);
|
|
75
80
|
anchor.addEventListener("click", (event) => event.stopPropagation());
|
|
76
81
|
return anchor;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
function
|
|
80
|
-
const children =
|
|
84
|
+
function appendLinkedText(container, text, task) {
|
|
85
|
+
const children = referenceSegments(text, {
|
|
81
86
|
projectRepositories: task.project?.githubRepos,
|
|
82
87
|
taskRepository: task.relatedGitHubRepository,
|
|
83
88
|
}).map((segment) => {
|
|
84
89
|
if (segment.kind === "text") return document.createTextNode(segment.text);
|
|
85
|
-
if (segment.kind === "link") return
|
|
90
|
+
if (segment.kind === "link") return referenceLink(segment);
|
|
86
91
|
const ambiguous = document.createElement("span");
|
|
87
92
|
ambiguous.className = "github-reference-ambiguous";
|
|
88
93
|
ambiguous.textContent = segment.text;
|
|
@@ -99,7 +104,7 @@ function relatedGitHubLinks(task, { compact = false } = {}) {
|
|
|
99
104
|
const container = document.createElement("nav");
|
|
100
105
|
container.className = `github-links${compact ? " github-links-compact" : ""}`;
|
|
101
106
|
container.setAttribute("aria-label", `Related GitHub links for ${task.title}`);
|
|
102
|
-
const children = links.map((link) =>
|
|
107
|
+
const children = links.map((link) => referenceLink(link, { compact }));
|
|
103
108
|
if (task.relatedGitHubLinksTruncated) {
|
|
104
109
|
const more = document.createElement("span");
|
|
105
110
|
more.className = "github-links-more";
|
|
@@ -307,7 +312,7 @@ function turnTimeline(task) {
|
|
|
307
312
|
requestLabel.textContent = "Request";
|
|
308
313
|
const request = document.createElement("p");
|
|
309
314
|
request.className = "preserve-lines";
|
|
310
|
-
|
|
315
|
+
appendLinkedText(
|
|
311
316
|
request,
|
|
312
317
|
turn.requestSummary ?? "Request not recorded by this TaskChef version.",
|
|
313
318
|
task,
|
|
@@ -316,7 +321,7 @@ function turnTimeline(task) {
|
|
|
316
321
|
resultLabel.textContent = "Result";
|
|
317
322
|
const result = document.createElement("p");
|
|
318
323
|
result.className = "preserve-lines";
|
|
319
|
-
|
|
324
|
+
appendLinkedText(result, presentation.summary, task);
|
|
320
325
|
const turnMetadata = document.createElement("p");
|
|
321
326
|
turnMetadata.className = "result-history-turn";
|
|
322
327
|
turnMetadata.textContent = turn.turnId
|
|
@@ -424,12 +429,12 @@ function taskCard(task) {
|
|
|
424
429
|
requestLabel.textContent = "Request";
|
|
425
430
|
const request = document.createElement("span");
|
|
426
431
|
request.className = "preserve-lines";
|
|
427
|
-
request
|
|
432
|
+
appendLinkedText(request, latest.requestSummary, task);
|
|
428
433
|
const resultLabel = document.createElement("strong");
|
|
429
434
|
resultLabel.textContent = "Result";
|
|
430
435
|
const result = document.createElement("span");
|
|
431
436
|
result.className = "preserve-lines";
|
|
432
|
-
result
|
|
437
|
+
appendLinkedText(result, latest.resultSummary, task);
|
|
433
438
|
summary.replaceChildren(requestLabel, request, resultLabel, result);
|
|
434
439
|
const relatedLinks = relatedGitHubLinks(task, { compact: true });
|
|
435
440
|
const time = timestampControl(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const GITHUB_REPOSITORY = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]+$/;
|
|
2
|
-
const REFERENCE_PATTERN = /https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)\/(issues|pull)\/([1-9]\d*)(?![A-Za-z0-9_])|([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)#([1-9]\d*)(?![A-Za-z0-9_])|#([1-9]\d*)(?![A-Za-z0-9_])|https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9_-])?)\/?(?=$|[\s),.;:!?'"\]}>])/gi;
|
|
2
|
+
const REFERENCE_PATTERN = /https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)\/(issues|pull)\/([1-9]\d*)(?![A-Za-z0-9_])|([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)#([1-9]\d*)(?![A-Za-z0-9_])|#([1-9]\d*)(?![A-Za-z0-9_])|https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9_-])?)\/?(?=$|[\s#),.;:!?'"\]}>])/gi;
|
|
3
|
+
const ABSOLUTE_HTTP_PATTERN = /https?:\/\/[^\s<>"'`]+/gi;
|
|
3
4
|
export const MAX_RELATED_GITHUB_LINKS = 20;
|
|
4
5
|
|
|
5
6
|
function canonicalRepository(owner, repository) {
|
|
@@ -23,10 +24,35 @@ export function normalizeGitHubRepository(value) {
|
|
|
23
24
|
|
|
24
25
|
function rawReferences(text) {
|
|
25
26
|
const value = String(text ?? "");
|
|
27
|
+
const absoluteRanges = absoluteHttpCandidates(value);
|
|
26
28
|
const references = [];
|
|
27
29
|
for (const match of value.matchAll(REFERENCE_PATTERN)) {
|
|
28
30
|
const start = match.index;
|
|
29
31
|
const preceding = start > 0 ? value[start - 1] : "";
|
|
32
|
+
let explicitAbsoluteUrl = null;
|
|
33
|
+
const containingAbsoluteUrl = absoluteRanges.find((range) => (
|
|
34
|
+
start >= range.start && start < range.end
|
|
35
|
+
));
|
|
36
|
+
if (containingAbsoluteUrl) {
|
|
37
|
+
const explicitGitHubUrl = Boolean(match[1] || match[9]);
|
|
38
|
+
let absoluteHost = null;
|
|
39
|
+
try {
|
|
40
|
+
absoluteHost = new URL(containingAbsoluteUrl.text).hostname.toLowerCase();
|
|
41
|
+
} catch {
|
|
42
|
+
// Invalid absolute URL tokens still block nested shorthand recognition.
|
|
43
|
+
}
|
|
44
|
+
const completeGitHubUrl = explicitGitHubUrl
|
|
45
|
+
&& start === containingAbsoluteUrl.start
|
|
46
|
+
&& (absoluteHost === "github.com" || absoluteHost === "www.github.com");
|
|
47
|
+
if (!completeGitHubUrl) continue;
|
|
48
|
+
const safeUrl = safeAbsoluteHttpUrl(containingAbsoluteUrl.text);
|
|
49
|
+
if (!safeUrl) continue;
|
|
50
|
+
explicitAbsoluteUrl = {
|
|
51
|
+
end: containingAbsoluteUrl.end,
|
|
52
|
+
suffix: containingAbsoluteUrl.text.slice(match[0].length),
|
|
53
|
+
text: containingAbsoluteUrl.text,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
30
56
|
if (match[8] && /[\w&#/]/.test(preceding)) continue;
|
|
31
57
|
if ((match[5] || match[1] || match[9]) && /[\w/]/.test(preceding)) continue;
|
|
32
58
|
|
|
@@ -45,14 +71,15 @@ function rawReferences(text) {
|
|
|
45
71
|
[owner, repository] = normalized.split("/");
|
|
46
72
|
}
|
|
47
73
|
references.push({
|
|
48
|
-
end: start + match[0].length,
|
|
74
|
+
end: explicitAbsoluteUrl?.end ?? start + match[0].length,
|
|
49
75
|
explicit,
|
|
50
76
|
number,
|
|
51
77
|
owner,
|
|
52
78
|
repository,
|
|
53
79
|
start,
|
|
54
|
-
text: match[0],
|
|
80
|
+
text: explicitAbsoluteUrl?.text ?? match[0],
|
|
55
81
|
type,
|
|
82
|
+
urlSuffix: explicitAbsoluteUrl?.suffix ?? "",
|
|
56
83
|
});
|
|
57
84
|
}
|
|
58
85
|
return references;
|
|
@@ -83,26 +110,111 @@ function resolvedReference(reference, repositoryContext) {
|
|
|
83
110
|
if (reference.type === "repository") {
|
|
84
111
|
return {
|
|
85
112
|
kind: "link",
|
|
113
|
+
label: `${owner}/${repositoryName}`,
|
|
86
114
|
number: null,
|
|
87
115
|
owner,
|
|
116
|
+
provider: "github",
|
|
88
117
|
repository: repositoryName,
|
|
89
118
|
text: reference.text,
|
|
90
119
|
type: reference.type,
|
|
91
|
-
url: `https://github.com/${owner}/${repositoryName}`,
|
|
120
|
+
url: `https://github.com/${owner}/${repositoryName}${reference.urlSuffix}`,
|
|
92
121
|
};
|
|
93
122
|
}
|
|
94
123
|
const path = reference.type === "pull" ? "pull" : "issues";
|
|
95
124
|
return {
|
|
96
125
|
kind: "link",
|
|
126
|
+
label: `${owner}/${repositoryName}#${reference.number}`,
|
|
97
127
|
number: reference.number,
|
|
98
128
|
owner,
|
|
129
|
+
provider: "github",
|
|
99
130
|
repository: repositoryName,
|
|
100
131
|
text: reference.text,
|
|
101
132
|
type: reference.type,
|
|
102
|
-
url: `https://github.com/${owner}/${repositoryName}/${path}/${reference.number}`,
|
|
133
|
+
url: `https://github.com/${owner}/${repositoryName}/${path}/${reference.number}${reference.urlSuffix}`,
|
|
103
134
|
};
|
|
104
135
|
}
|
|
105
136
|
|
|
137
|
+
function trimAbsoluteUrl(candidate) {
|
|
138
|
+
let trimmed = candidate;
|
|
139
|
+
let previous;
|
|
140
|
+
do {
|
|
141
|
+
previous = trimmed;
|
|
142
|
+
trimmed = trimmed.replace(/[.,;:!?]+$/u, "");
|
|
143
|
+
for (const [opening, closing] of [["(", ")"], ["[", "]"], ["{", "}"]]) {
|
|
144
|
+
while (trimmed.endsWith(closing)) {
|
|
145
|
+
const openings = [...trimmed].filter((character) => character === opening).length;
|
|
146
|
+
const closings = [...trimmed].filter((character) => character === closing).length;
|
|
147
|
+
if (closings <= openings) break;
|
|
148
|
+
trimmed = trimmed.slice(0, -1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} while (trimmed !== previous);
|
|
152
|
+
return trimmed;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function absoluteHttpCandidates(text) {
|
|
156
|
+
const candidates = [];
|
|
157
|
+
for (const match of String(text ?? "").matchAll(ABSOLUTE_HTTP_PATTERN)) {
|
|
158
|
+
const candidate = trimAbsoluteUrl(match[0]);
|
|
159
|
+
if (!candidate) continue;
|
|
160
|
+
candidates.push({
|
|
161
|
+
end: match.index + candidate.length,
|
|
162
|
+
start: match.index,
|
|
163
|
+
text: candidate,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return candidates;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function hasDotPathSegment(candidate) {
|
|
170
|
+
const authorityAndPath = candidate.slice(candidate.indexOf("//") + 2).split(/[?#]/u, 1)[0];
|
|
171
|
+
const pathStart = authorityAndPath.indexOf("/");
|
|
172
|
+
if (pathStart < 0) return false;
|
|
173
|
+
return authorityAndPath.slice(pathStart).split("/").some((segment) => {
|
|
174
|
+
try {
|
|
175
|
+
const decoded = decodeURIComponent(segment);
|
|
176
|
+
return decoded === "." || decoded === "..";
|
|
177
|
+
} catch {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function safeAbsoluteHttpUrl(candidate) {
|
|
184
|
+
try {
|
|
185
|
+
const url = new URL(candidate);
|
|
186
|
+
if (
|
|
187
|
+
!url.hostname
|
|
188
|
+
|| url.username
|
|
189
|
+
|| url.password
|
|
190
|
+
|| candidate.includes("\\")
|
|
191
|
+
|| hasDotPathSegment(candidate)
|
|
192
|
+
) return null;
|
|
193
|
+
return url.href;
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function rawAbsoluteHttpReferences(text) {
|
|
200
|
+
const references = [];
|
|
201
|
+
for (const candidate of absoluteHttpCandidates(text)) {
|
|
202
|
+
const url = safeAbsoluteHttpUrl(candidate.text);
|
|
203
|
+
if (!url) continue;
|
|
204
|
+
references.push({
|
|
205
|
+
end: candidate.end,
|
|
206
|
+
kind: "link",
|
|
207
|
+
label: candidate.text,
|
|
208
|
+
provider: "web",
|
|
209
|
+
start: candidate.start,
|
|
210
|
+
text: candidate.text,
|
|
211
|
+
type: "url",
|
|
212
|
+
url,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return references;
|
|
216
|
+
}
|
|
217
|
+
|
|
106
218
|
export function githubReferenceSegments(text, {
|
|
107
219
|
projectRepositories = [],
|
|
108
220
|
taskRepository = null,
|
|
@@ -134,6 +246,45 @@ export function githubReferenceSegments(text, {
|
|
|
134
246
|
return segments;
|
|
135
247
|
}
|
|
136
248
|
|
|
249
|
+
export function referenceSegments(text, options = {}) {
|
|
250
|
+
const value = String(text ?? "");
|
|
251
|
+
const githubSegments = githubReferenceSegments(value, options);
|
|
252
|
+
const references = [];
|
|
253
|
+
let cursor = 0;
|
|
254
|
+
for (const segment of githubSegments) {
|
|
255
|
+
const start = value.indexOf(segment.text, cursor);
|
|
256
|
+
if (start < 0) continue;
|
|
257
|
+
if (segment.kind !== "text") {
|
|
258
|
+
references.push({ end: start + segment.text.length, segment, start });
|
|
259
|
+
}
|
|
260
|
+
cursor = start + segment.text.length;
|
|
261
|
+
}
|
|
262
|
+
const absoluteReferences = rawAbsoluteHttpReferences(value).filter((reference) => (
|
|
263
|
+
!references.some(({ end, start }) => reference.start < end && reference.end > start)
|
|
264
|
+
));
|
|
265
|
+
if (absoluteReferences.length === 0) return githubSegments;
|
|
266
|
+
|
|
267
|
+
references.push(...absoluteReferences.map((segment) => ({
|
|
268
|
+
end: segment.end,
|
|
269
|
+
segment,
|
|
270
|
+
start: segment.start,
|
|
271
|
+
})));
|
|
272
|
+
references.sort((left, right) => left.start - right.start);
|
|
273
|
+
|
|
274
|
+
const segments = [];
|
|
275
|
+
let offset = 0;
|
|
276
|
+
for (const reference of references) {
|
|
277
|
+
if (reference.start < offset) continue;
|
|
278
|
+
if (reference.start > offset) {
|
|
279
|
+
segments.push({ kind: "text", text: value.slice(offset, reference.start) });
|
|
280
|
+
}
|
|
281
|
+
segments.push(reference.segment);
|
|
282
|
+
offset = reference.end;
|
|
283
|
+
}
|
|
284
|
+
if (offset < value.length) segments.push({ kind: "text", text: value.slice(offset) });
|
|
285
|
+
return segments;
|
|
286
|
+
}
|
|
287
|
+
|
|
137
288
|
function taskTexts(task) {
|
|
138
289
|
const texts = [task.instruction];
|
|
139
290
|
for (const turn of task.turns ?? []) {
|
|
@@ -146,16 +297,9 @@ function taskTexts(task) {
|
|
|
146
297
|
return texts.filter((text) => typeof text === "string" && text.length > 0);
|
|
147
298
|
}
|
|
148
299
|
|
|
149
|
-
function relatedLinkLabel(link
|
|
300
|
+
function relatedLinkLabel(link) {
|
|
150
301
|
if (link.type === "repository") return `${link.owner}/${link.repository}`;
|
|
151
|
-
|
|
152
|
-
? `PR #${link.number}`
|
|
153
|
-
: link.type === "issue"
|
|
154
|
-
? `Issue #${link.number}`
|
|
155
|
-
: `#${link.number}`;
|
|
156
|
-
if (!includeRepository) return reference;
|
|
157
|
-
const repository = includeOwner ? `${link.owner}/${link.repository}` : link.repository;
|
|
158
|
-
return `${repository} ${reference}`;
|
|
302
|
+
return `${link.owner}/${link.repository}#${link.number}`;
|
|
159
303
|
}
|
|
160
304
|
|
|
161
305
|
export function taskGitHubProjection(task) {
|
|
@@ -179,7 +323,7 @@ export function taskGitHubProjection(task) {
|
|
|
179
323
|
taskRepository,
|
|
180
324
|
})) {
|
|
181
325
|
if (segment.kind !== "link") continue;
|
|
182
|
-
const key = segment.url
|
|
326
|
+
const key = segment.url;
|
|
183
327
|
if (seen.has(key)) continue;
|
|
184
328
|
if (links.length === MAX_RELATED_GITHUB_LINKS) {
|
|
185
329
|
truncated = true;
|
|
@@ -189,17 +333,9 @@ export function taskGitHubProjection(task) {
|
|
|
189
333
|
links.push(segment);
|
|
190
334
|
}
|
|
191
335
|
}
|
|
192
|
-
const repositories = new Set(links.map(({ owner, repository }) => `${owner}/${repository}`));
|
|
193
|
-
const repositoryNames = new Map();
|
|
194
|
-
for (const link of links) {
|
|
195
|
-
const owners = repositoryNames.get(link.repository) ?? new Set();
|
|
196
|
-
owners.add(link.owner);
|
|
197
|
-
repositoryNames.set(link.repository, owners);
|
|
198
|
-
}
|
|
199
|
-
const includeRepository = repositories.size > 1;
|
|
200
336
|
return {
|
|
201
337
|
relatedGitHubLinks: links.map((link) => ({
|
|
202
|
-
label: relatedLinkLabel(link
|
|
338
|
+
label: relatedLinkLabel(link),
|
|
203
339
|
number: link.number,
|
|
204
340
|
owner: link.owner,
|
|
205
341
|
repository: link.repository,
|