team 0.1.1 → 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 +38 -12
- package/dist/ansi.d.ts +13 -0
- package/dist/ansi.js +75 -0
- package/dist/approve/approval.d.ts +11 -0
- package/dist/approve/approval.js +80 -8
- package/dist/approve/fingerprint.d.ts +29 -6
- package/dist/approve/fingerprint.js +57 -32
- package/dist/budgets/gate.d.ts +5 -4
- package/dist/budgets/gate.js +23 -14
- package/dist/budgets/readings.d.ts +57 -9
- package/dist/budgets/readings.js +134 -18
- package/dist/budgets/run.js +8 -2
- package/dist/budgets/table.d.ts +4 -2
- package/dist/budgets/table.js +29 -11
- 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 +18 -9
- package/dist/commands/approve.js +17 -1
- package/dist/commands/doctor.d.ts +17 -1
- package/dist/commands/doctor.js +128 -8
- package/dist/commands/down.d.ts +6 -2
- package/dist/commands/down.js +40 -14
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +26 -4
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +22 -3
- package/dist/commands/up.d.ts +3 -1
- package/dist/commands/up.js +12 -6
- package/dist/commands/watch.d.ts +4 -0
- package/dist/commands/watch.js +99 -19
- 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 +6 -0
- package/dist/file/validate.d.ts +4 -12
- package/dist/file/validate.js +45 -668
- package/dist/herdr.d.ts +5 -0
- package/dist/herdr.js +51 -12
- package/dist/launch/agent.d.ts +1 -0
- package/dist/launch/agent.js +13 -0
- package/dist/launch/deliver.d.ts +7 -1
- package/dist/launch/deliver.js +159 -7
- package/dist/launch/execute.d.ts +4 -2
- package/dist/launch/execute.js +36 -13
- package/dist/launch/plan.js +16 -2
- package/dist/launch/rules.d.ts +4 -2
- package/dist/launch/rules.js +17 -3
- package/dist/profiles/antigravity.yaml +25 -2
- package/dist/profiles/claude-code.yaml +51 -6
- package/dist/profiles/codex.yaml +14 -1
- package/dist/profiles/cursor.yaml +46 -3
- 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 +9 -4
- package/dist/state.js +37 -0
- package/dist/status/compare.js +7 -4
- package/dist/status/statusline.js +3 -1
- package/dist/store/store.d.ts +5 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/dist/watch/check.d.ts +1 -0
- package/dist/watch/checks/budget.js +50 -51
- package/dist/watch/dialect.d.ts +6 -1
- package/dist/watch/dialect.js +68 -12
- package/dist/watch/pass.d.ts +10 -2
- package/dist/watch/pass.js +70 -29
- package/dist/watch/screen-core.d.ts +41 -4
- package/dist/watch/screen-core.js +510 -89
- package/dist/watch/screen-data.d.ts +34 -0
- package/dist/watch/screen-file.d.ts +8 -2
- package/dist/watch/screen-file.js +344 -39
- package/dist/watch/screen-profile.d.ts +19 -0
- package/dist/watch/screen-profile.js +1 -0
- package/dist/watch/screen.d.ts +25 -6
- package/dist/watch/screen.js +48 -9
- package/examples/team.yaml +8 -0
- package/package.json +9 -3
- package/schema/team.schema.json +498 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// The screen core. Stage order, dialog primitives, the four composer modes and the
|
|
2
|
+
// safety floor live here, never in a profile. A profile is data: it can add a
|
|
3
|
+
// pattern, and it cannot move a stage or turn the floor off.
|
|
4
|
+
import { allDimAfter, hasSgr, stripSgr } from "../ansi.js";
|
|
1
5
|
const BUDGET_MS = 100;
|
|
2
6
|
// The rule a `below_last_rule` pattern has to sit under. It is the core's, not the profile's.
|
|
3
7
|
const RULE_LINE = /^\s*[─━]{8,}\s*$/;
|
|
@@ -39,71 +43,380 @@ export function statusRowOf(data, lines) {
|
|
|
39
43
|
const at = statusIndex(composer, lines, composer.mode === 'status-then-one', () => false);
|
|
40
44
|
return at === 'stop' || at < 0 ? null : (lines[at] ?? null);
|
|
41
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
|
+
}
|
|
131
|
+
/** `lines` is already the window: the last 20 lines, each keeping any ANSI styling. Matching
|
|
132
|
+
* runs on each line's plain form, trimmed at the end; the styled form is kept for the one
|
|
133
|
+
* question styling can answer, whether the composer's input line is all dim. */
|
|
134
|
+
function plainLines(lines) {
|
|
135
|
+
return lines.map((line) => stripSgr(line).trimEnd());
|
|
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
|
+
}
|
|
42
221
|
/** `lines` is already the window: the last 20 lines, each trimmed at the end. */
|
|
43
222
|
export function classifyLines(data, lines, clock) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
[
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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())
|
|
61
241
|
return { kind: 'unknown' };
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
}
|
|
64
259
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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' };
|
|
70
279
|
return { kind: composed.kind };
|
|
71
|
-
|
|
72
|
-
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
73
282
|
return { kind: 'unknown' };
|
|
74
|
-
|
|
283
|
+
}
|
|
75
284
|
}
|
|
76
285
|
/**
|
|
77
|
-
* The composer alone, for a turn that is still running. The
|
|
78
|
-
*
|
|
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.
|
|
79
288
|
*/
|
|
80
289
|
export function composeLines(data, lines, clock) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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')
|
|
89
328
|
return { kind: 'unknown' };
|
|
90
|
-
|
|
329
|
+
const marked = floorHits(plain, 0, -1, data.chrome, tick);
|
|
330
|
+
if (marked === 'stop' || marked)
|
|
91
331
|
return { kind: 'unknown' };
|
|
332
|
+
return { kind: composed.kind };
|
|
92
333
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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' };
|
|
98
371
|
return { kind: composed.kind };
|
|
99
|
-
|
|
100
|
-
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
101
374
|
return { kind: 'unknown' };
|
|
102
|
-
|
|
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 };
|
|
103
415
|
}
|
|
104
416
|
function ruleMatches(data, lines, rule, tick) {
|
|
417
|
+
const where = rule.onFooter ? [footerLine(data, lines)] : lines;
|
|
105
418
|
if (rule.any) {
|
|
106
|
-
const hit = anyLine(
|
|
419
|
+
const hit = anyLine(where, rule.any, tick);
|
|
107
420
|
if (hit === 'stop')
|
|
108
421
|
return 'stop';
|
|
109
422
|
if (!hit)
|
|
@@ -111,7 +424,7 @@ function ruleMatches(data, lines, rule, tick) {
|
|
|
111
424
|
}
|
|
112
425
|
if (rule.all) {
|
|
113
426
|
for (const pattern of rule.all) {
|
|
114
|
-
const hit = lineSomewhere(
|
|
427
|
+
const hit = lineSomewhere(where, pattern, tick);
|
|
115
428
|
if (hit === 'stop')
|
|
116
429
|
return 'stop';
|
|
117
430
|
if (!hit)
|
|
@@ -122,12 +435,20 @@ function ruleMatches(data, lines, rule, tick) {
|
|
|
122
435
|
if (footerLine(data, lines) !== rule.footer)
|
|
123
436
|
return false;
|
|
124
437
|
}
|
|
438
|
+
if (rule.withoutRule) {
|
|
439
|
+
if (tick())
|
|
440
|
+
return 'stop';
|
|
441
|
+
if (findLast(lines, (line) => RULE_LINE.test(line)) >= 0)
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
125
444
|
if (rule.belowLastRule) {
|
|
126
445
|
if (tick())
|
|
127
446
|
return 'stop';
|
|
128
|
-
//
|
|
129
|
-
//
|
|
447
|
+
// A pattern sits below a composer rule. A window with no rule line is not
|
|
448
|
+
// below one: a dialog that fills the pane says so with `without_rule`.
|
|
130
449
|
const ruleAt = findLast(lines, (line) => RULE_LINE.test(line));
|
|
450
|
+
if (ruleAt < 0)
|
|
451
|
+
return false;
|
|
131
452
|
const at = findLast(lines, (line) => rule.belowLastRule?.test(line) ?? false);
|
|
132
453
|
if (at < 0 || at <= ruleAt)
|
|
133
454
|
return false;
|
|
@@ -201,41 +522,123 @@ function matches(line, pattern, tick) {
|
|
|
201
522
|
}
|
|
202
523
|
return true;
|
|
203
524
|
}
|
|
204
|
-
|
|
525
|
+
/** `plain` is the window's plain form; `styled` the window itself, for the input line's dim question. */
|
|
526
|
+
function compose(data, plain, styled, tick) {
|
|
205
527
|
const composer = data.composer;
|
|
206
528
|
if (composer.mode === 'box-to-rule')
|
|
207
|
-
return boxToRule(
|
|
529
|
+
return boxToRule(plain, styled, composer, tick);
|
|
208
530
|
if (composer.mode === 'status-last')
|
|
209
|
-
return statusLast(
|
|
531
|
+
return statusLast(plain, styled, composer, tick, false);
|
|
210
532
|
if (composer.mode === 'status-then-one')
|
|
211
|
-
return statusThenOne(
|
|
212
|
-
return twoRules(
|
|
533
|
+
return statusThenOne(plain, styled, composer, tick);
|
|
534
|
+
return twoRules(plain, styled, composer, tick);
|
|
213
535
|
}
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
}
|
|
542
|
+
function boxToRule(lines, styled, composer, tick) {
|
|
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;
|
|
216
549
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
217
550
|
if (tick())
|
|
218
551
|
return { kind: 'stop' };
|
|
219
|
-
if (composer
|
|
220
|
-
|
|
552
|
+
if (ruleRun(composer, lines[i] ?? '') !== null) {
|
|
553
|
+
close = i;
|
|
221
554
|
break;
|
|
222
555
|
}
|
|
223
556
|
}
|
|
224
|
-
if (
|
|
557
|
+
if (close < 0)
|
|
225
558
|
return { kind: 'unknown' };
|
|
226
|
-
|
|
227
|
-
|
|
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++) {
|
|
228
564
|
if (tick())
|
|
229
565
|
return { kind: 'stop' };
|
|
230
|
-
if (composer.
|
|
566
|
+
if (composer.prompt.test(lines[i] ?? ''))
|
|
567
|
+
return { kind: 'unknown' };
|
|
568
|
+
}
|
|
569
|
+
let open = -1;
|
|
570
|
+
for (let i = close - 1; i >= 0; i--) {
|
|
571
|
+
if (tick())
|
|
572
|
+
return { kind: 'stop' };
|
|
573
|
+
if (ruleRun(composer, lines[i] ?? '') !== null) {
|
|
574
|
+
open = i;
|
|
231
575
|
break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
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);
|
|
634
|
+
for (let i = input + 1; i < close; i++) {
|
|
232
635
|
if ((lines[i] ?? '').trim())
|
|
233
|
-
return { kind: 'unsent', from, input };
|
|
636
|
+
return { kind: 'unsent', from, input, rows };
|
|
234
637
|
}
|
|
235
638
|
const typed = (lines[input] ?? '').replace(composer.prompt, '').trim();
|
|
236
|
-
return { kind: placeholder(typed, composer
|
|
639
|
+
return { kind: placeholder(typed, composer, lines[input] ?? '', styled[input]) ? 'idle' : 'unsent', from, input, rows };
|
|
237
640
|
}
|
|
238
|
-
function statusLast(lines, composer, tick, allowOneTrailing) {
|
|
641
|
+
function statusLast(lines, styled, composer, tick, allowOneTrailing) {
|
|
239
642
|
const status = statusIndex(composer, lines, allowOneTrailing, tick);
|
|
240
643
|
if (status === 'stop')
|
|
241
644
|
return { kind: 'stop' };
|
|
@@ -246,13 +649,14 @@ function statusLast(lines, composer, tick, allowOneTrailing) {
|
|
|
246
649
|
input--;
|
|
247
650
|
if (input < 0)
|
|
248
651
|
return { kind: 'unknown' };
|
|
652
|
+
const rows = lines.slice(input + 1, status);
|
|
249
653
|
for (let i = input + 1; i < status; i++)
|
|
250
654
|
if ((lines[i] ?? '').trim())
|
|
251
|
-
return { kind: 'unsent', from: input, input };
|
|
252
|
-
return { kind: placeholder(stripTyped(lines[input] ?? '', composer), composer
|
|
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 };
|
|
253
657
|
}
|
|
254
|
-
function statusThenOne(lines, composer, tick) {
|
|
255
|
-
const found = statusLast(lines, composer, tick, true);
|
|
658
|
+
function statusThenOne(lines, styled, composer, tick) {
|
|
659
|
+
const found = statusLast(lines, styled, composer, tick, true);
|
|
256
660
|
if (found.kind !== 'unknown')
|
|
257
661
|
return found;
|
|
258
662
|
// No status line: the fallback rules decide. Anything they don't name is unknown.
|
|
@@ -279,26 +683,38 @@ function statusThenOne(lines, composer, tick) {
|
|
|
279
683
|
}
|
|
280
684
|
return { kind: 'unknown' };
|
|
281
685
|
}
|
|
282
|
-
function twoRules(lines, composer, tick) {
|
|
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.
|
|
283
691
|
let bottom = -1;
|
|
284
692
|
let top = -1;
|
|
285
|
-
let input = -1;
|
|
286
693
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
287
694
|
if (tick())
|
|
288
695
|
return { kind: 'stop' };
|
|
289
|
-
|
|
290
|
-
if (composer.rule.test(line)) {
|
|
696
|
+
if (composer.rule.test(lines[i] ?? '')) {
|
|
291
697
|
if (bottom === -1)
|
|
292
698
|
bottom = i;
|
|
293
|
-
else
|
|
699
|
+
else {
|
|
294
700
|
top = i;
|
|
295
701
|
break;
|
|
296
702
|
}
|
|
297
703
|
}
|
|
298
|
-
else if (bottom !== -1 && input === -1 && composer.prompt.test(line))
|
|
299
|
-
input = i;
|
|
300
704
|
}
|
|
301
|
-
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] ?? ''))
|
|
302
718
|
return { kind: 'unknown' };
|
|
303
719
|
let footer = false;
|
|
304
720
|
for (let i = bottom + 1; i < lines.length; i++) {
|
|
@@ -309,23 +725,28 @@ function twoRules(lines, composer, tick) {
|
|
|
309
725
|
}
|
|
310
726
|
if (!footer)
|
|
311
727
|
return { kind: 'unknown' };
|
|
312
|
-
|
|
313
|
-
|
|
728
|
+
const rows = lines.slice(input + 1, bottom);
|
|
729
|
+
if (rows.some((line) => line.trim()))
|
|
730
|
+
return { kind: 'unsent', from: top, input, rows };
|
|
314
731
|
const typed = (lines[input] ?? '').replace(composer.prompt, '').trim();
|
|
315
|
-
return { kind: placeholder(typed, composer
|
|
316
|
-
}
|
|
317
|
-
function boundaryBefore(lines, input, rule) {
|
|
318
|
-
for (let i = input - 1; i >= 0; i--)
|
|
319
|
-
if (rule.test(lines[i] ?? ''))
|
|
320
|
-
return i;
|
|
321
|
-
return input;
|
|
732
|
+
return { kind: placeholder(typed, composer, lines[input] ?? '', styled[input]) ? 'idle' : 'unsent', from: top, input, rows };
|
|
322
733
|
}
|
|
323
734
|
function stripTyped(line, composer) {
|
|
324
735
|
const stripped = composer.stripSuffix ? line.replace(composer.prompt, '').replace(composer.stripSuffix, '') : line.replace(composer.prompt, '');
|
|
325
736
|
return stripped.trim();
|
|
326
737
|
}
|
|
327
|
-
|
|
328
|
-
|
|
738
|
+
// A placeholder, not text. When the composer says its suggestions render dim and the input
|
|
739
|
+
// line carries any styling at all, the styling decides alone: all faint past the prompt is a
|
|
740
|
+
// greyed suggestion, anything else is text — the list is not asked, so no suggestion's
|
|
741
|
+
// styling can lend its words to typed characters after it. A line without styling (a plain
|
|
742
|
+
// read, an old herdr) leaves the list to decide, as does a composer without the style.
|
|
743
|
+
function placeholder(typed, composer, plainLine, styledLine) {
|
|
744
|
+
if (composer.placeholderStyle === 'dim' && styledLine !== undefined && hasSgr(styledLine)) {
|
|
745
|
+
const hit = composer.prompt.exec(plainLine);
|
|
746
|
+
const skip = hit === null ? 0 : plainLine.slice(0, hit.index + hit[0].length).replace(/\s/g, '').length;
|
|
747
|
+
return allDimAfter(styledLine, skip);
|
|
748
|
+
}
|
|
749
|
+
return composer.placeholders.some((entry) => ('equals' in entry ? typed === entry.equals : typed.startsWith(entry.prefix)));
|
|
329
750
|
}
|
|
330
751
|
function floorHits(lines, from, input, chrome, tick) {
|
|
331
752
|
for (let i = Math.max(0, from); i < lines.length; i++) {
|