team 0.1.2 → 0.2.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/README.md +22 -11
- package/dist/approve/approval.js +14 -9
- package/dist/approve/fingerprint.d.ts +14 -2
- package/dist/approve/fingerprint.js +21 -21
- package/dist/budgets/gate.js +7 -2
- package/dist/budgets/readings.d.ts +6 -2
- package/dist/budgets/readings.js +28 -2
- package/dist/budgets/run.js +8 -2
- package/dist/budgets/table.d.ts +2 -0
- package/dist/budgets/table.js +10 -5
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +8 -5
- package/dist/commands/add.d.ts +0 -1
- package/dist/commands/add.js +7 -5
- package/dist/commands/approve.js +17 -1
- package/dist/commands/doctor.d.ts +15 -1
- package/dist/commands/doctor.js +126 -8
- package/dist/commands/down.d.ts +4 -0
- package/dist/commands/down.js +32 -13
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/remove.js +5 -2
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +19 -1
- package/dist/commands/up.js +3 -1
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +28 -4
- package/dist/conformance/adapter.d.ts +5 -0
- package/dist/conformance/adapter.js +44 -0
- package/dist/file/check.d.ts +20 -0
- package/dist/file/check.js +112 -0
- package/dist/file/lines.js +13 -2
- package/dist/file/sections/budgets.d.ts +15 -0
- package/dist/file/sections/budgets.js +180 -0
- package/dist/file/sections/coordinator.d.ts +2 -0
- package/dist/file/sections/coordinator.js +11 -0
- package/dist/file/sections/format.d.ts +2 -0
- package/dist/file/sections/format.js +13 -0
- package/dist/file/sections/identity.d.ts +2 -0
- package/dist/file/sections/identity.js +103 -0
- package/dist/file/sections/index.d.ts +9 -0
- package/dist/file/sections/index.js +43 -0
- package/dist/file/sections/lead.d.ts +4 -0
- package/dist/file/sections/lead.js +18 -0
- package/dist/file/sections/limits.d.ts +2 -0
- package/dist/file/sections/limits.js +43 -0
- package/dist/file/sections/machine.d.ts +2 -0
- package/dist/file/sections/machine.js +38 -0
- package/dist/file/sections/operator.d.ts +2 -0
- package/dist/file/sections/operator.js +11 -0
- package/dist/file/sections/project.d.ts +2 -0
- package/dist/file/sections/project.js +13 -0
- package/dist/file/sections/rules.d.ts +2 -0
- package/dist/file/sections/rules.js +9 -0
- package/dist/file/sections/seats.d.ts +7 -0
- package/dist/file/sections/seats.js +161 -0
- package/dist/file/sections/section.d.ts +32 -0
- package/dist/file/sections/section.js +7 -0
- package/dist/file/sections/session.d.ts +2 -0
- package/dist/file/sections/session.js +16 -0
- package/dist/file/sections/tools.d.ts +2 -0
- package/dist/file/sections/tools.js +43 -0
- package/dist/file/sections/trust.d.ts +2 -0
- package/dist/file/sections/trust.js +20 -0
- package/dist/file/sections/units.d.ts +16 -0
- package/dist/file/sections/units.js +8 -0
- package/dist/file/sections/visibility.d.ts +2 -0
- package/dist/file/sections/visibility.js +12 -0
- package/dist/file/sections/watch-checks.d.ts +9 -0
- package/dist/file/sections/watch-checks.js +14 -0
- package/dist/file/sections/watch.d.ts +14 -0
- package/dist/file/sections/watch.js +111 -0
- package/dist/file/sections/workspace.d.ts +4 -0
- package/dist/file/sections/workspace.js +77 -0
- package/dist/file/types.d.ts +1 -0
- package/dist/file/validate.d.ts +4 -12
- package/dist/file/validate.js +45 -688
- package/dist/herdr.d.ts +1 -0
- package/dist/herdr.js +11 -1
- package/dist/launch/agent.js +4 -3
- package/dist/launch/deliver.d.ts +4 -0
- package/dist/launch/deliver.js +151 -8
- package/dist/launch/execute.d.ts +2 -0
- package/dist/launch/execute.js +21 -11
- package/dist/launch/plan.js +16 -2
- package/dist/profiles/antigravity.yaml +12 -2
- package/dist/profiles/claude-code.yaml +21 -6
- package/dist/profiles/codex.yaml +14 -1
- package/dist/profiles/cursor.yaml +45 -5
- package/dist/profiles/overrides.d.ts +67 -0
- package/dist/profiles/overrides.js +190 -0
- package/dist/profiles/profile.d.ts +3 -0
- package/dist/profiles/profile.js +5 -1
- package/dist/state.d.ts +2 -0
- package/dist/status/compare.js +7 -4
- package/dist/store/store.d.ts +5 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/dist/watch/checks/budget.js +49 -53
- package/dist/watch/dialect.d.ts +6 -1
- package/dist/watch/dialect.js +68 -12
- package/dist/watch/pass.d.ts +8 -1
- package/dist/watch/pass.js +19 -9
- package/dist/watch/screen-core.d.ts +41 -4
- package/dist/watch/screen-core.js +458 -93
- package/dist/watch/screen-data.d.ts +25 -0
- package/dist/watch/screen-file.d.ts +8 -2
- package/dist/watch/screen-file.js +325 -38
- package/dist/watch/screen-profile.d.ts +19 -0
- package/dist/watch/screen-profile.js +1 -0
- package/dist/watch/screen.d.ts +22 -4
- package/dist/watch/screen.js +39 -5
- package/package.json +9 -3
- package/schema/team.schema.json +498 -0
|
@@ -43,75 +43,375 @@ export function statusRowOf(data, lines) {
|
|
|
43
43
|
const at = statusIndex(composer, lines, composer.mode === 'status-then-one', () => false);
|
|
44
44
|
return at === 'stop' || at < 0 ? null : (lines[at] ?? null);
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The fold-shaped frame a window shows, or null: the marker row and the count it names (any
|
|
48
|
+
* integer, zero included), the tail rows below it, the box's two rules around both, and a footer
|
|
49
|
+
* under the bottom one. The top rule's width is the width the text wrapped at. The shape alone;
|
|
50
|
+
* whether the fold holds the typed text is the caller's to verify.
|
|
51
|
+
*/
|
|
52
|
+
function foldFrame(data, lines) {
|
|
53
|
+
const composer = data.composer;
|
|
54
|
+
if (composer.mode !== 'two-rules-footer-below' || !composer.fold)
|
|
55
|
+
return null;
|
|
56
|
+
let marker = -1;
|
|
57
|
+
let found = null;
|
|
58
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
59
|
+
const hit = composer.fold.exec(lines[i] ?? '');
|
|
60
|
+
if (hit) {
|
|
61
|
+
marker = i;
|
|
62
|
+
found = hit;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (marker < 0 || !found)
|
|
67
|
+
return null;
|
|
68
|
+
// The frame, as the capture draws it (folded-rules.txt): the box's opening rule at the pane's
|
|
69
|
+
// first column — the profile's rule pattern is anchored to it — sits directly above the
|
|
70
|
+
// marker, and the box's closing rule is the window's last rule row, drawn at the opening
|
|
71
|
+
// rule's own width. A rule-looking row between the marker and the closing rule is a tail row
|
|
72
|
+
// then, content that cannot match the typed text's own rows; a rule row where the capture
|
|
73
|
+
// draws none — directly above the opening rule, or at a width the opening rule does not have
|
|
74
|
+
// — leaves the read unable to tell the frame from content, and it fails closed.
|
|
75
|
+
let top = -1;
|
|
76
|
+
for (let i = marker - 1; i >= 0; i--) {
|
|
77
|
+
if (composer.rule.test(lines[i] ?? '')) {
|
|
78
|
+
top = i;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (top < 0)
|
|
83
|
+
return null;
|
|
84
|
+
if (top > 0 && composer.rule.test(lines[top - 1] ?? ''))
|
|
85
|
+
return null;
|
|
86
|
+
for (let i = top + 1; i < marker; i++) {
|
|
87
|
+
if ((lines[i] ?? '').trim())
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
let bottom = -1;
|
|
91
|
+
for (let i = lines.length - 1; i > marker; i--) {
|
|
92
|
+
if (composer.rule.test(lines[i] ?? '')) {
|
|
93
|
+
bottom = i;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (bottom < 0)
|
|
98
|
+
return null;
|
|
99
|
+
const width = (lines[top] ?? '').trim().length;
|
|
100
|
+
if ((lines[bottom] ?? '').trim().length !== width)
|
|
101
|
+
return null;
|
|
102
|
+
const rows = lines.slice(marker + 1, bottom).filter((line) => line.trim());
|
|
103
|
+
if (rows.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
if (!lines.slice(bottom + 1).some((line) => composer.footers.some((pattern) => pattern.test(line))))
|
|
106
|
+
return null;
|
|
107
|
+
const count = Number(found[1]);
|
|
108
|
+
if (!Number.isInteger(count))
|
|
109
|
+
return null;
|
|
110
|
+
return { count, rows, width };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The fold a `two-rules-footer-below` composer's window shows, or null. A marker whose count is
|
|
114
|
+
* zero names no hidden rows to compare against, so it is no fold; it is still a marker, and
|
|
115
|
+
* `foldMarked` catches it.
|
|
116
|
+
*/
|
|
117
|
+
export function foldOf(data, lines) {
|
|
118
|
+
const frame = foldFrame(data, lines);
|
|
119
|
+
if (frame === null || frame.count < 1)
|
|
120
|
+
return null;
|
|
121
|
+
return frame;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Whether the window shows a fold marker at all, whatever count it names: a marker claiming zero
|
|
125
|
+
* hidden rows (or a count a fold could not have) is still a box that is not showing the whole
|
|
126
|
+
* text, and it must never be taken for an ordinary unsent box and submitted unverified.
|
|
127
|
+
*/
|
|
128
|
+
export function foldMarked(data, lines) {
|
|
129
|
+
return foldFrame(data, lines) !== null;
|
|
130
|
+
}
|
|
46
131
|
/** `lines` is already the window: the last 20 lines, each keeping any ANSI styling. Matching
|
|
47
132
|
* runs on each line's plain form, trimmed at the end; the styled form is kept for the one
|
|
48
133
|
* question styling can answer, whether the composer's input line is all dim. */
|
|
49
134
|
function plainLines(lines) {
|
|
50
135
|
return lines.map((line) => stripSgr(line).trimEnd());
|
|
51
136
|
}
|
|
137
|
+
const SCREEN_KINDS = new Set([
|
|
138
|
+
'idle',
|
|
139
|
+
'working',
|
|
140
|
+
'unsent',
|
|
141
|
+
'permission',
|
|
142
|
+
'trust',
|
|
143
|
+
'question',
|
|
144
|
+
'unknown',
|
|
145
|
+
]);
|
|
146
|
+
function getProfileFn(profile, key) {
|
|
147
|
+
if (!profile || (typeof profile !== 'object' && typeof profile !== 'function'))
|
|
148
|
+
return undefined;
|
|
149
|
+
try {
|
|
150
|
+
const fn = profile[key];
|
|
151
|
+
if (fn === undefined || fn === null)
|
|
152
|
+
return undefined;
|
|
153
|
+
if (typeof fn !== 'function')
|
|
154
|
+
return 'fail';
|
|
155
|
+
return fn;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return 'fail';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function evalPredicate(predicate, lines) {
|
|
162
|
+
if (predicate === 'fail')
|
|
163
|
+
return 'fail';
|
|
164
|
+
if (typeof predicate !== 'function')
|
|
165
|
+
return 'fail';
|
|
166
|
+
try {
|
|
167
|
+
const res = predicate(lines);
|
|
168
|
+
if (res && (typeof res === 'object' || typeof res === 'function') && typeof res.catch === 'function') {
|
|
169
|
+
try {
|
|
170
|
+
res.catch(() => { });
|
|
171
|
+
}
|
|
172
|
+
catch { }
|
|
173
|
+
return 'fail';
|
|
174
|
+
}
|
|
175
|
+
if (res === true)
|
|
176
|
+
return 'match';
|
|
177
|
+
if (res === false)
|
|
178
|
+
return 'miss';
|
|
179
|
+
return 'fail';
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return 'fail';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function callComposer(composer, lines) {
|
|
186
|
+
if (composer === 'fail')
|
|
187
|
+
return { kind: 'unknown' };
|
|
188
|
+
if (typeof composer !== 'function')
|
|
189
|
+
return { kind: 'unknown' };
|
|
190
|
+
try {
|
|
191
|
+
const reading = composer(lines);
|
|
192
|
+
if (reading && (typeof reading === 'object' || typeof reading === 'function') && typeof reading.catch === 'function') {
|
|
193
|
+
try {
|
|
194
|
+
reading.catch(() => { });
|
|
195
|
+
}
|
|
196
|
+
catch { }
|
|
197
|
+
return { kind: 'unknown' };
|
|
198
|
+
}
|
|
199
|
+
if (!reading || typeof reading !== 'object')
|
|
200
|
+
return { kind: 'unknown' };
|
|
201
|
+
let kind;
|
|
202
|
+
try {
|
|
203
|
+
kind = reading.kind;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return { kind: 'unknown' };
|
|
207
|
+
}
|
|
208
|
+
if (typeof kind !== 'string' || !SCREEN_KINDS.has(kind)) {
|
|
209
|
+
return { kind: 'unknown' };
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
kind: kind,
|
|
213
|
+
from: 0,
|
|
214
|
+
input: -1,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return { kind: 'unknown' };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
52
221
|
/** `lines` is already the window: the last 20 lines, each trimmed at the end. */
|
|
53
222
|
export function classifyLines(data, lines, clock) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
[
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
223
|
+
try {
|
|
224
|
+
const now = clock?.now ?? Date.now;
|
|
225
|
+
const budget = clock?.budgetMs ?? BUDGET_MS;
|
|
226
|
+
const start = now();
|
|
227
|
+
const tick = () => now() - start > budget;
|
|
228
|
+
const plain = plainLines(lines);
|
|
229
|
+
// Rule (a): Every hatch predicate is monotone toward caution: hatch OR data, for working,
|
|
230
|
+
// every dialog (trust, permission, question) and unknown. The data stage of a profile always runs;
|
|
231
|
+
// a hatch predicate can only add a match. A hatch can only add caution, never remove it.
|
|
232
|
+
const cautionStages = [
|
|
233
|
+
['unknown', data.unknown],
|
|
234
|
+
['trust', data.trust],
|
|
235
|
+
['permission', data.permission],
|
|
236
|
+
['question', data.question],
|
|
237
|
+
['working', data.working],
|
|
238
|
+
];
|
|
239
|
+
for (const [kind, stage] of cautionStages) {
|
|
240
|
+
if (tick())
|
|
72
241
|
return { kind: 'unknown' };
|
|
73
|
-
|
|
74
|
-
|
|
242
|
+
const fn = getProfileFn(data.profile, kind);
|
|
243
|
+
if (fn !== undefined) {
|
|
244
|
+
const outcome = evalPredicate(fn, plain);
|
|
245
|
+
if (outcome === 'match')
|
|
246
|
+
return { kind };
|
|
247
|
+
if (outcome === 'fail')
|
|
248
|
+
return { kind: 'unknown' };
|
|
249
|
+
}
|
|
250
|
+
if (stage) {
|
|
251
|
+
for (const rule of stage.rules) {
|
|
252
|
+
const hit = ruleMatches(data, plain, rule, tick);
|
|
253
|
+
if (hit === 'stop')
|
|
254
|
+
return { kind: 'unknown' };
|
|
255
|
+
if (hit)
|
|
256
|
+
return { kind };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
75
259
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
260
|
+
// Composer stage: comes from hatch OR from data (refused if both at load).
|
|
261
|
+
const composerFn = getProfileFn(data.profile, 'composer');
|
|
262
|
+
if (composerFn !== undefined) {
|
|
263
|
+
const composed = callComposer(composerFn, plain);
|
|
264
|
+
if (composed.kind === 'stop' || composed.kind === 'unknown')
|
|
265
|
+
return { kind: 'unknown' };
|
|
266
|
+
const marked = floorHits(plain, 0, -1, data.chrome, tick);
|
|
267
|
+
if (marked === 'stop' || marked)
|
|
268
|
+
return { kind: 'unknown' };
|
|
269
|
+
return { kind: composed.kind };
|
|
270
|
+
}
|
|
271
|
+
const composed = compose(data, plain, lines, tick);
|
|
272
|
+
if (composed.kind === 'stop' || composed.kind === 'unknown')
|
|
273
|
+
return { kind: 'unknown' };
|
|
274
|
+
if (composed.kind !== 'idle' && composed.kind !== 'unsent')
|
|
275
|
+
return { kind: composed.kind };
|
|
276
|
+
const marked = floorHits(plain, composed.from, composed.input, data.chrome, tick);
|
|
277
|
+
if (marked === 'stop' || marked)
|
|
278
|
+
return { kind: 'unknown' };
|
|
81
279
|
return { kind: composed.kind };
|
|
82
|
-
|
|
83
|
-
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
84
282
|
return { kind: 'unknown' };
|
|
85
|
-
|
|
283
|
+
}
|
|
86
284
|
}
|
|
87
285
|
/**
|
|
88
|
-
* The composer alone, for a turn that is still running. The
|
|
89
|
-
*
|
|
286
|
+
* The composer alone, for a turn that is still running. The dialog stages and
|
|
287
|
+
* floor must not be overridden by composer readings returning idle or unsent.
|
|
90
288
|
*/
|
|
91
289
|
export function composeLines(data, lines, clock) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
290
|
+
try {
|
|
291
|
+
const now = clock?.now ?? Date.now;
|
|
292
|
+
const budget = clock?.budgetMs ?? BUDGET_MS;
|
|
293
|
+
const start = now();
|
|
294
|
+
const tick = () => now() - start > budget;
|
|
295
|
+
const plain = plainLines(lines);
|
|
296
|
+
const composerFn = getProfileFn(data.profile, 'composer');
|
|
297
|
+
if (composerFn !== undefined) {
|
|
298
|
+
// Profile has a HATCH composer (no data composer).
|
|
299
|
+
// Check if any caution stage (data or hatch) matches:
|
|
300
|
+
const stages = [
|
|
301
|
+
['unknown', data.unknown],
|
|
302
|
+
['trust', data.trust],
|
|
303
|
+
['permission', data.permission],
|
|
304
|
+
['question', data.question],
|
|
305
|
+
['working', data.working],
|
|
306
|
+
];
|
|
307
|
+
for (const [kind, stage] of stages) {
|
|
308
|
+
if (tick())
|
|
309
|
+
return { kind: 'unknown' };
|
|
310
|
+
const fn = getProfileFn(data.profile, kind);
|
|
311
|
+
if (fn !== undefined) {
|
|
312
|
+
const outcome = evalPredicate(fn, plain);
|
|
313
|
+
if (outcome === 'match' || outcome === 'fail')
|
|
314
|
+
return { kind: 'unknown' };
|
|
315
|
+
}
|
|
316
|
+
if (stage) {
|
|
317
|
+
for (const rule of stage.rules) {
|
|
318
|
+
const hit = ruleMatches(data, plain, rule, tick);
|
|
319
|
+
if (hit === 'stop')
|
|
320
|
+
return { kind: 'unknown' };
|
|
321
|
+
if (hit)
|
|
322
|
+
return { kind: 'unknown' };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const composed = callComposer(composerFn, plain);
|
|
327
|
+
if (composed.kind === 'stop' || composed.kind === 'unknown')
|
|
101
328
|
return { kind: 'unknown' };
|
|
102
|
-
|
|
329
|
+
const marked = floorHits(plain, 0, -1, data.chrome, tick);
|
|
330
|
+
if (marked === 'stop' || marked)
|
|
103
331
|
return { kind: 'unknown' };
|
|
332
|
+
return { kind: composed.kind };
|
|
104
333
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
334
|
+
// Profile has a DATA composer.
|
|
335
|
+
// Check hatch dialog predicates:
|
|
336
|
+
const dialogKinds = ['unknown', 'trust', 'permission', 'question'];
|
|
337
|
+
for (const kind of dialogKinds) {
|
|
338
|
+
if (tick())
|
|
339
|
+
return { kind: 'unknown' };
|
|
340
|
+
const fn = getProfileFn(data.profile, kind);
|
|
341
|
+
if (fn !== undefined) {
|
|
342
|
+
const outcome = evalPredicate(fn, plain);
|
|
343
|
+
if (outcome === 'match' || outcome === 'fail')
|
|
344
|
+
return { kind: 'unknown' };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Also if hatch working returns 'fail', fail safe:
|
|
348
|
+
const workingFn = getProfileFn(data.profile, 'working');
|
|
349
|
+
if (workingFn !== undefined) {
|
|
350
|
+
const outcome = evalPredicate(workingFn, plain);
|
|
351
|
+
if (outcome === 'fail')
|
|
352
|
+
return { kind: 'unknown' };
|
|
353
|
+
}
|
|
354
|
+
if (data.unknown) {
|
|
355
|
+
for (const rule of data.unknown.rules) {
|
|
356
|
+
const hit = ruleMatches(data, plain, rule, tick);
|
|
357
|
+
if (hit === 'stop')
|
|
358
|
+
return { kind: 'unknown' };
|
|
359
|
+
if (hit)
|
|
360
|
+
return { kind: 'unknown' };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const composed = compose(data, plain, lines, tick);
|
|
364
|
+
if (composed.kind === 'stop' || composed.kind === 'unknown')
|
|
365
|
+
return { kind: 'unknown' };
|
|
366
|
+
if (composed.kind !== 'idle' && composed.kind !== 'unsent')
|
|
367
|
+
return { kind: composed.kind };
|
|
368
|
+
const marked = floorHits(plain, composed.from, composed.input, data.chrome, tick);
|
|
369
|
+
if (marked === 'stop' || marked)
|
|
370
|
+
return { kind: 'unknown' };
|
|
110
371
|
return { kind: composed.kind };
|
|
111
|
-
|
|
112
|
-
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
113
374
|
return { kind: 'unknown' };
|
|
114
|
-
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* The input box a composer draws for its current text, or null when the composer does not read
|
|
379
|
+
* `unsent`: an idle box holds nothing, and every other shape is not the box. The rows are the
|
|
380
|
+
* ones the mode's own frame delimits — between the prompt row and the box's closing line; the
|
|
381
|
+
* `status-then-one` fallback (the captured completion frame) delimits nothing, so its box is
|
|
382
|
+
* its one input row, the popup rows below it being its filter's to exclude. The caller compares
|
|
383
|
+
* the box to the text it typed; nothing here decides that.
|
|
384
|
+
*/
|
|
385
|
+
export function composerBox(data, lines) {
|
|
386
|
+
if (!data.composer || data.profile?.composer)
|
|
387
|
+
return null;
|
|
388
|
+
const plain = plainLines(lines);
|
|
389
|
+
const hit = compose(data, plain, lines, () => false);
|
|
390
|
+
if (hit.kind === 'stop' || hit.kind === 'unknown')
|
|
391
|
+
return null;
|
|
392
|
+
if (hit.kind !== 'unsent')
|
|
393
|
+
return null;
|
|
394
|
+
const input = plain[hit.input] ?? '';
|
|
395
|
+
const found = data.composer.prompt.exec(input);
|
|
396
|
+
if (!found)
|
|
397
|
+
return null;
|
|
398
|
+
let rest = input.slice(found[0].length);
|
|
399
|
+
const suffix = 'stripSuffix' in data.composer ? data.composer.stripSuffix : undefined;
|
|
400
|
+
if (suffix)
|
|
401
|
+
rest = rest.replace(suffix, '');
|
|
402
|
+
const indent = found[0].length + rest.length - rest.trimStart().length;
|
|
403
|
+
const rows = hit.rows ?? [];
|
|
404
|
+
// The pane draws empty rows of its own under the text, inside the box's frame — the drop
|
|
405
|
+
// before the status line, or before the closing rule. The captures name them per profile
|
|
406
|
+
// (`frame_rows`), and only those are stripped: they are the box's frame, not its content.
|
|
407
|
+
// Every row after them stays, so a box that shows an empty row the typed text does not have
|
|
408
|
+
// is not the typed text, and the caller refuses it.
|
|
409
|
+
let frame = data.composer.frameRows;
|
|
410
|
+
while (frame > 0 && rows.length > 0 && rows[rows.length - 1] === '') {
|
|
411
|
+
rows.pop();
|
|
412
|
+
frame -= 1;
|
|
413
|
+
}
|
|
414
|
+
return { first: rest.trimStart(), indent, rows, wrap: 'wrap' in data.composer ? data.composer.wrap : undefined };
|
|
115
415
|
}
|
|
116
416
|
function ruleMatches(data, lines, rule, tick) {
|
|
117
417
|
const where = rule.onFooter ? [footerLine(data, lines)] : lines;
|
|
@@ -233,59 +533,110 @@ function compose(data, plain, styled, tick) {
|
|
|
233
533
|
return statusThenOne(plain, styled, composer, tick);
|
|
234
534
|
return twoRules(plain, styled, composer, tick);
|
|
235
535
|
}
|
|
536
|
+
/** The rule a line carries, or null: the profile's rule pattern anchored to the line's own
|
|
537
|
+
* first column, as the captures draw the box's rules. */
|
|
538
|
+
function ruleRun(composer, line) {
|
|
539
|
+
const hit = composer.rule.exec(line);
|
|
540
|
+
return hit === null ? null : hit[0];
|
|
541
|
+
}
|
|
236
542
|
function boxToRule(lines, styled, composer, tick) {
|
|
237
|
-
|
|
543
|
+
// The frame first, as the captures draw it (unsent-typed-ansi.txt): a rule is an unbroken run
|
|
544
|
+
// from the pane's first column — the profile's rule pattern is anchored to it — and the box's
|
|
545
|
+
// closing rule is the window's last rule row; nothing rule-shaped sits under the box's
|
|
546
|
+
// footers, and a rule-looking row the box holds is drawn at the content column: content,
|
|
547
|
+
// never the frame.
|
|
548
|
+
let close = -1;
|
|
238
549
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
239
550
|
if (tick())
|
|
240
551
|
return { kind: 'stop' };
|
|
241
|
-
if (composer
|
|
242
|
-
|
|
552
|
+
if (ruleRun(composer, lines[i] ?? '') !== null) {
|
|
553
|
+
close = i;
|
|
243
554
|
break;
|
|
244
555
|
}
|
|
245
556
|
}
|
|
246
|
-
if (
|
|
557
|
+
if (close < 0)
|
|
247
558
|
return { kind: 'unknown' };
|
|
248
|
-
// The
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
for (let i =
|
|
559
|
+
// The capture draws only the box's own footers under the closing rule. A prompt-looking row
|
|
560
|
+
// there is a row the frame does not draw — leftover-box.txt shows a second, leftover prompt
|
|
561
|
+
// under the box's status — so this pane is not the one box the caller can compare against
|
|
562
|
+
// typed text, and the read fails closed.
|
|
563
|
+
for (let i = close + 1; i < lines.length; i++) {
|
|
253
564
|
if (tick())
|
|
254
565
|
return { kind: 'stop' };
|
|
255
|
-
if (composer.
|
|
256
|
-
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
566
|
+
if (composer.prompt.test(lines[i] ?? ''))
|
|
567
|
+
return { kind: 'unknown' };
|
|
259
568
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const above = input > 0 && composer.rule.test(lines[input - 1] ?? '');
|
|
263
|
-
let footer = false;
|
|
264
|
-
for (let j = close + 1; j < lines.length; j++) {
|
|
569
|
+
let open = -1;
|
|
570
|
+
for (let i = close - 1; i >= 0; i--) {
|
|
265
571
|
if (tick())
|
|
266
572
|
return { kind: 'stop' };
|
|
267
|
-
if (
|
|
268
|
-
|
|
573
|
+
if (ruleRun(composer, lines[i] ?? '') !== null) {
|
|
574
|
+
open = i;
|
|
269
575
|
break;
|
|
270
576
|
}
|
|
271
577
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
578
|
+
let input;
|
|
579
|
+
let from;
|
|
580
|
+
if (open >= 0) {
|
|
581
|
+
// The two rules of one box are drawn at one width (53 columns in unsent-typed-ansi.txt).
|
|
582
|
+
if (ruleRun(composer, lines[close] ?? '') !== ruleRun(composer, lines[open] ?? ''))
|
|
583
|
+
return { kind: 'unknown' };
|
|
584
|
+
// The input row is the box's first row under its opening frame, and it carries the prompt
|
|
585
|
+
// at the capture's own column. A row there that does not is not this box's input — the read
|
|
586
|
+
// fails closed rather than take some later row whose content begins with a prompt glyph,
|
|
587
|
+
// which would drop the rows above it from the box the caller compares.
|
|
588
|
+
input = open + 1;
|
|
589
|
+
if (input >= close || !composer.prompt.test(lines[input] ?? ''))
|
|
590
|
+
return { kind: 'unknown' };
|
|
591
|
+
from = open;
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
// The opening rule has scrolled out of the window. The frame is then the ordered footers
|
|
595
|
+
// under the closing rule — every pattern matched in sequence, equal count, as the capture
|
|
596
|
+
// draws them — and the input row is the lowest row above the closing rule carrying the
|
|
597
|
+
// prompt at that column: content is drawn at the text's column, so a row at the prompt's
|
|
598
|
+
// own can only be the box's first. Any non-blank row above it is a row the frame does not
|
|
599
|
+
// explain, and the read fails closed.
|
|
600
|
+
let footer = false;
|
|
601
|
+
const rawFooters = [];
|
|
602
|
+
for (let j = close + 1; j < lines.length; j++) {
|
|
603
|
+
if (tick())
|
|
604
|
+
return { kind: 'stop' };
|
|
605
|
+
const raw = lines[j] ?? '';
|
|
606
|
+
if (raw.trim())
|
|
607
|
+
rawFooters.push(raw);
|
|
608
|
+
}
|
|
609
|
+
if (composer.footers.length > 0 && rawFooters.length === composer.footers.length) {
|
|
610
|
+
footer = composer.footers.every((pattern, idx) => pattern.test(rawFooters[idx] ?? ''));
|
|
611
|
+
}
|
|
612
|
+
if (!footer)
|
|
613
|
+
return { kind: 'unknown' };
|
|
614
|
+
input = -1;
|
|
615
|
+
for (let i = close - 1; i >= 0; i--) {
|
|
616
|
+
if (tick())
|
|
617
|
+
return { kind: 'stop' };
|
|
618
|
+
if (composer.prompt.test(lines[i] ?? '')) {
|
|
619
|
+
input = i;
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (input < 0)
|
|
624
|
+
return { kind: 'unknown' };
|
|
625
|
+
for (let i = 0; i < input; i++) {
|
|
626
|
+
if (tick())
|
|
627
|
+
return { kind: 'stop' };
|
|
628
|
+
if ((lines[i] ?? '').trim())
|
|
629
|
+
return { kind: 'unknown' };
|
|
630
|
+
}
|
|
631
|
+
from = input;
|
|
632
|
+
}
|
|
633
|
+
const rows = lines.slice(input + 1, close);
|
|
277
634
|
for (let i = input + 1; i < close; i++) {
|
|
278
635
|
if ((lines[i] ?? '').trim())
|
|
279
|
-
return { kind: 'unsent', from, input };
|
|
636
|
+
return { kind: 'unsent', from, input, rows };
|
|
280
637
|
}
|
|
281
638
|
const typed = (lines[input] ?? '').replace(composer.prompt, '').trim();
|
|
282
|
-
return { kind: placeholder(typed, composer, lines[input] ?? '', styled[input]) ? 'idle' : 'unsent', from, input };
|
|
283
|
-
}
|
|
284
|
-
// The composer footer, not a line of shell output under a stray rule. The model
|
|
285
|
-
// row uses a middot; the permissions line is the other footer.
|
|
286
|
-
function statusFooter(line) {
|
|
287
|
-
const text = line.trim();
|
|
288
|
-
return text.includes('·') || /bypass permissions/i.test(text);
|
|
639
|
+
return { kind: placeholder(typed, composer, lines[input] ?? '', styled[input]) ? 'idle' : 'unsent', from, input, rows };
|
|
289
640
|
}
|
|
290
641
|
function statusLast(lines, styled, composer, tick, allowOneTrailing) {
|
|
291
642
|
const status = statusIndex(composer, lines, allowOneTrailing, tick);
|
|
@@ -298,10 +649,11 @@ function statusLast(lines, styled, composer, tick, allowOneTrailing) {
|
|
|
298
649
|
input--;
|
|
299
650
|
if (input < 0)
|
|
300
651
|
return { kind: 'unknown' };
|
|
652
|
+
const rows = lines.slice(input + 1, status);
|
|
301
653
|
for (let i = input + 1; i < status; i++)
|
|
302
654
|
if ((lines[i] ?? '').trim())
|
|
303
|
-
return { kind: 'unsent', from: input, input };
|
|
304
|
-
return { kind: placeholder(stripTyped(lines[input] ?? '', composer), composer, lines[input] ?? '', styled[input]) ? 'idle' : 'unsent', from: input, input };
|
|
655
|
+
return { kind: 'unsent', from: input, input, rows };
|
|
656
|
+
return { kind: placeholder(stripTyped(lines[input] ?? '', composer), composer, lines[input] ?? '', styled[input]) ? 'idle' : 'unsent', from: input, input, rows };
|
|
305
657
|
}
|
|
306
658
|
function statusThenOne(lines, styled, composer, tick) {
|
|
307
659
|
const found = statusLast(lines, styled, composer, tick, true);
|
|
@@ -332,25 +684,37 @@ function statusThenOne(lines, styled, composer, tick) {
|
|
|
332
684
|
return { kind: 'unknown' };
|
|
333
685
|
}
|
|
334
686
|
function twoRules(lines, styled, composer, tick) {
|
|
687
|
+
// The frame first: the box's bottom rule is the window's last rule row, and its top rule is
|
|
688
|
+
// the nearest rule row above it — the captures draw exactly two rules around the box, both
|
|
689
|
+
// unbroken runs from the pane's first column (unsent.txt draws them at rows 7 and 12). A
|
|
690
|
+
// rule-looking row the box holds is drawn at the content column and is content, never frame.
|
|
335
691
|
let bottom = -1;
|
|
336
692
|
let top = -1;
|
|
337
|
-
let input = -1;
|
|
338
693
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
339
694
|
if (tick())
|
|
340
695
|
return { kind: 'stop' };
|
|
341
|
-
|
|
342
|
-
if (composer.rule.test(line)) {
|
|
696
|
+
if (composer.rule.test(lines[i] ?? '')) {
|
|
343
697
|
if (bottom === -1)
|
|
344
698
|
bottom = i;
|
|
345
|
-
else
|
|
699
|
+
else {
|
|
346
700
|
top = i;
|
|
347
701
|
break;
|
|
348
702
|
}
|
|
349
703
|
}
|
|
350
|
-
else if (bottom !== -1 && input === -1 && composer.prompt.test(line))
|
|
351
|
-
input = i;
|
|
352
704
|
}
|
|
353
|
-
if (top < 0 || bottom < 0
|
|
705
|
+
if (top < 0 || bottom < 0)
|
|
706
|
+
return { kind: 'unknown' };
|
|
707
|
+
// The two rules of one box are drawn at one width (53 columns in unsent.txt, 54 in
|
|
708
|
+
// folded-rules.txt). A window whose rules differ is not that frame — the box the caller
|
|
709
|
+
// compares against typed text cannot be established — and the read fails closed.
|
|
710
|
+
if (ruleRun(composer, lines[bottom] ?? '') !== ruleRun(composer, lines[top] ?? ''))
|
|
711
|
+
return { kind: 'unknown' };
|
|
712
|
+
// The input row is the box's first row under its opening frame, and it carries the prompt at
|
|
713
|
+
// the capture's own column. A row there that does not — a folded box's marker row
|
|
714
|
+
// (folded-rules.txt) — is not this box's input, and the read fails closed rather than take
|
|
715
|
+
// some later row whose content begins with a prompt glyph.
|
|
716
|
+
const input = top + 1;
|
|
717
|
+
if (input >= bottom || !composer.prompt.test(lines[input] ?? ''))
|
|
354
718
|
return { kind: 'unknown' };
|
|
355
719
|
let footer = false;
|
|
356
720
|
for (let i = bottom + 1; i < lines.length; i++) {
|
|
@@ -361,10 +725,11 @@ function twoRules(lines, styled, composer, tick) {
|
|
|
361
725
|
}
|
|
362
726
|
if (!footer)
|
|
363
727
|
return { kind: 'unknown' };
|
|
364
|
-
|
|
365
|
-
|
|
728
|
+
const rows = lines.slice(input + 1, bottom);
|
|
729
|
+
if (rows.some((line) => line.trim()))
|
|
730
|
+
return { kind: 'unsent', from: top, input, rows };
|
|
366
731
|
const typed = (lines[input] ?? '').replace(composer.prompt, '').trim();
|
|
367
|
-
return { kind: placeholder(typed, composer, lines[input] ?? '', styled[input]) ? 'idle' : 'unsent', from: top, input };
|
|
732
|
+
return { kind: placeholder(typed, composer, lines[input] ?? '', styled[input]) ? 'idle' : 'unsent', from: top, input, rows };
|
|
368
733
|
}
|
|
369
734
|
function stripTyped(line, composer) {
|
|
370
735
|
const stripped = composer.stripSuffix ? line.replace(composer.prompt, '').replace(composer.stripSuffix, '') : line.replace(composer.prompt, '');
|