@agentwhy/cli 0.2.1 → 0.3.1
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/dist/adapter/codex/contract/assumptions.js +4 -4
- package/dist/adapter/codex/contract/deliveries.js +12 -0
- package/dist/adapter/codex/contract/messages.js +8 -1
- package/dist/adapter/codex/events/cell-commands.js +191 -0
- package/dist/adapter/codex/events/delivered-output.js +30 -0
- package/dist/adapter/codex/events/rollout-scan.js +99 -7
- package/dist/core/access/command-line.js +4 -1
- package/dist/core/access/listing.js +10 -3
- package/dist/core/access/protected-access.js +94 -1
- package/dist/core/access/recorded-effect.js +22 -1
- package/dist/report/build-report.js +125 -19
- package/dist/report/render/report-page/file-story.js +33 -2
- package/dist/report/render/report-page/files-view.js +176 -55
- package/dist/report/render/report-page/files.js +51 -8
- package/dist/report/render/report-page/fix-wizard-script.js +17 -7
- package/dist/report/render/report-page/fix-wizard.js +4 -1
- package/dist/report/render/report-page/helpers.js +2 -1
- package/dist/report/render/report-page/report-page-renderer.js +51 -12
- package/dist/report/render/report-page/story-window.js +33 -12
- package/dist/report/render/report-page/to-do-view.js +42 -10
- package/dist/report/render/ui/app-sidebar.js +31 -2
- package/dist/report/render/ui/confirm-dialog.js +69 -4
- package/dist/report/render/ui/fold-line.js +2 -0
- package/dist/report/render/ui/popup.js +1 -1
- package/dist/report/render/ui/project-list.js +49 -10
- package/dist/report/render/ui/words/app-words.js +6 -0
- package/dist/report/render/ui/words/conversations-words.js +12 -0
- package/dist/report/render/ui/words/projects-words.js +9 -0
- package/dist/report/render/ui/words/report-words.js +114 -12
- package/dist/report/render/ui/words/settings-words.js +12 -9
- package/dist/report/start/conversations/calendar-view.js +1 -1
- package/dist/report/start/conversations/conversation-columns.js +6 -4
- package/dist/report/start/conversations/period-section.js +33 -17
- package/dist/report/start/conversations/periods.js +7 -1
- package/dist/report/start/render/session-status.js +14 -9
- package/dist/report/start/session-start.js +6 -3
- package/dist/report/start/settings/alerts-tab.js +2 -1
- package/package.json +1 -1
|
@@ -24,8 +24,12 @@ import { fixId, storyId } from './to-do-view.js';
|
|
|
24
24
|
* `protectKeys` names the files **Protect it** is offered for - private, not protected yet, and inside the project - and
|
|
25
25
|
* the everyday ones inside it, offered **Make it private**.
|
|
26
26
|
*/
|
|
27
|
-
export function filesView(rows, items, protectKeys, names) {
|
|
27
|
+
export function filesView(rows, items, protectKeys, names, leftOut = 0) {
|
|
28
28
|
const entries = rows.map((row, key) => ({ row, key }));
|
|
29
|
+
// Names past the most a page lists are said as a number, never dropped in silence (invariant 4).
|
|
30
|
+
const more = leftOut === 0 ? '' : '<p class="fl-note">' + inLanguages((t) => t('fl.namesLeftOut', { n: leftOut })) + '</p>';
|
|
31
|
+
// OW4 as amended 2026-10-05: an order is offered only where the files came up at more than one moment.
|
|
32
|
+
const orderable = momentsOf(rows) > 1;
|
|
29
33
|
const opened = entries.filter(({ row }) => !onlyNamed(row));
|
|
30
34
|
// One list, in the order a person deals with it (the maintainer, 2026-09-24): what the AI read, in coral; then the
|
|
31
35
|
// private files it only saw the name of, in blue; then everything else. A name only seen is still never counted as
|
|
@@ -33,7 +37,8 @@ export function filesView(rows, items, protectKeys, names) {
|
|
|
33
37
|
const listed = [...entries].sort((a, b) => tierOf(a.row) - tierOf(b.row));
|
|
34
38
|
if (opened.length === 0) {
|
|
35
39
|
return '<div class="fl"><div class="rp-alone">' + guideCard({ tone: 'mint', title: inLanguages((t) => t('fl.noneListed')), body: inLanguages((t) => t('fl.commands')) }) +
|
|
36
|
-
'</div>' + (listed.length === 0 ? '' :
|
|
40
|
+
'</div>' + (listed.length === 0 ? '' : (orderable ? '<div class="fl-filters js-only"><div class="fl-row">' + inLanguages(orderChoice) + '</div></div>' : '') +
|
|
41
|
+
table(listed, items, protectKeys, false, names, orderable)) + more + '</div>';
|
|
37
42
|
}
|
|
38
43
|
const privates = opened.filter(({ row }) => row.private).length;
|
|
39
44
|
const action = privates === 0 ? inLanguages((t) => t('fl.noneRead'))
|
|
@@ -42,14 +47,27 @@ export function filesView(rows, items, protectKeys, names) {
|
|
|
42
47
|
const lead = privates === 0 ? 'fl.leadNone' : privates === opened.length ? 'fl.leadAll' : 'fl.lead';
|
|
43
48
|
return '<div class="fl' + (privates === 0 ? ' fl-clean' : '') + '">' +
|
|
44
49
|
hero({ eyebrow: inLanguages((t) => t('fl.eyebrow')), fact: inLanguages((t) => t('fl.fact', { n: opened.length })), action, lead: inLanguages((t) => t(lead)) }) +
|
|
45
|
-
filters(listed.map(({ row }) => row)) + table(listed, items, protectKeys, true, names) +
|
|
50
|
+
filters(listed.map(({ row }) => row), orderable) + table(listed, items, protectKeys, true, names, orderable) +
|
|
46
51
|
'<p class="fl-note">' + inLanguages((t) => t('fl.shown', { shown: '<span data-files-shown>' + listed.length + '</span>', n: listed.length })) + ' ' +
|
|
47
|
-
inLanguages((t) => t('fl.commands')) + '</p
|
|
52
|
+
inLanguages((t) => t('fl.commands')) + '</p>' + more + '</div>';
|
|
48
53
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The table's parts, in the order a person deals with them (guidelines §9: the incident first, then what was stopped,
|
|
56
|
+
* then what is fine): read (coral), not known whether read (amber), stopped (mint), only its name seen (blue), the rest.
|
|
57
|
+
*/
|
|
58
|
+
const TIERS = [
|
|
59
|
+
{ key: 'fl.tier.read', tone: 'coral' }, { key: 'fl.tier.unknown', tone: 'amber' }, { key: 'fl.tier.stopped', tone: 'mint' },
|
|
60
|
+
{ key: 'fl.tier.name', tone: 'blue' }, { key: 'fl.tier.rest', tone: 'grey' },
|
|
61
|
+
];
|
|
51
62
|
function tierOf(row) {
|
|
52
|
-
|
|
63
|
+
// Only a private file has a part of its own; an everyday file, whatever was done to it, is part of the rest (P32,
|
|
64
|
+
// 2026-10-05). A private file stopped or of no known end was listed with the rest until 2026-10-05, so a conversation
|
|
65
|
+
// the AI was stopped in drew one part, and no heading at all.
|
|
66
|
+
if (!row.private)
|
|
67
|
+
return 4;
|
|
68
|
+
if (row.access === 'read' || row.access === 'changed')
|
|
69
|
+
return 0;
|
|
70
|
+
return row.access === 'unknown' ? 1 : row.access === 'stopped' ? 2 : onlyNamed(row) ? 3 : 4;
|
|
53
71
|
}
|
|
54
72
|
/**
|
|
55
73
|
* The window of every row not on the to-do list, written beside the page's other windows, outside the views. A private
|
|
@@ -61,8 +79,11 @@ export function fileWindows(rows, protectKeys, report, clock, names) {
|
|
|
61
79
|
return rows.map((row, key) => {
|
|
62
80
|
if (row.item !== undefined)
|
|
63
81
|
return '';
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
// `the-same-window-for-every-file` EF7: the window follows what the model holds for the row, not whether the file is
|
|
83
|
+
// private. An everyday file the AI read, changed or was stopped from has its calls (EF2) and tells the same story; a
|
|
84
|
+
// name only seen has none kept (EFD1) and keeps the simple window, as a file the flows name nowhere always has.
|
|
85
|
+
const story = fileStory(report, row.path);
|
|
86
|
+
return story.entries.length === 0 ? fileWindow(row, key, protectKeys, names) : rowStoryWindow(row, key, story, report, protectKeys, clock, names);
|
|
66
87
|
}).join('');
|
|
67
88
|
}
|
|
68
89
|
/** A private file with nothing to fix, in the story window: its row's words for what happened and what to do. */
|
|
@@ -71,7 +92,9 @@ function rowStoryWindow(row, key, story, report, protectKeys, clock, names) {
|
|
|
71
92
|
const read = report.privateFiles.find((file) => file.path === row.path);
|
|
72
93
|
const pattern = report.findings.find((finding) => finding.path === row.path)?.pattern;
|
|
73
94
|
const reach = LOOKS[LOOK[row.access === 'changed' ? 'read' : row.access]];
|
|
74
|
-
|
|
95
|
+
// A copy reached the AI, so the diagram ends at the company. A write is not that: for a private file it has always
|
|
96
|
+
// followed a read, and for an everyday file (EF1) nothing says what was inside before it was written.
|
|
97
|
+
const exposed = row.access === 'read' || (row.private && row.access === 'changed');
|
|
75
98
|
const protectKey = protectKeys.get(row.path);
|
|
76
99
|
return storyPopup(story, {
|
|
77
100
|
path: row.path,
|
|
@@ -79,11 +102,18 @@ function rowStoryWindow(row, key, story, report, protectKeys, clock, names) {
|
|
|
79
102
|
title: label(row.kind),
|
|
80
103
|
...(pattern === undefined ? {} : { pattern }),
|
|
81
104
|
exposed,
|
|
82
|
-
|
|
105
|
+
...(row.private ? {} : { everyday: true }),
|
|
106
|
+
summary: label(didKey(row)),
|
|
83
107
|
...(exposed ? {} : { look: { glyph: reach.glyph, tone: reach.tone } }),
|
|
84
108
|
}, 'file-' + key, report.scope.sessionId, clock, { context: label(nextOf(row)), names }, protectKey === undefined ? '' : '<span data-protect-open="' + protectKey + '">' +
|
|
85
109
|
pill({ label: label(protectLabel(row)), tone: 'light', size: 'md', href: '#protect-' + protectKey, attributes: opener('protect-' + protectKey) }) + '</span>');
|
|
86
110
|
}
|
|
111
|
+
/** What the AI did to a file, in its window's words: an everyday file's read and stop have words of their own. */
|
|
112
|
+
function didKey(row) {
|
|
113
|
+
if (row.private)
|
|
114
|
+
return 'fl.w.did.' + row.access;
|
|
115
|
+
return row.access === 'changed' ? 'fl.w.did.changed' : row.access === 'stopped' ? 'fl.w.did.everydayStopped' : 'fl.w.did.everydayRead';
|
|
116
|
+
}
|
|
87
117
|
/** What to do about a file with nothing on the to-do list: the last answer of its window. */
|
|
88
118
|
function nextOf(row) {
|
|
89
119
|
return !row.private ? 'fl.w.next.none' : row.protection === 'yes' ? 'fl.w.next.safe' : row.protection === 'told' ? 'fl.w.next.told' : 'fl.w.next.maybe';
|
|
@@ -102,8 +132,54 @@ const PROTECTED = {
|
|
|
102
132
|
function didOf(row) {
|
|
103
133
|
return row.access === 'read' || row.access === 'name' || row.access === 'stopped' ? row.access : 'other';
|
|
104
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* OW4: the order of the rows - what needs the person first, as drawn, or the order the AI went - asked as a question,
|
|
137
|
+
* as the filters are, and a script's: without one the rows stay as drawn (OW6).
|
|
138
|
+
*/
|
|
139
|
+
function orderChoice(t) {
|
|
140
|
+
return labelledSelect(e(t('fl.order.q')), [
|
|
141
|
+
{ value: 'need', label: e(t('fl.order.need')) },
|
|
142
|
+
{ value: 'steps', label: e(t('fl.order.steps')) },
|
|
143
|
+
], ' data-files-sort data-live-keep');
|
|
144
|
+
}
|
|
145
|
+
/** The moments the rows came up at: each AI's times it came across files, and one more for rows with none known. */
|
|
146
|
+
function momentsOf(rows) {
|
|
147
|
+
return new Set(rows.map((row) => (row.step === undefined ? 'none' : row.step.rank + ':' + row.step.number))).size;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* OW3 as amended 2026-10-05: a heading over the files of each moment, in words - "First", then "Then" - shown only in
|
|
151
|
+
* the order the AI went, where its rows are put under it. A helper's are named for it. Drawn hidden after the rows: the
|
|
152
|
+
* script moves each to its moment, before the moment's first file, and shows it.
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* EF6: one moment in words - "First", "Then", a helper's - the same words the table heads its moments with (OW3), for
|
|
156
|
+
* the window of a row that is not ordered beside anything. An action number is Advanced's alone (guidelines §7).
|
|
157
|
+
*/
|
|
158
|
+
function momentWords(step) {
|
|
159
|
+
return inLanguages((t) => step === undefined ? t('fl.order.unknown')
|
|
160
|
+
: step.helper === undefined ? t(step.number === 1 ? 'fl.order.first' : 'fl.order.then')
|
|
161
|
+
: t(step.number === 1 ? 'fl.order.helperFirst' : 'fl.order.helperThen', { helper: t('st.helper', { ordinal: step.helper }) }));
|
|
162
|
+
}
|
|
163
|
+
function momentHeadings(rows) {
|
|
164
|
+
const moments = new Map();
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
const key = row.step === undefined ? 'none' : row.step.rank + ':' + row.step.number;
|
|
167
|
+
const known = moments.get(key);
|
|
168
|
+
if (known === undefined)
|
|
169
|
+
moments.set(key, { ...(row.step === undefined ? {} : { step: row.step }), count: 1 });
|
|
170
|
+
else
|
|
171
|
+
known.count += 1;
|
|
172
|
+
}
|
|
173
|
+
const at = (step) => [step?.rank ?? 9999, step?.number ?? 0];
|
|
174
|
+
return [...moments.values()].sort((a, b) => at(a.step)[0] - at(b.step)[0] || at(a.step)[1] - at(b.step)[1]).map(({ step, count }) => ({
|
|
175
|
+
group: momentWords(step),
|
|
176
|
+
tone: 'grey',
|
|
177
|
+
count,
|
|
178
|
+
attributes: ' data-files-step hidden data-order-agent="' + (step?.rank ?? 9999) + '" data-order-step="' + (step?.number ?? 0) + '" data-order-place="-1"',
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
105
181
|
/** P34: the search, the three groups with their counts, and the two questions - a script's, so only with one. */
|
|
106
|
-
function filters(rows) {
|
|
182
|
+
function filters(rows, orderable) {
|
|
107
183
|
const count = (test) => rows.filter(test).length;
|
|
108
184
|
const groups = [['all', rows.length], ['fix', count((row) => row.group === 'fix')], ['fixed', count((row) => row.group === 'fixed')]]
|
|
109
185
|
.map(([group, n]) => '<button type="button" class="fl-pill' + (group === 'all' ? ' fl-on' : '') + '" data-files-group="' + group + '" data-live-keep aria-pressed="' + String(group === 'all') + '">' +
|
|
@@ -120,13 +196,13 @@ function filters(rows) {
|
|
|
120
196
|
'<input class="fl-search" type="search" data-files-search data-live-keep placeholder="' + e(translator('en')('fl.search')) + '"' +
|
|
121
197
|
LANGS.map((lang) => ' data-placeholder-' + lang + '="' + e(translator(lang)('fl.search')) + '"').join('') + labelAttributes((t) => e(t('fl.search'))) + '>' +
|
|
122
198
|
'<div class="fl-pills">' + groups + '</div></div>' +
|
|
123
|
-
'<div class="fl-row">' + inLanguages(did) + inLanguages(prot) + textButton(inLanguages((t) => t('fl.clear')), ' data-files-clear hidden', 'fl-clear') + '</div></div>';
|
|
199
|
+
'<div class="fl-row">' + (orderable ? inLanguages(orderChoice) : '') + inLanguages(did) + inLanguages(prot) + textButton(inLanguages((t) => t('fl.clear')), ' data-files-clear hidden', 'fl-clear') + '</div></div>';
|
|
124
200
|
}
|
|
125
201
|
/**
|
|
126
202
|
* P35: `File · AI · Private file · When your AI reaches it · Action` (the mode its own column since 2026-09-25; X28 names
|
|
127
203
|
* the AI it holds for since 2026-09-29), a row opening its story window or its simple one (P37).
|
|
128
204
|
*/
|
|
129
|
-
function table(entries, items, protectKeys, filtered, names) {
|
|
205
|
+
function table(entries, items, protectKeys, filtered, names, orderable) {
|
|
130
206
|
const label = (key) => inLanguages((t) => t(key));
|
|
131
207
|
return dataTable({
|
|
132
208
|
head: [label('fl.col.file'), label('fl.col.ai'), label('fl.col.prot'), label('fl.col.mode'), label('fl.col.action')],
|
|
@@ -134,38 +210,40 @@ function table(entries, items, protectKeys, filtered, names) {
|
|
|
134
210
|
columns: 'minmax(240px,2fr) 120px 140px minmax(250px,1.2fr) 110px',
|
|
135
211
|
minWidth: 980,
|
|
136
212
|
empty: label('fl.noMatch'),
|
|
137
|
-
// Only the table of opened files is the one the filters work on;
|
|
138
|
-
|
|
139
|
-
rows: entries.flatMap(({ row, key }, at) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
213
|
+
// Only the table of opened files is the one the filters work on; either may be put in the order the AI went (OW4).
|
|
214
|
+
attributes: (filtered ? ' data-files-table' : '') + (orderable ? ' data-files-orderable' : ''),
|
|
215
|
+
rows: [...entries.flatMap(({ row, key }, at) => {
|
|
216
|
+
const tier = tierOf(row);
|
|
217
|
+
// A heading over each part, where the table has more than one: its first row is where the part begins.
|
|
218
|
+
const parts = new Set(entries.map((entry) => tierOf(entry.row))).size;
|
|
219
|
+
const heading = parts > 1 && (at === 0 || tierOf(entries[at - 1].row) !== tier)
|
|
220
|
+
? [{
|
|
221
|
+
group: label(TIERS[tier].key), tone: TIERS[tier].tone, count: entries.filter((entry) => tierOf(entry.row) === tier).length,
|
|
222
|
+
attributes: ' data-files-tier="' + tier + '"',
|
|
223
|
+
}]
|
|
224
|
+
: [];
|
|
225
|
+
const item = row.item === undefined ? undefined : items[row.item];
|
|
226
|
+
const target = row.item === undefined ? 'file-' + key : storyId(row.item);
|
|
227
|
+
const kind = item === undefined ? label(row.kind) : inLanguages((t, lang) => titleOf(item, t, lang));
|
|
228
|
+
return [...heading, {
|
|
229
|
+
cells: [
|
|
230
|
+
'<span class="fl-kind">' + kind + '</span><span class="fl-chip" title="' + e(row.path) + '">' + e(names(row.path)) + '</span>',
|
|
231
|
+
row.private ? statusIcon(LOOK[row.access === 'changed' ? 'read' : row.access], label('fl.acc.' + row.access)) : '<span class="fl-plain">' + label('fl.acc.' + row.access) + '</span>',
|
|
232
|
+
privateCell(row, protectKeys.get(row.path)),
|
|
233
|
+
modeCell(row, protectKeys.get(row.path)),
|
|
234
|
+
actionCell(row),
|
|
235
|
+
],
|
|
236
|
+
href: '#' + target,
|
|
237
|
+
linkAttributes: opener(target),
|
|
238
|
+
label: e(row.path),
|
|
239
|
+
// The status bar: protected now, read before, and not fixed - the one row whose rule came too late (guidelines §4).
|
|
240
|
+
bar: row.private && row.protection === 'yes' && row.access === 'read' && row.group === 'fix',
|
|
241
|
+
attributes: ' data-live-key="' + e(row.path) + '" data-file-key="' + key + '" data-tier="' + tier + '" data-group="' + row.group + '" data-did="' + didOf(row) + '" data-prot="' + row.protection + '"' +
|
|
242
|
+
' data-search="' + e(row.path.toLowerCase()) + '"' + (row.item === undefined ? '' : ' data-file-item="' + row.item + '"') +
|
|
243
|
+
// OW5: by AI, then step, then the file's place in it; a row with no step last.
|
|
244
|
+
' data-order-agent="' + (row.step?.rank ?? 9999) + '" data-order-step="' + (row.step?.number ?? 0) + '" data-order-place="' + (row.step?.place ?? 0) + '"',
|
|
245
|
+
}];
|
|
246
|
+
}), ...(orderable ? momentHeadings(entries.map(({ row }) => row)) : [])],
|
|
169
247
|
});
|
|
170
248
|
}
|
|
171
249
|
/**
|
|
@@ -193,7 +271,12 @@ function privateCell(row, key) {
|
|
|
193
271
|
function modeCell(row, key) {
|
|
194
272
|
const label = (id) => inLanguages((t) => t(id));
|
|
195
273
|
const blocked = glyphIcon(MODE_SVG.block, 'mint', label('fl.mode.yes'));
|
|
196
|
-
const
|
|
274
|
+
const tracked = glyphIcon(MODE_SVG.tell, 'sand', label('fl.mode.told'));
|
|
275
|
+
// `block-or-track-from-the-report` BT7: the window writes one of two answers, so both are drawn beside the row's own
|
|
276
|
+
// and the script shows the one the person chose - it never writes markup of its own.
|
|
277
|
+
const made = key === undefined ? ''
|
|
278
|
+
: '<span data-made="block" data-made-key="' + key + '" hidden>' + blocked + '</span>' +
|
|
279
|
+
'<span data-made="tell" data-made-key="' + key + '" hidden>' + tracked + '</span>';
|
|
197
280
|
if (row.protection === 'yes')
|
|
198
281
|
return blocked;
|
|
199
282
|
// F57: the person chose this. Nothing is wrong with it, so it is in Track's sand, and nothing offers to protect it.
|
|
@@ -230,9 +313,7 @@ function actionCell(row) {
|
|
|
230
313
|
function fileWindow(row, key, protectKeys, names) {
|
|
231
314
|
const id = 'file-' + key;
|
|
232
315
|
const label = (k) => inLanguages((t) => t(k));
|
|
233
|
-
const did = row
|
|
234
|
-
? 'fl.w.did.' + row.access
|
|
235
|
-
: row.access === 'changed' ? 'fl.w.did.changed' : row.access === 'stopped' ? 'fl.w.did.everydayStopped' : 'fl.w.did.everydayRead';
|
|
316
|
+
const did = didKey(row);
|
|
236
317
|
const next = nextOf(row);
|
|
237
318
|
const protectKey = protectKeys.get(row.path);
|
|
238
319
|
// Each answer carries its colour's glyph, so the state is seen before it is read (guidelines §9.3): what the AI did
|
|
@@ -242,12 +323,15 @@ function fileWindow(row, key, protectKeys, names) {
|
|
|
242
323
|
[reach.glyph, row.private ? reach.tone : 'grey'],
|
|
243
324
|
PROTECTED[row.protection],
|
|
244
325
|
[LOOKS.none.glyph, 'grey'],
|
|
326
|
+
['·', 'grey'],
|
|
245
327
|
];
|
|
246
|
-
const
|
|
328
|
+
const written = (at, question, answer) => {
|
|
247
329
|
const [glyph, tone] = glyphs[at];
|
|
248
330
|
return '<div class="fw-row"><span class="fw-glyph fw-' + tone + '" aria-hidden="true">' + glyph + '</span>' +
|
|
249
|
-
'<div><div class="fw-q">' + label(question) + '</div><div class="fw-a">' +
|
|
331
|
+
'<div><div class="fw-q">' + label(question) + '</div><div class="fw-a">' + answer + '</div></div></div>';
|
|
250
332
|
};
|
|
333
|
+
const fact = (at, question, answer) => written(at, question, label(answer));
|
|
334
|
+
const moment = (at, answer) => written(at, 'fl.w.when', answer);
|
|
251
335
|
return popup({
|
|
252
336
|
id,
|
|
253
337
|
size: 'small',
|
|
@@ -255,7 +339,10 @@ function fileWindow(row, key, protectKeys, names) {
|
|
|
255
339
|
body: '<div class="fw"><div class="fw-top"><div class="fw-id"><h2 class="fw-title" id="' + id + '-title">' + label(row.kind) + '</h2>' +
|
|
256
340
|
'<span class="fl-chip" title="' + e(row.path) + '">' + e(names(row.path)) + '</span></div>' +
|
|
257
341
|
closeButton(labelAttributes((t) => t('app.close')) + CLOSES, 'sm') + '</div>' +
|
|
258
|
-
'<div class="fw-facts">' + fact(0, 'fl.w.did', did) + fact(1, 'fl.w.prot', 'fl.w.prot.' + row.protection) + fact(2, 'fl.w.next', next) +
|
|
342
|
+
'<div class="fw-facts">' + fact(0, 'fl.w.did', did) + fact(1, 'fl.w.prot', 'fl.w.prot.' + row.protection) + fact(2, 'fl.w.next', next) +
|
|
343
|
+
// EF6: where the model says at which moment the file came up, this window says it too - the row beside it does.
|
|
344
|
+
// No step known draws no row: an absent answer is not the claim that nothing is known about the order (invariant 4).
|
|
345
|
+
(row.step === undefined ? '' : moment(3, momentWords(row.step))) + '</div></div>' +
|
|
259
346
|
popupFoot('', pill({ label: label('app.close'), tone: 'outline', size: 'md', button: true, attributes: CLOSES }) +
|
|
260
347
|
(protectKey === undefined ? '' : '<span data-protect-open="' + protectKey + '">' +
|
|
261
348
|
pill({ label: label(protectLabel(row)), tone: row.protection === 'na' ? 'outline' : 'light', size: 'md', href: '#protect-' + protectKey, attributes: opener('protect-' + protectKey) }) + '</span>'), true),
|
|
@@ -269,6 +356,14 @@ function fileWindow(row, key, protectKeys, names) {
|
|
|
269
356
|
*/
|
|
270
357
|
export const FILES_SCRIPT = String.raw `
|
|
271
358
|
(() => {
|
|
359
|
+
// OW5 as amended 2026-10-05: in the order the AI went, each moment's heading stands over its files while one shows.
|
|
360
|
+
const moments = (table) => {
|
|
361
|
+
const by = table.classList.contains('fl-by-step');
|
|
362
|
+
const files = [...table.querySelectorAll('[data-file-key]')];
|
|
363
|
+
table.querySelectorAll('[data-files-step]').forEach((heading) => {
|
|
364
|
+
heading.hidden = !by || !files.some((row) => !row.hidden && row.dataset.orderAgent === heading.dataset.orderAgent && row.dataset.orderStep === heading.dataset.orderStep);
|
|
365
|
+
});
|
|
366
|
+
};
|
|
272
367
|
const started = new WeakSet();
|
|
273
368
|
const start = (table) => {
|
|
274
369
|
if (started.has(table)) return;
|
|
@@ -288,6 +383,7 @@ export const FILES_SCRIPT = String.raw `
|
|
|
288
383
|
table.querySelectorAll('[data-files-tier]').forEach((heading) => {
|
|
289
384
|
heading.hidden = !rows.some((row) => !row.hidden && row.dataset.tier === heading.dataset.filesTier);
|
|
290
385
|
});
|
|
386
|
+
moments(table);
|
|
291
387
|
box.querySelectorAll('[data-files-shown]').forEach((element) => { element.textContent = String(shown); });
|
|
292
388
|
const empty = table.querySelector('.dt-empty');
|
|
293
389
|
if (empty) empty.hidden = shown !== 0;
|
|
@@ -321,7 +417,31 @@ export const FILES_SCRIPT = String.raw `
|
|
|
321
417
|
document.addEventListener('files-changed', apply);
|
|
322
418
|
apply();
|
|
323
419
|
};
|
|
324
|
-
|
|
420
|
+
// OW5, OW6: the order the AI went moves the rows and their moments' headings, by AI, step and place, and hides the
|
|
421
|
+
// parts' headings by a class the filters do not touch; the default puts every element back where it was drawn, the
|
|
422
|
+
// moments' headings hidden. Filters and search are left as they are.
|
|
423
|
+
const ordered = new WeakSet();
|
|
424
|
+
const order = (table) => {
|
|
425
|
+
if (ordered.has(table)) return;
|
|
426
|
+
ordered.add(table);
|
|
427
|
+
const box = table.closest('.fl') || document;
|
|
428
|
+
const drawn = [...table.children];
|
|
429
|
+
const key = (row) => [Number(row.dataset.orderAgent), Number(row.dataset.orderStep), Number(row.dataset.orderPlace)];
|
|
430
|
+
box.addEventListener('change', (event) => {
|
|
431
|
+
if (!event.target.matches('[data-files-sort]')) return;
|
|
432
|
+
const bySteps = event.target.value === 'steps';
|
|
433
|
+
box.querySelectorAll('[data-files-sort]').forEach((select) => { select.value = event.target.value; });
|
|
434
|
+
table.classList.toggle('fl-by-step', bySteps);
|
|
435
|
+
const rows = drawn.filter((element) => element.hasAttribute('data-order-agent'));
|
|
436
|
+
const next = bySteps ? rows.slice().sort((a, b) => { const x = key(a); const y = key(b); return x[0] - y[0] || x[1] - y[1] || x[2] - y[2]; }) : drawn;
|
|
437
|
+
next.forEach((element) => table.appendChild(element));
|
|
438
|
+
moments(table);
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
window.agentwhyFiles = (scope) => {
|
|
442
|
+
scope.querySelectorAll('[data-files-table]').forEach(start);
|
|
443
|
+
scope.querySelectorAll('[data-files-orderable]').forEach(order);
|
|
444
|
+
};
|
|
325
445
|
window.agentwhyFiles(document);
|
|
326
446
|
})();
|
|
327
447
|
`;
|
|
@@ -334,6 +454,7 @@ export const FILES_SCRIPT = String.raw `
|
|
|
334
454
|
*/
|
|
335
455
|
export const FILES_VIEW_STYLE = String.raw `
|
|
336
456
|
.fl{max-width:1100px;margin:0 auto;container:files/inline-size}
|
|
457
|
+
.fl-by-step [data-files-tier]{display:none}
|
|
337
458
|
.fl .dt-group{position:sticky;top:0;z-index:3;padding:13px 20px;font-size:16px;font-weight:650;letter-spacing:0;color:var(--text)}
|
|
338
459
|
.fl .dt-group-cell{gap:10px}
|
|
339
460
|
.fl .dt-group-dot{width:9px;height:9px}
|
|
@@ -359,7 +480,7 @@ export const FILES_VIEW_STYLE = String.raw `
|
|
|
359
480
|
.fl-chip{display:inline-flex;margin-top:6px;font-family:var(--mono);font-size:13.5px;font-weight:500;color:var(--text);background:var(--white-07);border:1px solid var(--white-10);border-radius:6px;padding:2px 7px;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
|
|
360
481
|
.fl-plain{font-size:13.5px;font-weight:600;color:var(--text-2)}
|
|
361
482
|
.fl-prot{display:flex;align-items:center;gap:10px;flex-wrap:nowrap}
|
|
362
|
-
.fl-prot[hidden],[data-protected][hidden]{display:none}
|
|
483
|
+
.fl-prot[hidden],[data-protected][hidden],[data-made-key][hidden]{display:none}
|
|
363
484
|
.fw-glyph svg{width:13px;height:13px}
|
|
364
485
|
[data-row-control]{position:relative;z-index:2}
|
|
365
486
|
.fl-act{font-size:13.5px;font-weight:600;white-space:nowrap}.fl-act[hidden]{display:none}
|
|
@@ -4,8 +4,40 @@ import { matchesGlob } from '../../../core/policy/glob.js';
|
|
|
4
4
|
import { helperViews } from './helpers.js';
|
|
5
5
|
import { nameOf, ruleKey } from './item-names.js';
|
|
6
6
|
import { toDoItems } from './to-do.js';
|
|
7
|
+
/** Each file's step, by its path, with each AI ranked as the Helpers tab orders them: Your AI, then helpers by number. */
|
|
8
|
+
function stepsOf(report, accessOf) {
|
|
9
|
+
const helpers = [...report.graph.agents].sort((a, b) => (a.ordinal ?? Infinity) - (b.ordinal ?? Infinity) || a.index - b.index);
|
|
10
|
+
const agents = new Map([report.graph.main, ...helpers].map((agent, rank) => [agent.index, { rank, ordinal: agent === report.graph.main ? undefined : agent.ordinal }]));
|
|
11
|
+
// OW1 as amended 2026-10-05: a row stands at the first step that did to its file what the row says - the read after
|
|
12
|
+
// the listing that named it - else at the first that reached it. The steps come in the page's order of AIs.
|
|
13
|
+
const chosen = new Map();
|
|
14
|
+
for (const step of report.fileSteps ?? []) {
|
|
15
|
+
const access = accessOf.get(step.path);
|
|
16
|
+
const fits = (one) => access !== undefined && one.how === HOW[access];
|
|
17
|
+
const known = chosen.get(step.path);
|
|
18
|
+
if (known === undefined || (!fits(known) && fits(step)))
|
|
19
|
+
chosen.set(step.path, step);
|
|
20
|
+
}
|
|
21
|
+
// Each AI's moments, in its own order, numbered from 1: the action's own number stays in Advanced.
|
|
22
|
+
const times = new Map();
|
|
23
|
+
for (const step of chosen.values())
|
|
24
|
+
times.set(step.agentIndex, [...new Set([...(times.get(step.agentIndex) ?? []), step.step])].sort((a, b) => a - b));
|
|
25
|
+
return new Map([...chosen.values()].flatMap((step) => {
|
|
26
|
+
const agent = agents.get(step.agentIndex);
|
|
27
|
+
if (agent === undefined)
|
|
28
|
+
return [];
|
|
29
|
+
return [[step.path, {
|
|
30
|
+
rank: agent.rank, ...(agent.ordinal === undefined ? {} : { helper: agent.ordinal }),
|
|
31
|
+
number: (times.get(step.agentIndex) ?? []).indexOf(step.step) + 1, place: step.place,
|
|
32
|
+
}]];
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
7
35
|
const REACH = { read: 'read', unknown: 'unknown', named: 'name', stopped: 'stopped' };
|
|
8
|
-
|
|
36
|
+
/** What an action did to a file, in the model's words, for each thing a row says. */
|
|
37
|
+
const HOW = { read: 'read', name: 'named', unknown: 'unknown', stopped: 'stopped', changed: 'changed' };
|
|
38
|
+
// A stop outranks a name seen (changed 2026-10-05 by the maintainer): `rg --files` found it, `cat` of it was stopped -
|
|
39
|
+
// the rule held, which the person is to hear, and the name alone is the step before it.
|
|
40
|
+
const STRENGTH = ['read', 'unknown', 'stopped', 'name', 'changed'];
|
|
9
41
|
export function fileRows(report, items, done, denied) {
|
|
10
42
|
const patternOf = new Map(report.findings.map((finding) => [finding.path, finding.pattern]));
|
|
11
43
|
const reached = new Map();
|
|
@@ -46,23 +78,34 @@ export function fileRows(report, items, done, denied) {
|
|
|
46
78
|
const everydayRows = report.everydayFiles.filter((file) => !reached.has(file.path)).map((file) => ({
|
|
47
79
|
path: file.path,
|
|
48
80
|
private: false,
|
|
49
|
-
access: file.how === 'changed' ? 'changed' : file.how === 'stopped' ? 'stopped' : 'read',
|
|
81
|
+
access: file.how === 'changed' ? 'changed' : file.how === 'stopped' ? 'stopped' : file.how === 'named' ? 'name' : 'read',
|
|
50
82
|
protection: 'na',
|
|
51
83
|
group: 'none',
|
|
52
84
|
kind: kindOfName(nameOf(file.path)),
|
|
53
85
|
}));
|
|
54
|
-
|
|
86
|
+
const rows = [...privateRows, ...everydayRows];
|
|
87
|
+
// A row's step is chosen by what the row says, so it is found once every row is.
|
|
88
|
+
const steps = stepsOf(report, new Map(rows.map((row) => [row.path, row.access])));
|
|
89
|
+
return rows.map((row) => {
|
|
90
|
+
const step = steps.get(row.path);
|
|
91
|
+
return step === undefined ? row : { ...row, step };
|
|
92
|
+
});
|
|
55
93
|
}
|
|
56
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* A file the AI only saw the name of, private or not: nothing shows it opened it, or that it exists, so it is never
|
|
96
|
+
* counted as opened (P32a) - a row of the table all the same.
|
|
97
|
+
*/
|
|
57
98
|
export function onlyNamed(row) {
|
|
58
|
-
return row.
|
|
99
|
+
return row.access === 'name';
|
|
59
100
|
}
|
|
60
101
|
/**
|
|
61
|
-
* How many
|
|
62
|
-
*
|
|
102
|
+
* How many rows the Files tab lists - what a conversation's "See all {n} files" leads to (F14). Every row, a name only
|
|
103
|
+
* seen among them (changed 2026-10-05: a session whose AI listed a folder had no link to the files it saw); the
|
|
104
|
+
* sidebar's count and the tab's heading keep to the files opened (P32a). Marks and rules change a row's group and
|
|
105
|
+
* protection, never whether it is a row, so none is needed to count them.
|
|
63
106
|
*/
|
|
64
107
|
export function listedFiles(report) {
|
|
65
|
-
return fileRows(report, toDoItems(report), new Set(), undefined).
|
|
108
|
+
return fileRows(report, toDoItems(report), new Set(), undefined).length;
|
|
66
109
|
}
|
|
67
110
|
const SOURCE = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|c|h|cpp|hpp|cs|php|sh|vue|svelte|css|scss|html|sql)$/i;
|
|
68
111
|
/** P33's fixed table: what an everyday file is, from its name alone. */
|
|
@@ -46,30 +46,40 @@ export const FIX_WIZARD_SCRIPT = String.raw `
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
// P38: the rule is sent, and the window closes only once it is written; otherwise it says why, or what to run.
|
|
49
|
+
// BT6: the window may ask which mode the file gets, and each of its two buttons says which it is. A block is the deny
|
|
50
|
+
// rule it has always been; Track takes no rule out - there is none holding the file - and puts the pattern on this
|
|
51
|
+
// person's own told list, where a block written from here already goes without asking (BTD2).
|
|
49
52
|
const protect = (button) => {
|
|
50
53
|
const dialog = button.closest('dialog');
|
|
51
|
-
const
|
|
54
|
+
const mode = button.getAttribute('data-protect-mode') || 'block';
|
|
55
|
+
const every = dialog.querySelector('[data-protect-every="' + mode + '"]');
|
|
52
56
|
const pattern = every && every.checked ? button.getAttribute('data-pattern-every') : button.getAttribute('data-pattern');
|
|
53
57
|
const reason = dialog.querySelector('[data-note-reason]');
|
|
54
|
-
const command = dialog.querySelector('[data-note-command]');
|
|
58
|
+
const command = dialog.querySelector('[data-note-command="' + mode + '"]');
|
|
55
59
|
reason.hidden = true;
|
|
56
|
-
command.hidden = true;
|
|
60
|
+
if (command) command.hidden = true;
|
|
57
61
|
const buttons = [...dialog.querySelectorAll('button')];
|
|
58
62
|
buttons.forEach((each) => { each.disabled = true; });
|
|
59
|
-
|
|
63
|
+
const body = mode === 'tell'
|
|
64
|
+
? { change: 'mode', to: 'tell', patterns: [pattern], rules: [], where: 'local' }
|
|
65
|
+
: { change: 'protect', pattern };
|
|
66
|
+
send('api/settings', body).then((answer) => {
|
|
60
67
|
buttons.forEach((each) => { each.disabled = false; });
|
|
61
68
|
if (answer.ok) {
|
|
62
69
|
const at = button.getAttribute('data-protect');
|
|
63
70
|
document.querySelectorAll('[data-protect-open="' + at + '"]').forEach((open) => { open.hidden = true; open.style.display = 'none'; });
|
|
64
71
|
document.querySelectorAll('[data-protected="' + at + '"]').forEach((tag) => { tag.hidden = false; });
|
|
65
|
-
|
|
72
|
+
// BT7: the answer the person chose, out of the ones the row was drawn with.
|
|
73
|
+
document.querySelectorAll('[data-made-key="' + at + '"]').forEach((one) => { one.hidden = one.getAttribute('data-made') !== mode; });
|
|
74
|
+
document.querySelectorAll('[data-file-key="' + at + '"]').forEach((row) => { row.dataset.prot = mode === 'tell' ? 'told' : 'yes'; });
|
|
66
75
|
document.dispatchEvent(new CustomEvent('files-changed'));
|
|
67
76
|
dialog.close();
|
|
68
77
|
} else if (answer.reached) {
|
|
69
78
|
reason.textContent = answer.message;
|
|
70
79
|
reason.hidden = false;
|
|
71
|
-
} else {
|
|
72
|
-
|
|
80
|
+
} else if (command) {
|
|
81
|
+
// BT8: no flag writes a told list, so Track hands over the command that opens the page where it can be written.
|
|
82
|
+
handOver(command, mode === 'tell' ? 'agentwhy start' : 'agentwhy init --protect ' + quoted(pattern));
|
|
73
83
|
}
|
|
74
84
|
});
|
|
75
85
|
};
|
|
@@ -165,7 +165,9 @@ function dataSteps(item, page, note) {
|
|
|
165
165
|
: key === undefined
|
|
166
166
|
? '<p class="wz-p">' + label(page.shared ? 'wz.stop.shared' : 'wz.stop.outside') + '</p>'
|
|
167
167
|
: pill({ label: label('wz.protect'), tone: 'light', size: 'lg', href: '#protect-' + key, attributes: opener('protect-' + key) + ' data-protect-open="' + key + '"' }) +
|
|
168
|
-
|
|
168
|
+
// BT7: the window writes one of two answers, so the step holds both and the script shows the one chosen.
|
|
169
|
+
'<span class="wz-protected" data-made="block" data-made-key="' + key + '" hidden>' + label('pr.done') + '</span>' +
|
|
170
|
+
'<span class="wz-protected wz-tracked" data-made="tell" data-made-key="' + key + '" hidden>' + label('md.done.tell') + '</span>' +
|
|
169
171
|
'<p class="wz-honest">' + label('wz.stop.honest') + '</p>';
|
|
170
172
|
return '<section class="wz-step" data-step="1">' + stepHead('wz.stepOf', 1, 'wz.data.title') +
|
|
171
173
|
'<div class="wz-ask">' + label('wz.data.ask') + '</div>' +
|
|
@@ -284,6 +286,7 @@ export const FIX_WIZARD_STYLE = String.raw `
|
|
|
284
286
|
.wz-reason{margin:18px 0 0;padding:12px 14px;border-radius:12px;background:var(--coral-07);border:1px solid var(--coral-35);font-size:14px;line-height:1.5;white-space:pre-line}
|
|
285
287
|
.wz-reason[hidden],.wz-handover[hidden],.wz-protected[hidden]{display:none}
|
|
286
288
|
.wz-protected{display:inline-flex;align-items:center;padding:12px 20px;border-radius:999px;background:var(--mint-14);color:var(--mint);font-size:15px;font-weight:600}
|
|
289
|
+
.wz-tracked{background:var(--sand-16);color:var(--sand)}
|
|
287
290
|
.wz-foot{position:sticky;bottom:0;z-index:1;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:16px 24px;border-top:1px solid var(--white-07);background:var(--popup-foot)}
|
|
288
291
|
.wz-foot-left,.wz-foot-right{display:flex;gap:14px;align-items:center}.wz-foot-right{gap:10px}
|
|
289
292
|
.wz-back{background:transparent;border:1px solid var(--white-12);color:var(--text-soft);border-radius:999px;padding:10px 16px;font:inherit;font-size:14px;cursor:pointer}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isTemplate } from '../../check/session-actions.js';
|
|
2
2
|
import { entryOf, storyAgents } from './file-story.js';
|
|
3
|
-
|
|
3
|
+
// A stop outranks a name seen, as a file's access does (`files.ts`, 2026-10-05).
|
|
4
|
+
const RANK = ['read', 'unknown', 'stopped', 'named'];
|
|
4
5
|
export function helperViews(report) {
|
|
5
6
|
const agentOf = storyAgents(report);
|
|
6
7
|
const helpers = [...report.graph.agents].sort((a, b) => (a.ordinal ?? Infinity) - (b.ordinal ?? Infinity) || a.index - b.index);
|