specrails-desktop 2.24.2 → 2.25.0
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/client/dist/assets/{ActivityFeedPage-D0HbUDeS.js → ActivityFeedPage-BGRTOOhT.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-Brth-3zu.js → AgentBrowserCapture-DvDLxsUy.js} +1 -1
- package/client/dist/assets/{AgentModeCodePane-UuKgXzPX.js → AgentModeCodePane-DfwctjzN.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-_P5Xm6ij.js → AgentModeJobsPane-G3WrS-sE.js} +1 -1
- package/client/dist/assets/{AgentsPage-BHpan6kZ.js → AgentsPage-DGFFijMC.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-kryIHuZy.js → AnalyticsPage-hoJmlEpk.js} +1 -1
- package/client/dist/assets/{BarChart-C5W41oN4.js → BarChart-BEhAXVK-.js} +1 -1
- package/client/dist/assets/{CodePage-B0WhKBeS.js → CodePage-B4zB9DVC.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-u9a30yar.js → DesktopAnalyticsPage-CBX-Qpxd.js} +1 -1
- package/client/dist/assets/{DocsDialog-GSzYhxfg.js → DocsDialog-lTsnNdAM.js} +1 -1
- package/client/dist/assets/{DocsPage-Bm0J873n.js → DocsPage-CkvdGroT.js} +1 -1
- package/client/dist/assets/{ExportDropdown-CXc3MBkq.js → ExportDropdown-Da98gR-P.js} +1 -1
- package/client/dist/assets/{IntegrationsPage-Z5Qy3n6G.js → IntegrationsPage-Cf9o4eZq.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-BbBSAQm1.js → InteractiveJobComposer-C8N-Bbnn.js} +1 -1
- package/client/dist/assets/{JobDetailModal-CdPgkHdb.js → JobDetailModal-BZVwIIEx.js} +1 -1
- package/client/dist/assets/{JobDetailPage-CprNUYlt.js → JobDetailPage-CQewQ1zy.js} +1 -1
- package/client/dist/assets/{JobsPage-ClIaG5ow.js → JobsPage-CB4wOxmE.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-rWwSzVr_.js → LoopBuilderPage-B9Y5gt08.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-B0VUdegW.js → LoopPreviewModal-kxQ5kV1t.js} +1 -1
- package/client/dist/assets/{LoopsPage-Brrl03uv.js → LoopsPage-RysW-6sC.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-i1aAs49_.js → ProjectSettingsDialog-BHN5qsGT.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-C92-Ygm2.js → TemplatePreviewModal-DW6lZCIs.js} +1 -1
- package/client/dist/assets/agent-4ARs0uac2.js +1 -0
- package/client/dist/assets/agent-C55wwsUX.js +1 -0
- package/client/dist/assets/agent-C_kqyrXo.js +1 -0
- package/client/dist/assets/agent-Df3lnHMu.js +1 -0
- package/client/dist/assets/agent-G6Zn2gpr.js +1 -0
- package/client/dist/assets/agent-MQhun7cv.js +1 -0
- package/client/dist/assets/agent-NqlER4Yh2.js +1 -0
- package/client/dist/assets/agent-OTGI6t7e2.js +1 -0
- package/client/dist/assets/{brain-CJ92kxwM.js → brain-B9dkGuAN.js} +1 -1
- package/client/dist/assets/{dist-js-CAU8gTUN.js → dist-js-BW5-IZTP.js} +1 -1
- package/client/dist/assets/{dist-js-BVonQLiD.js → dist-js-Bbd7Wzdw.js} +1 -1
- package/client/dist/assets/index-BtAks9nm.css +2 -0
- package/client/dist/assets/{index-CfHBmcjb.js → index-DC-REAcL.js} +49 -49
- package/client/dist/assets/{lib-DJy6HBRz.js → lib-EIkIPf26.js} +1 -1
- package/client/dist/assets/{upload-D_1UxNS_.js → upload-BCMl-nCg.js} +1 -1
- package/client/dist/assets/{value-BH0Vlt96.js → value-CG-amSq1.js} +1 -1
- package/client/dist/index.html +3 -3
- package/package.json +1 -1
- package/server/dist/active-pr-continuation.js +84 -13
- package/server/dist/jira/jira-adf.js +317 -36
- package/server/dist/loop-factory.js +1 -1
- package/server/dist/loop-run-manager.js +49 -6
- package/server/dist/project-router-git.js +26 -0
- package/server/dist/rail-isolated-launch.js +9 -0
- package/server/dist/rail-pr-store.js +1 -0
- package/server/dist/worktree-manager.js +24 -1
- package/client/dist/assets/agent-2gmrdgKk.js +0 -1
- package/client/dist/assets/agent-Bq74VvOq.js +0 -1
- package/client/dist/assets/agent-BrhvIxZF.js +0 -1
- package/client/dist/assets/agent-CbHTqhBt.js +0 -1
- package/client/dist/assets/agent-Ch_OIjXA2.js +0 -1
- package/client/dist/assets/agent-E-X9b1uz2.js +0 -1
- package/client/dist/assets/agent-i5VEzNfj2.js +0 -1
- package/client/dist/assets/agent-zI95bSiF.js +0 -1
- package/client/dist/assets/index-B9483NYu.css +0 -2
|
@@ -16,15 +16,195 @@ exports.railReviewCommentMarker = railReviewCommentMarker;
|
|
|
16
16
|
exports.bodyContainsMarker = bodyContainsMarker;
|
|
17
17
|
exports.commentHasMarker = commentHasMarker;
|
|
18
18
|
exports.adfToText = adfToText;
|
|
19
|
-
|
|
19
|
+
function textNode(text, marks) {
|
|
20
|
+
if (!text)
|
|
21
|
+
return [];
|
|
22
|
+
return marks && Array.isArray(marks) && marks.length > 0
|
|
23
|
+
? [{ type: 'text', text, marks }]
|
|
24
|
+
: [{ type: 'text', text }];
|
|
25
|
+
}
|
|
26
|
+
function parseInline(input) {
|
|
27
|
+
const out = [];
|
|
28
|
+
let i = 0;
|
|
29
|
+
const pushPlainUntil = (next) => {
|
|
30
|
+
if (next > i)
|
|
31
|
+
out.push(...textNode(input.slice(i, next)));
|
|
32
|
+
i = next;
|
|
33
|
+
};
|
|
34
|
+
while (i < input.length) {
|
|
35
|
+
const starts = [
|
|
36
|
+
input.indexOf('`', i),
|
|
37
|
+
input.indexOf('**', i),
|
|
38
|
+
input.indexOf('*', i),
|
|
39
|
+
input.indexOf('[', i),
|
|
40
|
+
].filter((n) => n >= 0);
|
|
41
|
+
const next = starts.length > 0 ? Math.min(...starts) : -1;
|
|
42
|
+
if (next < 0) {
|
|
43
|
+
out.push(...textNode(input.slice(i)));
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
pushPlainUntil(next);
|
|
47
|
+
if (input.startsWith('`', i)) {
|
|
48
|
+
const end = input.indexOf('`', i + 1);
|
|
49
|
+
if (end > i + 1) {
|
|
50
|
+
out.push(...textNode(input.slice(i + 1, end), [{ type: 'code' }]));
|
|
51
|
+
i = end + 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (input.startsWith('**', i)) {
|
|
56
|
+
const end = input.indexOf('**', i + 2);
|
|
57
|
+
if (end > i + 2) {
|
|
58
|
+
out.push(...textNode(input.slice(i + 2, end), [{ type: 'strong' }]));
|
|
59
|
+
i = end + 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (input.startsWith('[', i)) {
|
|
64
|
+
const close = input.indexOf('](', i + 1);
|
|
65
|
+
const end = close >= 0 ? input.indexOf(')', close + 2) : -1;
|
|
66
|
+
if (close > i + 1 && end > close + 2) {
|
|
67
|
+
const label = input.slice(i + 1, close);
|
|
68
|
+
const href = input.slice(close + 2, end);
|
|
69
|
+
out.push(...textNode(label, [{ type: 'link', attrs: { href } }]));
|
|
70
|
+
i = end + 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (input.startsWith('*', i) && !input.startsWith('**', i)) {
|
|
75
|
+
const end = input.indexOf('*', i + 1);
|
|
76
|
+
if (end > i + 1) {
|
|
77
|
+
out.push(...textNode(input.slice(i + 1, end), [{ type: 'em' }]));
|
|
78
|
+
i = end + 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
out.push(...textNode(input[i]));
|
|
83
|
+
i += 1;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function paragraphFromLines(lines) {
|
|
88
|
+
if (lines.length === 0 || lines.every((line) => line.length === 0))
|
|
89
|
+
return { type: 'paragraph' };
|
|
90
|
+
const content = [];
|
|
91
|
+
lines.forEach((line, index) => {
|
|
92
|
+
if (index > 0)
|
|
93
|
+
content.push({ type: 'hardBreak' });
|
|
94
|
+
content.push(...parseInline(line));
|
|
95
|
+
});
|
|
96
|
+
return content.length > 0 ? { type: 'paragraph', content } : { type: 'paragraph' };
|
|
97
|
+
}
|
|
98
|
+
function isSpecialMarkdownLine(line) {
|
|
99
|
+
return /^```/.test(line) ||
|
|
100
|
+
/^(#{1,6})\s+\S/.test(line) ||
|
|
101
|
+
/^\s*[-*+]\s+\S/.test(line) ||
|
|
102
|
+
/^\s*\d+[.)]\s+\S/.test(line) ||
|
|
103
|
+
/^>\s?/.test(line) ||
|
|
104
|
+
/^\s*---+\s*$/.test(line);
|
|
105
|
+
}
|
|
106
|
+
function listItem(text) {
|
|
107
|
+
return { type: 'listItem', content: [paragraphFromLines([text])] };
|
|
108
|
+
}
|
|
109
|
+
/** Build an ADF document from Specrails Markdown. */
|
|
20
110
|
function textToAdf(text) {
|
|
21
|
-
const
|
|
111
|
+
const lines = text.split('\n');
|
|
112
|
+
const content = [];
|
|
113
|
+
let i = 0;
|
|
114
|
+
const pushParagraph = (paragraphLines) => {
|
|
115
|
+
content.push(paragraphFromLines(paragraphLines));
|
|
116
|
+
};
|
|
117
|
+
while (i < lines.length) {
|
|
118
|
+
const line = lines[i];
|
|
119
|
+
if (line.length === 0) {
|
|
120
|
+
content.push({ type: 'paragraph' });
|
|
121
|
+
i += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const fence = /^```([A-Za-z0-9_-]+)?\s*$/.exec(line);
|
|
125
|
+
if (fence) {
|
|
126
|
+
const codeLines = [];
|
|
127
|
+
i += 1;
|
|
128
|
+
while (i < lines.length && !/^```\s*$/.test(lines[i])) {
|
|
129
|
+
codeLines.push(lines[i]);
|
|
130
|
+
i += 1;
|
|
131
|
+
}
|
|
132
|
+
if (i < lines.length)
|
|
133
|
+
i += 1;
|
|
134
|
+
content.push({
|
|
135
|
+
type: 'codeBlock',
|
|
136
|
+
...(fence[1] ? { attrs: { language: fence[1] } } : {}),
|
|
137
|
+
content: codeLines.length > 0 ? [{ type: 'text', text: codeLines.join('\n') }] : [],
|
|
138
|
+
});
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
142
|
+
if (heading) {
|
|
143
|
+
content.push({
|
|
144
|
+
type: 'heading',
|
|
145
|
+
attrs: { level: heading[1].length },
|
|
146
|
+
content: parseInline(heading[2]),
|
|
147
|
+
});
|
|
148
|
+
i += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const bullet = /^\s*[-*+]\s+(.+)$/.exec(line);
|
|
152
|
+
if (bullet) {
|
|
153
|
+
const items = [];
|
|
154
|
+
while (i < lines.length) {
|
|
155
|
+
const m = /^\s*[-*+]\s+(.+)$/.exec(lines[i]);
|
|
156
|
+
if (!m)
|
|
157
|
+
break;
|
|
158
|
+
items.push(listItem(m[1]));
|
|
159
|
+
i += 1;
|
|
160
|
+
}
|
|
161
|
+
content.push({ type: 'bulletList', content: items });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const ordered = /^\s*(\d+)[.)]\s+(.+)$/.exec(line);
|
|
165
|
+
if (ordered) {
|
|
166
|
+
const items = [];
|
|
167
|
+
const order = parseInt(ordered[1], 10);
|
|
168
|
+
while (i < lines.length) {
|
|
169
|
+
const m = /^\s*\d+[.)]\s+(.+)$/.exec(lines[i]);
|
|
170
|
+
if (!m)
|
|
171
|
+
break;
|
|
172
|
+
items.push(listItem(m[1]));
|
|
173
|
+
i += 1;
|
|
174
|
+
}
|
|
175
|
+
content.push({ type: 'orderedList', attrs: { order }, content: items });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const quote = /^>\s?(.*)$/.exec(line);
|
|
179
|
+
if (quote) {
|
|
180
|
+
const quoteLines = [];
|
|
181
|
+
while (i < lines.length) {
|
|
182
|
+
const m = /^>\s?(.*)$/.exec(lines[i]);
|
|
183
|
+
if (!m)
|
|
184
|
+
break;
|
|
185
|
+
quoteLines.push(m[1]);
|
|
186
|
+
i += 1;
|
|
187
|
+
}
|
|
188
|
+
content.push({ type: 'blockquote', content: [paragraphFromLines(quoteLines)] });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (/^\s*---+\s*$/.test(line)) {
|
|
192
|
+
content.push({ type: 'rule' });
|
|
193
|
+
i += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const paragraphLines = [line];
|
|
197
|
+
i += 1;
|
|
198
|
+
while (i < lines.length && lines[i].length > 0 && !isSpecialMarkdownLine(lines[i])) {
|
|
199
|
+
paragraphLines.push(lines[i]);
|
|
200
|
+
i += 1;
|
|
201
|
+
}
|
|
202
|
+
pushParagraph(paragraphLines);
|
|
203
|
+
}
|
|
22
204
|
return {
|
|
23
205
|
type: 'doc',
|
|
24
206
|
version: 1,
|
|
25
|
-
content:
|
|
26
|
-
? { type: 'paragraph' }
|
|
27
|
-
: { type: 'paragraph', content: [{ type: 'text', text: line }] }),
|
|
207
|
+
content: content.length > 0 ? content : [{ type: 'paragraph' }],
|
|
28
208
|
};
|
|
29
209
|
}
|
|
30
210
|
/** Render a body for the target deployment: ADF for Cloud v3, plain for DC v2. */
|
|
@@ -74,6 +254,8 @@ function railReviewCommentMarker(refId, ticketId) {
|
|
|
74
254
|
function bodyContainsMarker(body, marker) {
|
|
75
255
|
if (typeof body === 'string')
|
|
76
256
|
return body.includes(marker);
|
|
257
|
+
if (adfToText(body).includes(marker))
|
|
258
|
+
return true;
|
|
77
259
|
try {
|
|
78
260
|
return JSON.stringify(body).includes(marker);
|
|
79
261
|
}
|
|
@@ -100,42 +282,141 @@ function commentHasMarker(comment, marker) {
|
|
|
100
282
|
}
|
|
101
283
|
return bodyContainsMarker(comment.body, marker);
|
|
102
284
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Flatten an ADF document (or plain string) back to text — used to read inbound
|
|
105
|
-
* Jira descriptions/comments into the local cache.
|
|
106
|
-
*/
|
|
107
|
-
const BLOCK_SEPARATOR_TYPES = new Set([
|
|
108
|
-
'paragraph',
|
|
109
|
-
'heading',
|
|
110
|
-
'codeBlock',
|
|
111
|
-
'blockquote',
|
|
112
|
-
'panel',
|
|
113
|
-
'tableRow',
|
|
114
|
-
]);
|
|
115
285
|
function adfToText(body) {
|
|
116
286
|
if (body == null)
|
|
117
287
|
return '';
|
|
118
288
|
if (typeof body === 'string')
|
|
119
289
|
return body;
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
290
|
+
const plainParagraphLines = (nodes) => {
|
|
291
|
+
if (!Array.isArray(nodes))
|
|
292
|
+
return null;
|
|
293
|
+
const lines = [];
|
|
294
|
+
for (const raw of nodes) {
|
|
295
|
+
if (!raw || typeof raw !== 'object')
|
|
296
|
+
return null;
|
|
297
|
+
const node = raw;
|
|
298
|
+
if (node.type !== 'paragraph')
|
|
299
|
+
return null;
|
|
300
|
+
if (node.content === undefined) {
|
|
301
|
+
lines.push('');
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (!Array.isArray(node.content))
|
|
305
|
+
return null;
|
|
306
|
+
let line = '';
|
|
307
|
+
for (const child of node.content) {
|
|
308
|
+
if (!child || typeof child !== 'object')
|
|
309
|
+
return null;
|
|
310
|
+
const inline = child;
|
|
311
|
+
if (inline.type !== 'text' || typeof inline.text !== 'string')
|
|
312
|
+
return null;
|
|
313
|
+
if (Array.isArray(inline.marks) && inline.marks.length > 0)
|
|
314
|
+
return null;
|
|
315
|
+
line += inline.text;
|
|
316
|
+
}
|
|
317
|
+
lines.push(line);
|
|
137
318
|
}
|
|
319
|
+
return lines;
|
|
320
|
+
};
|
|
321
|
+
const inlineText = (nodes) => {
|
|
322
|
+
if (!Array.isArray(nodes))
|
|
323
|
+
return '';
|
|
324
|
+
return nodes.map((node) => {
|
|
325
|
+
if (!node || typeof node !== 'object')
|
|
326
|
+
return '';
|
|
327
|
+
const n = node;
|
|
328
|
+
if (n.type === 'hardBreak')
|
|
329
|
+
return '\n';
|
|
330
|
+
if (n.type === 'mention' && typeof n.attrs?.text === 'string')
|
|
331
|
+
return n.attrs.text;
|
|
332
|
+
if (n.type !== 'text' || typeof n.text !== 'string')
|
|
333
|
+
return '';
|
|
334
|
+
let text = n.text;
|
|
335
|
+
const marks = Array.isArray(n.marks) ? n.marks : [];
|
|
336
|
+
for (const mark of marks) {
|
|
337
|
+
if (mark.type === 'code')
|
|
338
|
+
text = `\`${text}\``;
|
|
339
|
+
else if (mark.type === 'strong')
|
|
340
|
+
text = `**${text}**`;
|
|
341
|
+
else if (mark.type === 'em')
|
|
342
|
+
text = `*${text}*`;
|
|
343
|
+
else if (mark.type === 'strike')
|
|
344
|
+
text = `~~${text}~~`;
|
|
345
|
+
}
|
|
346
|
+
const link = marks.find((mark) => mark.type === 'link' && typeof mark.attrs?.href === 'string');
|
|
347
|
+
if (link?.attrs?.href)
|
|
348
|
+
text = `[${text}](${link.attrs.href})`;
|
|
349
|
+
return text;
|
|
350
|
+
}).join('');
|
|
138
351
|
};
|
|
139
|
-
|
|
140
|
-
|
|
352
|
+
const renderBlocks = (nodes) => {
|
|
353
|
+
if (!Array.isArray(nodes))
|
|
354
|
+
return [];
|
|
355
|
+
const blocks = [];
|
|
356
|
+
const renderListItem = (node, prefix) => {
|
|
357
|
+
const rendered = renderBlocks(node?.content);
|
|
358
|
+
const body = rendered.length > 0 ? rendered.join('\n\n') : '';
|
|
359
|
+
const lines = body.split('\n');
|
|
360
|
+
return lines.map((line, index) => index === 0 ? `${prefix}${line}` : ` ${line}`).join('\n');
|
|
361
|
+
};
|
|
362
|
+
for (const raw of nodes) {
|
|
363
|
+
if (!raw || typeof raw !== 'object')
|
|
364
|
+
continue;
|
|
365
|
+
const node = raw;
|
|
366
|
+
if (node.type === 'paragraph') {
|
|
367
|
+
blocks.push(inlineText(node.content));
|
|
368
|
+
}
|
|
369
|
+
else if (node.type === 'heading') {
|
|
370
|
+
const level = Math.min(6, Math.max(1, Number(node.attrs?.level) || 2));
|
|
371
|
+
blocks.push(`${'#'.repeat(level)} ${inlineText(node.content)}`.trim());
|
|
372
|
+
}
|
|
373
|
+
else if (node.type === 'bulletList') {
|
|
374
|
+
const items = Array.isArray(node.content) ? node.content.map((item) => renderListItem(item, '- ')) : [];
|
|
375
|
+
blocks.push(items.join('\n'));
|
|
376
|
+
}
|
|
377
|
+
else if (node.type === 'orderedList') {
|
|
378
|
+
const start = Number(node.attrs?.order) || 1;
|
|
379
|
+
const items = Array.isArray(node.content)
|
|
380
|
+
? node.content.map((item, index) => renderListItem(item, `${start + index}. `))
|
|
381
|
+
: [];
|
|
382
|
+
blocks.push(items.join('\n'));
|
|
383
|
+
}
|
|
384
|
+
else if (node.type === 'listItem') {
|
|
385
|
+
blocks.push(...renderBlocks(node.content));
|
|
386
|
+
}
|
|
387
|
+
else if (node.type === 'codeBlock') {
|
|
388
|
+
const lang = typeof node.attrs?.language === 'string' ? node.attrs.language : '';
|
|
389
|
+
blocks.push(`\`\`\`${lang}\n${inlineText(node.content)}\n\`\`\``);
|
|
390
|
+
}
|
|
391
|
+
else if (node.type === 'blockquote') {
|
|
392
|
+
const quote = renderBlocks(node.content).join('\n\n');
|
|
393
|
+
blocks.push(quote.split('\n').map((line) => line ? `> ${line}` : '>').join('\n'));
|
|
394
|
+
}
|
|
395
|
+
else if (node.type === 'rule') {
|
|
396
|
+
blocks.push('---');
|
|
397
|
+
}
|
|
398
|
+
else if (node.type === 'panel') {
|
|
399
|
+
blocks.push(renderBlocks(node.content).join('\n\n'));
|
|
400
|
+
}
|
|
401
|
+
else if (node.type === 'table') {
|
|
402
|
+
blocks.push(renderBlocks(node.content).join('\n\n'));
|
|
403
|
+
}
|
|
404
|
+
else if (node.type === 'tableRow') {
|
|
405
|
+
blocks.push(renderBlocks(node.content).join('\n'));
|
|
406
|
+
}
|
|
407
|
+
else if (node.type === 'tableCell' || node.type === 'tableHeader') {
|
|
408
|
+
blocks.push(renderBlocks(node.content).join(' '));
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
blocks.push(...renderBlocks(node.content));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return blocks;
|
|
415
|
+
};
|
|
416
|
+
const root = body;
|
|
417
|
+
const legacyLines = plainParagraphLines(root.content);
|
|
418
|
+
if (legacyLines) {
|
|
419
|
+
return legacyLines.join('\n').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
420
|
+
}
|
|
421
|
+
return renderBlocks(root.content).join('\n\n').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
141
422
|
}
|
|
@@ -6,7 +6,7 @@ exports.isFactoryLoopId = isFactoryLoopId;
|
|
|
6
6
|
exports.factoryLoopMode = factoryLoopMode;
|
|
7
7
|
exports.factoryLoopForMode = factoryLoopForMode;
|
|
8
8
|
const loop_templates_1 = require("./loop-templates");
|
|
9
|
-
const GREEN_GOAL = '
|
|
9
|
+
const GREEN_GOAL = 'Stop only when the latest verification step reports {{const:VERIFICATION_PASS}} and the history proves the spec is implemented with all required tests/build checks passing.';
|
|
10
10
|
// Factory loops run the WHOLE architect→developer→reviewer pipeline inside a
|
|
11
11
|
// single AI step (`/specrails:implement` etc.), so they need far more headroom
|
|
12
12
|
// than the engine defaults (loop 30 min / step 15 min): a real implement can run
|
|
@@ -17,6 +17,37 @@ const loop_decider_1 = require("./loop-decider");
|
|
|
17
17
|
const loop_runs_store_1 = require("./loop-runs-store");
|
|
18
18
|
const ai_invocations_1 = require("./ai-invocations");
|
|
19
19
|
const ids_1 = require("./ids");
|
|
20
|
+
const IMPLEMENT_CMD_TOKEN_RE = /\{\{\s*cmd:(?:implement|batch)\s*\}\}/;
|
|
21
|
+
const IMPLEMENT_CMD_TEXT_RE = /(?:^|\s)(?:\/(?:specrails:|sr:)?(?:implement|batch-implement)|\$(?:implement|batch-implement))\b/;
|
|
22
|
+
const LOOP_BLOCKED_LINE_RE = /(?:^|\n)\s*LOOP_BLOCKED:\s*(.+?)(?:\n|$)/i;
|
|
23
|
+
const HUMAN_PROCEED_QUESTION_RE = /(?:^|\n)\s*(?:\*\*)?How would you like to proceed\??/i;
|
|
24
|
+
const HUMAN_PROCEED_NO_WORK_RE = /\b(?:no code has changed|I haven't launched any agents|I have not launched any agents|not a fresh feature to build|would duplicate work|sync first|treat this as)\b/i;
|
|
25
|
+
function withReviewContinuationContext(base, rawTemplate, spec) {
|
|
26
|
+
if (spec?.status !== 'on_review')
|
|
27
|
+
return base;
|
|
28
|
+
if (!IMPLEMENT_CMD_TOKEN_RE.test(rawTemplate) && !IMPLEMENT_CMD_TEXT_RE.test(rawTemplate) && !IMPLEMENT_CMD_TEXT_RE.test(base))
|
|
29
|
+
return base;
|
|
30
|
+
return [
|
|
31
|
+
base,
|
|
32
|
+
'',
|
|
33
|
+
'---',
|
|
34
|
+
'Specrails rail continuation context:',
|
|
35
|
+
'- This ticket is already on_review and this run is fully unattended; the user has already chosen to continue implementation work.',
|
|
36
|
+
'- If the current branch has an existing open PR, or the feature appears already implemented, do NOT pause to ask whether to proceed and do NOT restart from scratch.',
|
|
37
|
+
'- Treat the run as review follow-ups on the current PR/worktree: inspect the ticket description, PR context, current branch, and working diff; implement only the missing requested deltas.',
|
|
38
|
+
'- If the local branch is ahead/behind its remote, do not stop for confirmation. Continue safely on the current worktree; only pull/rebase when it is clearly safe and necessary.',
|
|
39
|
+
'- If truly no code change is needed, state that clearly and leave the tree unchanged; otherwise make the smallest correct change and let the following verify step prove it.',
|
|
40
|
+
].join('\n');
|
|
41
|
+
}
|
|
42
|
+
function aiStepBlockedReason(text) {
|
|
43
|
+
const explicit = LOOP_BLOCKED_LINE_RE.exec(text);
|
|
44
|
+
if (explicit?.[1]?.trim())
|
|
45
|
+
return explicit[1].trim();
|
|
46
|
+
if (HUMAN_PROCEED_QUESTION_RE.test(text) && HUMAN_PROCEED_NO_WORK_RE.test(text)) {
|
|
47
|
+
return 'The AI step asked how to proceed instead of performing unattended work.';
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
20
51
|
const HISTORY_MAX_CHARS = 1500;
|
|
21
52
|
/** Caps for the `loop_step` detail fields (`template` / `command`) — the
|
|
22
53
|
* payload persists per event, so a runaway prompt must not bloat the events
|
|
@@ -46,9 +77,10 @@ function truncate(s, max = 600) {
|
|
|
46
77
|
// ai-step prompts and shell commands as `{{run.<name>}}`. v1 captures exactly one
|
|
47
78
|
// — `changeId`, the OpenSpec change id — written so the family can generalize.
|
|
48
79
|
const RUN_TOKEN_RE = /\{\{\s*run\.(\w+)\s*\}\}/g;
|
|
49
|
-
/** First `openspec/changes/<id>` path mentioned in a step's output
|
|
50
|
-
*
|
|
51
|
-
|
|
80
|
+
/** First ACTIVE `openspec/changes/<id>` path mentioned in a step's output.
|
|
81
|
+
* Archive paths (`openspec/changes/archive/...`) are historical destinations,
|
|
82
|
+
* not runnable change ids for later `{{run.changeId}}` commands. */
|
|
83
|
+
const CHANGE_ID_RE = /openspec\/changes\/(?!archive(?:\/|$))([A-Za-z0-9._-]+)(?=\/|\s|$)/g;
|
|
52
84
|
/** Replace `{{run.<name>}}` with the captured value; uncaptured → '' (never a
|
|
53
85
|
* leaked literal token). Applied AFTER `{{cmd:*}}` and `{{spec.*}}`. */
|
|
54
86
|
function resolveRunVars(text, vars) {
|
|
@@ -57,6 +89,7 @@ function resolveRunVars(text, vars) {
|
|
|
57
89
|
/** Extract the OpenSpec change id from a step's output (first match wins), or
|
|
58
90
|
* undefined when none is present. */
|
|
59
91
|
function extractChangeId(text) {
|
|
92
|
+
CHANGE_ID_RE.lastIndex = 0;
|
|
60
93
|
const m = CHANGE_ID_RE.exec(text);
|
|
61
94
|
return m ? m[1] : undefined;
|
|
62
95
|
}
|
|
@@ -478,7 +511,8 @@ class LoopRunManager {
|
|
|
478
511
|
// --yes`, codex `$implement #<id> --yes`) — then resolve `{{spec.*}}`
|
|
479
512
|
// data tokens and finally `{{const:*}}` library constants.
|
|
480
513
|
const rawTemplate = String(node.data?.prompt ?? '');
|
|
481
|
-
const
|
|
514
|
+
const expanded = (0, loop_constants_1.resolveConstants)(resolveRunVars((0, loop_graph_1.interpolateSpec)((0, loop_command_catalog_1.expandCommands)(rawTemplate, { provider: nodeProvider, ticketIds: req.spec?.ticketIds, specId: req.spec?.id }), req.spec), runVars), constMap);
|
|
515
|
+
const base = withReviewContinuationContext(expanded, rawTemplate, req.spec);
|
|
482
516
|
// Inject the cross-iteration history only when there's no live session
|
|
483
517
|
// to carry it (a fresh pass) OR right after a Decider 'continue' (so the
|
|
484
518
|
// step sees the verdict). A mid-body resumed step already has it.
|
|
@@ -542,10 +576,13 @@ class LoopRunManager {
|
|
|
542
576
|
resultText: res.text.trim() ? res.text : null,
|
|
543
577
|
});
|
|
544
578
|
const zeroWork = res.zeroWork === true || oneShotZeroWork;
|
|
545
|
-
const
|
|
579
|
+
const blockedReason = aiStepBlockedReason(res.text);
|
|
580
|
+
const stepFailed = res.failed === true || zeroWork || blockedReason !== null;
|
|
546
581
|
const stepErrorText = res.errorText ?? (zeroWork
|
|
547
582
|
? `zero work performed — the command never ran${res.text.trim() ? `: ${res.text.trim()}` : ''}`
|
|
548
|
-
:
|
|
583
|
+
: blockedReason
|
|
584
|
+
? `blocked — ${blockedReason}`
|
|
585
|
+
: undefined);
|
|
549
586
|
// Make the failure reason land visibly INSIDE the step's log
|
|
550
587
|
// segment (the interactive session already surfaced its own note at
|
|
551
588
|
// settle; the Template/Command flat-log lines are gone, so without
|
|
@@ -568,6 +605,12 @@ class LoopRunManager {
|
|
|
568
605
|
aiSessionId = res.sessionId;
|
|
569
606
|
history.push(`AI Step: ${truncate(res.text)}`);
|
|
570
607
|
record(`loop:${runId}`, { ...res, failed: stepFailed }, aiStepStart);
|
|
608
|
+
if (blockedReason) {
|
|
609
|
+
logLine(`\n■ Loop blocked — needs a human decision: ${blockedReason}`, 'stderr');
|
|
610
|
+
outcome = 'blocked';
|
|
611
|
+
settled = true;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
571
614
|
// Capture the OpenSpec change id from a step's output the FIRST time it
|
|
572
615
|
// appears (first-match-wins, kept stable across loop-back iterations so
|
|
573
616
|
// the re-pass amends the same change). Used by `{{run.changeId}}` in the
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerGitRoutes = registerGitRoutes;
|
|
4
4
|
const project_git_1 = require("./project-git");
|
|
5
5
|
const git_diagnostics_1 = require("./git-diagnostics");
|
|
6
|
+
const pr_publisher_1 = require("./pr-publisher");
|
|
6
7
|
// ─── Git domain routes (/api/projects/:projectId/git) ─────────────────────────
|
|
7
8
|
//
|
|
8
9
|
// Backs the Agent-Mode git bar: current branch + last commit + local branches,
|
|
@@ -10,6 +11,7 @@ const git_diagnostics_1 = require("./git-diagnostics");
|
|
|
10
11
|
// REAL repo — under artifact relocation the workspace never holds the code).
|
|
11
12
|
function registerGitRoutes(deps) {
|
|
12
13
|
const { router, ctx } = deps;
|
|
14
|
+
const exec = deps.exec ?? pr_publisher_1.defaultExec;
|
|
13
15
|
router.get('/:projectId/git', async (req, res) => {
|
|
14
16
|
try {
|
|
15
17
|
res.json(await (0, project_git_1.getProjectGitInfo)(ctx(req).project.path));
|
|
@@ -19,6 +21,30 @@ function registerGitRoutes(deps) {
|
|
|
19
21
|
res.status(500).json({ error: 'Failed to read git info' });
|
|
20
22
|
}
|
|
21
23
|
});
|
|
24
|
+
router.get('/:projectId/git/pull-requests/:number', async (req, res) => {
|
|
25
|
+
const prNumber = Number.parseInt(String(req.params.number ?? ''), 10);
|
|
26
|
+
if (!Number.isInteger(prNumber) || prNumber <= 0) {
|
|
27
|
+
res.status(400).json({ error: 'invalid_pull_request_number' });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const r = await exec.run('gh', ['pr', 'view', String(prNumber), '--json', 'url'], ctx(req).project.path);
|
|
32
|
+
if (r.code !== 0) {
|
|
33
|
+
res.status(404).json({ error: 'pull_request_not_found' });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const parsed = JSON.parse(r.stdout);
|
|
37
|
+
if (typeof parsed.url !== 'string' || !parsed.url) {
|
|
38
|
+
res.status(404).json({ error: 'pull_request_not_found' });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
res.json({ prNumber, url: parsed.url });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error('[project-git] pull request lookup failed:', err);
|
|
45
|
+
res.status(500).json({ error: 'pull_request_lookup_failed' });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
22
48
|
// READ-ONLY git/gh diagnostics (backs the specrails_git MCP tool). A fixed
|
|
23
49
|
// allowlist of read-only commands — the query only names an action, never
|
|
24
50
|
// arguments. Git runs against project.path (the real repo; the workspace never
|
|
@@ -301,6 +301,14 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
301
301
|
originSurface: input.originSurface ?? 'dashboard',
|
|
302
302
|
originConversationId: input.originConversationId ?? null,
|
|
303
303
|
}).id;
|
|
304
|
+
if (launchContinuation) {
|
|
305
|
+
(0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, 'building', 'building', {
|
|
306
|
+
branch: launchContinuation.branch,
|
|
307
|
+
prUrl: launchContinuation.prUrl,
|
|
308
|
+
prNumber: launchContinuation.prNumber,
|
|
309
|
+
prState: 'pr-created',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
304
312
|
broadcastPrState();
|
|
305
313
|
syncOriginCard('post');
|
|
306
314
|
}
|
|
@@ -336,6 +344,7 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
336
344
|
ticketId: unit.ticketId,
|
|
337
345
|
baseRef: continuationTarget?.baseRef ?? worktreeBaseRef.baseRef,
|
|
338
346
|
branch: unitBranchName(unit.ticketId),
|
|
347
|
+
refreshFromBaseRef: Boolean(continuationTarget?.baseRef),
|
|
339
348
|
});
|
|
340
349
|
// Per-run overlay: merge-link the framework surface the checkout didn't
|
|
341
350
|
// bring into the worktree (idempotent; resume-safe via its manifest).
|
|
@@ -102,6 +102,22 @@ function worktreeBranch(slug, ticketId) {
|
|
|
102
102
|
function worktreePath(worktreesRoot, ticketId) {
|
|
103
103
|
return path.join(worktreesRoot, `ticket-${ticketId}`);
|
|
104
104
|
}
|
|
105
|
+
async function fastForwardExistingBranch(git, repoDir, branch, baseRef, mountedWorktreePath) {
|
|
106
|
+
if (!baseRef)
|
|
107
|
+
return;
|
|
108
|
+
if (mountedWorktreePath) {
|
|
109
|
+
const ancestor = await git.run(['merge-base', '--is-ancestor', 'HEAD', baseRef], mountedWorktreePath);
|
|
110
|
+
if (ancestor.code !== 0)
|
|
111
|
+
return;
|
|
112
|
+
await git.run(['merge', '--ff-only', baseRef], mountedWorktreePath).catch(() => { });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const localRef = `refs/heads/${branch}`;
|
|
116
|
+
const ancestor = await git.run(['merge-base', '--is-ancestor', localRef, baseRef], repoDir);
|
|
117
|
+
if (ancestor.code !== 0)
|
|
118
|
+
return;
|
|
119
|
+
await git.run(['update-ref', localRef, baseRef], repoDir).catch(() => { });
|
|
120
|
+
}
|
|
105
121
|
/**
|
|
106
122
|
* Create a fresh worktree+branch for a ticket. Idempotent-ish: if the branch
|
|
107
123
|
* already exists it is reused with `-B`. Throws on git failure so the caller can
|
|
@@ -128,9 +144,16 @@ async function createWorktree(git, input) {
|
|
|
128
144
|
// at local-only. Detached HEAD / a git failure falls back to the input.
|
|
129
145
|
const head = await git.run(['rev-parse', '--abbrev-ref', 'HEAD'], wt);
|
|
130
146
|
const actual = head.code === 0 ? head.stdout.trim() : '';
|
|
131
|
-
|
|
147
|
+
const actualBranch = actual && actual !== 'HEAD' ? actual : branch;
|
|
148
|
+
if (input.refreshFromBaseRef && actualBranch === branch) {
|
|
149
|
+
await fastForwardExistingBranch(git, input.repoDir, branch, input.baseRef, wt);
|
|
150
|
+
}
|
|
151
|
+
return { branch: actualBranch, worktreePath: wt };
|
|
132
152
|
}
|
|
133
153
|
const hasBranch = (await git.run(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], input.repoDir)).code === 0;
|
|
154
|
+
if (hasBranch && input.refreshFromBaseRef) {
|
|
155
|
+
await fastForwardExistingBranch(git, input.repoDir, branch, input.baseRef);
|
|
156
|
+
}
|
|
134
157
|
const args = hasBranch
|
|
135
158
|
? ['worktree', 'add', wt, branch]
|
|
136
159
|
: ['worktree', 'add', '-b', branch, wt, base];
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var e=`Missionszentrale`,t=`Specrails per Chat bedienen`,n=`Missionszentrale`,r=`Bitte den Agenten um alles…`,i=`Senden`,a=`Stopp`,o=`Kopieren`,s=`Maximieren`,c=`Wiederherstellen`,l=`Minimieren`,u=`Schließen`,d=`Neue Mission`,f=`Wie lautet die Mission?`,p=`Wähle ein Projekt und frag dann. Das Board aktualisiert sich live, während der Agent arbeitet.`,m=`Denkt nach…`,h=`Verbinden…`,g=`App wird abgefragt…`,_={compact:`Kompakt`,comfortable:`Komfortabel`,max:`Maximieren`},v={label:`Modell`},y={hint:`Verlauf durchsuchen — tippen zum Bearbeiten, ↑↓ zum Navigieren`},b={queued:`In Warteschlange`,send:`In Warteschlange senden`,sendHint:`Der Agent arbeitet noch — diese Nachricht wird eingereiht und danach ausgeführt`,placeholder:`Mehr hinzufügen, während der Agent arbeitet — wird eingereiht…`},x={editing:`Bearbeite eingereihte Nachricht {{n}} von {{m}}`,hint:`↑↓ zum Navigieren · Enter speichert · Esc bricht ab`,save:`Eingereihte Nachricht speichern`,cancel:`Bearbeitung abbrechen`,dispatched:`Diese eingereihte Nachricht wurde bereits gesendet — dein Text bleibt als Entwurf erhalten`,saveFailed:`Eingereihte Nachricht konnte nicht gespeichert werden`},S={branch:`Branch`,detached:`(detached)`,dirty:`Nicht committete Änderungen`,switched:`Zu {{branch}} gewechselt`,switchFailed:`Branch-Wechsel fehlgeschlagen`,lockedWhileWorking:`Branch gesperrt, während der Agent arbeitet`,searchPlaceholder:`Branches durchsuchen…`,noMatches:`Keine passenden Branches`,worktrees:`Worktrees`,worktreePathCopied:`Worktree-Pfad kopiert`,copyWorktreePath:`Worktree-Pfad kopieren`},C={label:`Anbieter`,claude:`Claude`,codex:`Codex`,gemini:`Gemini`},w={cycleHint:`⇧⇥ zum Wechseln`,observe:{name:`Beobachten`,desc:`Nur lesen — nichts ändert sich`},edit:{name:`Bearbeiten`,desc:`Erstellen und bearbeiten (umkehrbar)`},operate:{name:`Ausführen`,desc:`KI starten — kostet Geld`},autonomous:{name:`Autonom`,desc:`Löschen und beenden — unumkehrbar`},lockedTitle:`Höhere Stufe erforderlich`,lockedBody:`Diese Aktion erfordert die Stufe „{{level}}“. Du bist auf „{{current}}“.`,raiseHint:`⇧⇥ um die Stufe zu erhöhen, oder frag anders.`},T={title:`Aktion bestätigen`,costAction:`{{action}} · ~{{cost}} · {{tier}}`,approve:`Genehmigen`,cancel:`Abbrechen`,dontAskAgain:`In dieser Sitzung nicht mehr fragen`,approved:`Genehmigt`,cancelled:`Abgebrochen`},E={home:`Start`,searchPlaceholder:`Mit Specrails sprechen…`,recents:`Zuletzt verwendet`,allProjects:`Alle Projekte`,searchAll:`In allen Projekten suchen`,addProject:`Projekt hinzufügen…`,homeHint:`Kein Projekt ausgewählt — über alle Projekte hinweg.`,askCreateOrSearch:`Es ist kein Projekt ausgewählt. Möchtest du ein neues Projekt erstellen oder über alle Projekte hinweg suchen?`},D={label:`Mission wechseln`,untitled:`Neue Mission`,new:`Neue Mission`,searchPlaceholder:`Missionen durchsuchen…`,noMatches:`Keine Mission gefunden`,working:`Agent arbeitet`,queuedBadge:`{{n}} in Warteschlange`,delete:`Mission "{{title}}" löschen`,deleteConfirm:`Löschen?`,deleteLiveConfirm:`Der Agent arbeitet — trotzdem löschen?`,confirmDelete:`Löschen bestätigen`,cancelDelete:`Mission behalten`,deleteFailed:`Mission konnte nicht gelöscht werden`},O={title:`Kein KI-Anbieter installiert`,body:`Der Agent benötigt Claude, Codex oder Gemini auf deinem Rechner. Installiere einen und öffne den Agenten erneut, um zu chatten.`,placeholder:`Installiere einen KI-Anbieter, um den Agenten zu nutzen…`},k={title:`Specrails MCP ist aus`,body:`Der Agent braucht den eingebetteten MCP-Server, um die App zu bedienen.`,enable:`Specrails MCP aktivieren`,enabling:`Wird aktiviert…`,enabled:`MCP aktiviert`},A={live:`live`,queued:`in Warteschlange`,running:`läuft`,done:`fertig`,failed:`fehlgeschlagen`},j={generic:`Etwas ist schiefgelaufen. Versuch es erneut.`,busy:`Der Agent ist beschäftigt. Versuch es gleich noch einmal.`},M={kindAgent:`Agenten-Mission`},N=`Neue Mission`,P=`Zur Missionszentrale wechseln`,F=`Zum Board wechseln`,I=`Suchen`,L=`Start`,R=`Ohne Titel`,z={title:`Im Workspace`,terminal:`Terminal`,files:`Dateien`,browser:`Browser`,requiresProject:`Wähle ein Projekt, um dieses Tool zu nutzen`,resizeCode:`Code-Bereich anpassen`,closeFiles:`Code schließen`,browserConfirm:`Zum Agenten hinzufügen`,browserCaptured:`Aufnahme hinzugefügt`,uploadFailed:`Upload fehlgeschlagen`,requiresConversation:`Starte eine Mission, um dieses Werkzeug zu nutzen`,jobs:`Jobs`,closeJobs:`Jobs schließen`,resizeJobs:`Jobs-Bereich anpassen`,noJobs:`Noch keine Jobs — bitte den Agenten, einen zu starten`},B=`Datei anhängen`,V=`„{{title}}“ löschen`,H=`Löschen von „{{title}}“ bestätigen`,U=`bestätigen?`,W=`Dieser Anbieter kann noch keine Bilder lesen — hänge stattdessen Textdateien an`,G={label:`Aufwand`,minimal:`Minimal`,low:`Niedrig`,medium:`Mittel`,high:`Hoch`,xhigh:`Sehr hoch`},K=`Wie lautet die Mission, Neo?`,q={rail:`Rail {{index}}`,specCount_one:`{{count}} Spec`,specCount_other:`{{count}} Specs`,base:`Basis-Branch`,openPr:`PR öffnen`,title:{building:`Implementierung in einem isolierten Worktree…`,on_review:`Implementierung bereit zur Prüfung`,pr_draft:`Entwurfs-PR erstellt`,pr_draft_degraded:`PR-Auslieferung unvollständig`,pr_ready:`PR bereit zum Merge`,pr_failed:`PR-Erstellung fehlgeschlagen`,merged:`PR gemergt`,discarded:`Verworfen`},buildingHint:`Nach Abschluss wirst du gefragt, ob der PR erstellt werden soll.`,createPr:`PR erstellen`,publish:`Veröffentlichen`,retry:`Erneut versuchen`,retryPr:`PR erneut versuchen`,checkMerge:`Merge prüfen`,discard:`Verwerfen`,confirmDiscard:`Verwerfen bestätigen?`,mergedNote:`Gemergt — Specs nach Fertig verschoben`,discardedNote:`Verworfen — Specs zurück ins Backlog`,degraded:{pushed:`Der Branch wurde gepusht, aber die PR konnte nicht erstellt werden — erneut versuchen, um abzuschließen.`,localOnly:`Die Änderungen wurden lokal zusammengeführt, aber nicht gepusht — erneut versuchen, um die PR zu veröffentlichen.`},alreadyResolved:`Bereits anderweitig erledigt`,actionFailed:`Aktion fehlgeschlagen`,notMergedYet:`Noch nicht gemergt`,viewLog:`Ausführungsprotokoll für {{ref}} anzeigen`,runElapsedAria:`{{elapsed}} vergangen`,runCostAria:`{{cost}} ausgegeben`,pinned:{label:`Angeheftet`,marker:`Implementierungskarte — oben angeheftet`,collapse:`Angeheftete Karte minimieren`,expand:`Angeheftete Karte ausklappen`,expandCard:`Diese Karte ausklappen`,collapseCard:`Diese Karte minimieren`,collapsed_one:`Implementierungskarte`,collapsed_other:`{{count}} Implementierungskarten`,state:{building:`Läuft`,on_review:`Review`,pr_draft:`Entwurfs-PR`,pr_ready:`Veröffentlicht`,pr_failed:`Fehlgeschlagen`,merged:`Gemerged`,discarded:`Verworfen`}},mergeLocal:`Lokal integrieren`,mergeLocalTooltip:`Führt die gelieferten Branches lokal in {{base}} zusammen (ohne GitHub)`,mergeLocalBlockedDirty:`Integration nicht möglich: dein Arbeitsverzeichnis auf {{base}} hat uncommittete Änderungen. Committe oder stashe sie und versuche es erneut.`,mergeLocalBlockedBranch:`Integration nicht möglich: dein Checkout steht auf {{current}}, nicht auf {{base}}. Wechsle zu {{base}} und versuche es erneut.`,mergedLocally:`Lokal in {{base}} integriert`},J={heading:`Spec-Entwurf`,drafting:`Entwurf wird aktualisiert…`,acceptanceCriteria:`Akzeptanzkriterien`,untitled:`Spec ohne Titel`,expand:`Vollständige Beschreibung anzeigen`,collapse:`Beschreibung einklappen`},Y={openTicket:`Spec #{{id}} öffnen`,openJob:`Job {{id}} öffnen`,openLoop:`Loop {{id}} öffnen`,ticketNotFound:`Spec #{{id}} nicht gefunden — möglicherweise gelöscht`,jobNotFound:`Job nicht gefunden — möglicherweise gelöscht`,loopNotFound:`Loop nicht gefunden — er wurde möglicherweise gelöscht`,lookupFailed:`Referenz konnte nicht geprüft werden — bitte erneut versuchen`},X={untitled:`Unbenannte Mission`,menu:`Missionsmenü`,rename:`Umbenennen`,renamePlaceholder:`Name der Mission`,renameHint:`Zum Umbenennen doppelklicken`,renameFailed:`Umbenennen fehlgeschlagen`,copyName:`Missionsname kopieren`,copyId:`Missions-ID kopieren`,copyProject:`Projektname kopieren`,copyProjectPath:`Projektpfad kopieren`,copyFailed:`Kopieren fehlgeschlagen`,delete:`Mission löschen`,deleteConfirm:`Diese Mission löschen? Kann nicht rückgängig gemacht werden.`,deleteFailed:`Mission konnte nicht gelöscht werden`},Z={title:e,subtitle:t,trigger:n,composerPlaceholder:r,send:i,stop:a,copy:o,maximize:s,restore:c,minimize:l,close:u,newConversation:d,emptyTitle:f,emptyHint:p,thinking:m,connecting:h,consultingCode:g,size:_,model:v,history:y,queue:b,queueEdit:x,git:S,provider:C,tier:w,approval:T,project:E,mission:D,noProvider:O,degraded:k,toolCard:A,error:j,chip:M,newAgent:N,switchToAgent:P,switchToKanban:F,search:I,home:L,untitled:R,workspace:z,attachFile:B,deleteConversation:V,confirmDeleteConversation:H,confirmShort:U,imagesUnsupported:W,effort:G,emptyTitleMatrix:K,prCard:q,specDraft:J,refs:Y,header:X};export{T as approval,B as attachFile,M as chip,u as close,r as composerPlaceholder,H as confirmDeleteConversation,U as confirmShort,h as connecting,g as consultingCode,o as copy,Z as default,k as degraded,V as deleteConversation,G as effort,p as emptyHint,f as emptyTitle,K as emptyTitleMatrix,j as error,S as git,X as header,y as history,L as home,W as imagesUnsupported,s as maximize,l as minimize,D as mission,v as model,N as newAgent,d as newConversation,O as noProvider,q as prCard,E as project,C as provider,b as queue,x as queueEdit,Y as refs,c as restore,I as search,i as send,_ as size,J as specDraft,a as stop,t as subtitle,P as switchToAgent,F as switchToKanban,m as thinking,w as tier,e as title,A as toolCard,n as trigger,R as untitled,z as workspace};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var e=`Contrôle des missions`,t=`Pilotez Specrails en discutant`,n=`Contrôle des missions`,r=`Demandez à l'agent ce que vous voulez…`,i=`Envoyer`,a=`Arrêter`,o=`Copier`,s=`Agrandir`,c=`Restaurer`,l=`Réduire`,u=`Fermer`,d=`Nouvelle mission`,f=`Quelle est la mission ?`,p=`Choisissez un projet, puis demandez. Le tableau se met à jour en direct pendant que l'agent travaille.`,m=`Réflexion…`,h=`Connexion…`,g=`Consultation de l'app…`,_={compact:`Compact`,comfortable:`Confortable`,max:`Agrandir`},v={label:`Modèle`},y={hint:`Parcours de l'historique — tapez pour modifier, ↑↓ pour naviguer`},b={queued:`En file`,send:`Envoyer dans la file`,sendHint:`L'agent travaille encore — ce message sera mis en file puis traité`,placeholder:`Ajoutez-en pendant que l'agent travaille — mis en file…`},x={editing:`Modification du message en file {{n}} sur {{m}}`,hint:`↑↓ pour naviguer · Entrée enregistre · Échap annule`,save:`Enregistrer le message en file`,cancel:`Annuler la modification`,dispatched:`Ce message en file a déjà été envoyé — votre texte est conservé comme brouillon`,saveFailed:`Impossible d'enregistrer le message en file`},S={branch:`Branche`,detached:`(détaché)`,dirty:`Modifications non commitées`,switched:`Basculé sur {{branch}}`,switchFailed:`Impossible de changer de branche`,lockedWhileWorking:`Branche verrouillée pendant que l'agent travaille`,searchPlaceholder:`Rechercher des branches…`,noMatches:`Aucune branche ne correspond`,worktrees:`Worktrees`,worktreePathCopied:`Chemin du worktree copié`,copyWorktreePath:`Copier le chemin du worktree`},C={label:`Fournisseur`,claude:`Claude`,codex:`Codex`,gemini:`Gemini`},w={cycleHint:`⇧⇥ pour changer`,observe:{name:`Observer`,desc:`Lecture seule — rien ne change`},edit:{name:`Modifier`,desc:`Créer et modifier (réversible)`},operate:{name:`Opérer`,desc:`Lancer l'IA — c'est payant`},autonomous:{name:`Autonome`,desc:`Supprimer et arrêter — irréversible`},lockedTitle:`Nécessite un niveau supérieur`,lockedBody:`Cette action nécessite le niveau « {{level}} ». Vous êtes sur « {{current}} ».`,raiseHint:`⇧⇥ pour augmenter le niveau, ou demandez autrement.`},T={title:`Confirmer l'action`,costAction:`{{action}} · ~{{cost}} · {{tier}}`,approve:`Approuver`,cancel:`Annuler`,dontAskAgain:`Ne plus demander durant cette session`,approved:`Approuvé`,cancelled:`Annulé`},E={home:`Accueil`,searchPlaceholder:`Parlez à Specrails…`,recents:`Récents`,allProjects:`Tous les projets`,searchAll:`Rechercher dans tous les projets`,addProject:`Ajouter un projet…`,homeHint:`Aucun projet sélectionné — opération sur tous les projets.`,askCreateOrSearch:`Aucun projet n'est sélectionné. Voulez-vous créer un nouveau projet ou rechercher dans tous les projets ?`},D={label:`Changer de mission`,untitled:`Nouvelle mission`,new:`Nouvelle mission`,searchPlaceholder:`Rechercher des missions…`,noMatches:`Aucune mission ne correspond`,working:`L'agent travaille`,queuedBadge:`{{n}} en file`,delete:`Supprimer la mission "{{title}}"`,deleteConfirm:`Supprimer ?`,deleteLiveConfirm:`L'agent travaille — supprimer quand même ?`,confirmDelete:`Confirmer la suppression`,cancelDelete:`Garder la mission`,deleteFailed:`Impossible de supprimer la mission`},O={title:`Aucun fournisseur d'IA installé`,body:`L'agent a besoin de Claude, Codex ou Gemini sur votre machine. Installez-en un, puis rouvrez l'agent pour discuter.`,placeholder:`Installez un fournisseur d'IA pour utiliser l'agent…`},k={title:`Le MCP Specrails est désactivé`,body:`L'agent a besoin du serveur MCP intégré pour piloter l'app.`,enable:`Activer le MCP Specrails`,enabling:`Activation…`,enabled:`MCP activé`},A={live:`en direct`,queued:`en file`,running:`en cours`,done:`terminé`,failed:`échoué`},j={generic:`Une erreur est survenue. Réessayez.`,busy:`L'agent est occupé. Réessayez dans un instant.`},M={kindAgent:`Mission de l'agent`},N=`Nouvelle mission`,P=`Passer au Contrôle des missions`,F=`Passer au tableau`,I=`Rechercher`,L=`Accueil`,R=`Sans titre`,z={title:`Espace de travail`,terminal:`Terminal`,files:`Fichiers`,browser:`Navigateur`,requiresProject:`Sélectionnez un projet pour utiliser cet outil`,resizeCode:`Redimensionner le volet de code`,closeFiles:`Fermer le code`,browserConfirm:`Ajouter à l'agent`,browserCaptured:`Capture ajoutée`,uploadFailed:`Échec du téléversement`,requiresConversation:`Lancez une mission pour utiliser cet outil`,jobs:`Jobs`,closeJobs:`Fermer les jobs`,resizeJobs:`Redimensionner le panneau des jobs`,noJobs:`Aucun job pour l'instant — demandez à l'agent d'en lancer un`},B=`Joindre un fichier`,V=`Supprimer «\xA0{{title}}\xA0»`,H=`Confirmer la suppression de «\xA0{{title}}\xA0»`,U=`confirmer\xA0?`,W=`Ce fournisseur ne lit pas encore les images — joignez plutôt des fichiers texte`,G={label:`Effort`,minimal:`Minimal`,low:`Faible`,medium:`Moyen`,high:`Élevé`,xhigh:`Très élevé`},K=`Quelle est la mission, Neo ?`,q={rail:`Rail {{index}}`,specCount_one:`{{count}} spec`,specCount_other:`{{count}} specs`,base:`Branche de base`,openPr:`Ouvrir la PR`,title:{building:`Implémentation dans un worktree isolé…`,on_review:`Implémentation prête pour la revue`,pr_draft:`PR en brouillon créée`,pr_draft_degraded:`Livraison de la PR incomplète`,pr_ready:`PR prête à être fusionnée`,pr_failed:`Échec de la création de la PR`,merged:`PR fusionnée`,discarded:`Abandonnée`},buildingHint:`À la fin, il vous sera proposé de créer la PR.`,createPr:`Créer la PR`,publish:`Publier`,retry:`Réessayer`,retryPr:`Réessayer la PR`,checkMerge:`Vérifier le merge`,discard:`Abandonner`,confirmDiscard:`Confirmer l'abandon ?`,mergedNote:`Fusionnée — specs passées en Terminé`,discardedNote:`Abandonnée — specs renvoyées vers le backlog`,degraded:{pushed:`La branche a été poussée mais la PR n'a pas pu être créée — réessayez pour terminer.`,localOnly:`Les modifications ont été assemblées en local mais pas poussées — réessayez pour publier la PR.`},alreadyResolved:`Déjà résolue ailleurs`,actionFailed:`L'action a échoué`,notMergedYet:`Pas encore fusionnée`,viewLog:`Voir le journal d'exécution de {{ref}}`,runElapsedAria:`{{elapsed}} écoulées`,runCostAria:`{{cost}} dépensés`,pinned:{label:`Épinglée`,marker:`Carte d’implémentation — épinglée ci-dessus`,collapse:`Réduire la carte épinglée`,expand:`Déployer la carte épinglée`,expandCard:`Développer cette carte`,collapseCard:`Réduire cette carte`,collapsed_one:`Carte d’implémentation`,collapsed_other:`{{count}} cartes d’implémentation`,state:{building:`En cours`,on_review:`Revue`,pr_draft:`PR brouillon`,pr_ready:`Publiée`,pr_failed:`Échec`,merged:`Fusionnée`,discarded:`Abandonnée`}},mergeLocal:`Intégrer en local`,mergeLocalTooltip:`Fusionne les branches livrées dans {{base}} dans votre checkout local (sans GitHub)`,mergeLocalBlockedDirty:`Impossible d'intégrer : votre arbre de travail sur {{base}} contient des changements non commités. Commitez ou stashez-les, puis réessayez.`,mergeLocalBlockedBranch:`Impossible d'intégrer : votre checkout est sur {{current}}, pas sur {{base}}. Passez sur {{base}}, puis réessayez.`,mergedLocally:`Intégré dans {{base}} en local`},J={heading:`Brouillon de spec`,drafting:`Mise à jour du brouillon…`,acceptanceCriteria:`Critères d'acceptation`,untitled:`Spec sans titre`,expand:`Afficher la description complète`,collapse:`Réduire la description`},Y={openTicket:`Ouvrir la spec #{{id}}`,openJob:`Ouvrir le job {{id}}`,openLoop:`Ouvrir la boucle {{id}}`,ticketNotFound:`Spec #{{id}} introuvable — elle a peut-être été supprimée`,jobNotFound:`Job introuvable — il a peut-être été supprimé`,loopNotFound:`Boucle introuvable — elle a peut-être été supprimée`,lookupFailed:`Impossible de vérifier la référence — réessayez`},X={untitled:`Mission sans titre`,menu:`Menu de la mission`,rename:`Renommer`,renamePlaceholder:`Nom de la mission`,renameHint:`Double-clic pour renommer`,renameFailed:`Renommage impossible`,copyName:`Copier le nom de la mission`,copyId:`Copier l'ID de la mission`,copyProject:`Copier le nom du projet`,copyProjectPath:`Copier le chemin du projet`,copyFailed:`Copie impossible`,delete:`Supprimer la mission`,deleteConfirm:`Supprimer cette mission ? Action irréversible.`,deleteFailed:`Impossible de supprimer la mission`},Z={title:e,subtitle:t,trigger:n,composerPlaceholder:r,send:i,stop:a,copy:o,maximize:s,restore:c,minimize:l,close:u,newConversation:d,emptyTitle:f,emptyHint:p,thinking:m,connecting:h,consultingCode:g,size:_,model:v,history:y,queue:b,queueEdit:x,git:S,provider:C,tier:w,approval:T,project:E,mission:D,noProvider:O,degraded:k,toolCard:A,error:j,chip:M,newAgent:N,switchToAgent:P,switchToKanban:F,search:I,home:L,untitled:R,workspace:z,attachFile:B,deleteConversation:V,confirmDeleteConversation:H,confirmShort:U,imagesUnsupported:W,effort:G,emptyTitleMatrix:K,prCard:q,specDraft:J,refs:Y,header:X};export{T as approval,B as attachFile,M as chip,u as close,r as composerPlaceholder,H as confirmDeleteConversation,U as confirmShort,h as connecting,g as consultingCode,o as copy,Z as default,k as degraded,V as deleteConversation,G as effort,p as emptyHint,f as emptyTitle,K as emptyTitleMatrix,j as error,S as git,X as header,y as history,L as home,W as imagesUnsupported,s as maximize,l as minimize,D as mission,v as model,N as newAgent,d as newConversation,O as noProvider,q as prCard,E as project,C as provider,b as queue,x as queueEdit,Y as refs,c as restore,I as search,i as send,_ as size,J as specDraft,a as stop,t as subtitle,P as switchToAgent,F as switchToKanban,m as thinking,w as tier,e as title,A as toolCard,n as trigger,R as untitled,z as workspace};
|