@opencode-cockpit/trust 0.0.0-stage → 0.8.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/LICENSE +21 -0
- package/README.md +241 -2
- package/dist/cli/preview.js +251 -0
- package/dist/core/adapt/seen.js +24 -0
- package/dist/core/adapt/v1.js +109 -0
- package/dist/core/adapt/v2.js +109 -0
- package/dist/core/config.js +93 -0
- package/dist/core/danger.js +366 -0
- package/dist/core/engine.js +245 -0
- package/dist/core/family.js +512 -0
- package/dist/core/history.js +169 -0
- package/dist/core/index.js +16 -0
- package/dist/core/keys.js +149 -0
- package/dist/core/ledger.js +202 -0
- package/dist/core/paths.js +38 -0
- package/dist/core/policy.js +128 -0
- package/dist/core/rules.js +152 -0
- package/dist/core/sample.js +311 -0
- package/dist/core/shell.js +253 -0
- package/dist/core/signature.js +38 -0
- package/dist/core/view/actions.js +321 -0
- package/dist/core/view/activity.js +557 -0
- package/dist/core/view/explorer.js +970 -0
- package/dist/core/view/model.js +193 -0
- package/dist/core/view/parts.js +290 -0
- package/dist/core/view/rows.js +165 -0
- package/dist/core/view/sidebar.js +170 -0
- package/dist/tui/index.js +920 -0
- package/dist/tui/journal.js +80 -0
- package/dist/tui/render.js +85 -0
- package/dist/tui/source.js +112 -0
- package/dist/tui/view/dialog.js +43 -0
- package/dist/tui/view/rows.js +69 -0
- package/package.json +61 -4
- package/tui.js +6 -0
- package/types/cli/preview.d.ts +22 -0
- package/types/core/adapt/seen.d.ts +46 -0
- package/types/core/adapt/v1.d.ts +18 -0
- package/types/core/adapt/v2.d.ts +20 -0
- package/types/core/config.d.ts +57 -0
- package/types/core/danger.d.ts +50 -0
- package/types/core/engine.d.ts +117 -0
- package/types/core/family.d.ts +97 -0
- package/types/core/history.d.ts +81 -0
- package/types/core/index.d.ts +16 -0
- package/types/core/keys.d.ts +68 -0
- package/types/core/ledger.d.ts +158 -0
- package/types/core/paths.d.ts +24 -0
- package/types/core/policy.d.ts +52 -0
- package/types/core/rules.d.ts +55 -0
- package/types/core/sample.d.ts +22 -0
- package/types/core/shell.d.ts +40 -0
- package/types/core/signature.d.ts +18 -0
- package/types/core/view/actions.d.ts +76 -0
- package/types/core/view/activity.d.ts +101 -0
- package/types/core/view/explorer.d.ts +154 -0
- package/types/core/view/model.d.ts +106 -0
- package/types/core/view/parts.d.ts +76 -0
- package/types/core/view/rows.d.ts +60 -0
- package/types/core/view/sidebar.d.ts +62 -0
- package/types/tui/index.d.ts +15 -0
- package/types/tui/journal.d.ts +19 -0
- package/types/tui/render.d.ts +14 -0
- package/types/tui/source.d.ts +35 -0
- package/types/tui/view/dialog.d.ts +24 -0
- package/types/tui/view/rows.d.ts +19 -0
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
2
|
+
/** @jsxImportSource @opentui/solid */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Trust's interface half — the only half it has. OpenCode's `permission.ask` server hook is declared
|
|
6
|
+
* and never called (docs/opencode/permissions.md), so a plugin answers a request the way OpenCode's
|
|
7
|
+
* own auto mode does: from the interface, on `permission.asked`, with a reply of "once".
|
|
8
|
+
*
|
|
9
|
+
* Everything that decides is in `core/`; this file wires it to the host: events in, a reply out, the
|
|
10
|
+
* ledger file read and appended, and the two surfaces — the sidebar block and the ledger dialog.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { claimFeature, duplicateFeatureMessage } from "@opencode-cockpit/client/feature";
|
|
14
|
+
import { bindingLookup, dualTui } from "@opencode-cockpit/client/host";
|
|
15
|
+
import { sidebarOrder } from "@opencode-cockpit/client/sidebar";
|
|
16
|
+
import { createSignal } from "solid-js";
|
|
17
|
+
import { commandOf } from "../core/adapt/seen.js";
|
|
18
|
+
import { loadTrustConfig, resolveSettings } from "../core/config.js";
|
|
19
|
+
import { createEngine } from "../core/engine.js";
|
|
20
|
+
import { trustPaths } from "../core/paths.js";
|
|
21
|
+
import { rulesFrom } from "../core/rules.js";
|
|
22
|
+
import { configSnippet, revoke, widen, widenScope } from "../core/view/actions.js";
|
|
23
|
+
import { activityRows, targetOf } from "../core/view/activity.js";
|
|
24
|
+
import { ALWAYS_KEY, explorerRows, nodeTarget, reveal } from "../core/view/explorer.js";
|
|
25
|
+
import { sidebarRows, tally } from "../core/view/sidebar.js";
|
|
26
|
+
import { createJournal } from "./journal.js";
|
|
27
|
+
import { createSource } from "./source.js";
|
|
28
|
+
import { Dialog } from "./view/dialog.js";
|
|
29
|
+
import { Rows } from "./view/rows.js";
|
|
30
|
+
const TRUST_PACKAGE = "@opencode-cockpit/trust";
|
|
31
|
+
|
|
32
|
+
/** `<leader>p`, for permissions: free on both OpenCodes (1.18.32's and 2.0.18's defaults) and in Cockpit. */
|
|
33
|
+
const DEFAULT_KEYS = {
|
|
34
|
+
"cockpit.trust.ledger": "<leader>p"
|
|
35
|
+
};
|
|
36
|
+
/** How often other windows' events are read from the ledger, and pending requests checked. */
|
|
37
|
+
const SYNC_MS = 3_000;
|
|
38
|
+
/** How long a bash request waits for its call's command line before it is decided without one. */
|
|
39
|
+
const PARK_MS = 1_000;
|
|
40
|
+
/** Calls remembered for their command line: far more than can be waiting at once. */
|
|
41
|
+
const CALLS_MAX = 500;
|
|
42
|
+
/** The host's dialog: as wide as xlarge allows (Shell's console measured it). */
|
|
43
|
+
const DIALOG_COLUMNS = 116;
|
|
44
|
+
|
|
45
|
+
/** Trust's interface half as a factory, so the `opencode-cockpit` bundle can include it. */
|
|
46
|
+
export function createTrustTui({
|
|
47
|
+
source = TRUST_PACKAGE
|
|
48
|
+
} = {}) {
|
|
49
|
+
return async (api, rawOptions) => {
|
|
50
|
+
const log = api.log.child("trust");
|
|
51
|
+
const claim = claimFeature(api.renderer, "trust", source);
|
|
52
|
+
if (!claim.active) {
|
|
53
|
+
log.warn("configured twice", {
|
|
54
|
+
owner: claim.owner,
|
|
55
|
+
skipped: source
|
|
56
|
+
});
|
|
57
|
+
api.ui.toast({
|
|
58
|
+
variant: "warning",
|
|
59
|
+
title: "Trust",
|
|
60
|
+
message: duplicateFeatureMessage("Trust", claim.owner, source)
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
api.lifecycle.onDispose(() => claim.release());
|
|
65
|
+
const directory = api.state.path.directory;
|
|
66
|
+
const config = await loadTrustConfig(directory, rawOptions);
|
|
67
|
+
const settings = resolveSettings(config);
|
|
68
|
+
if (!settings.enabled) {
|
|
69
|
+
log.info("off by config", {
|
|
70
|
+
directory
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const keys = bindingLookup({
|
|
75
|
+
...DEFAULT_KEYS,
|
|
76
|
+
...config.keybinds
|
|
77
|
+
});
|
|
78
|
+
const paths = trustPaths(directory);
|
|
79
|
+
const journal = createJournal(paths);
|
|
80
|
+
const engine = createEngine({
|
|
81
|
+
...settings,
|
|
82
|
+
keep: 20
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/** OpenCode's config as its `config.get` returned it; undefined until read, and Trust stays out. */
|
|
86
|
+
let opencodeConfig;
|
|
87
|
+
let rulesReady = false;
|
|
88
|
+
/** Something you should know about. The sidebar says it whatever else it has to say. */
|
|
89
|
+
let trouble;
|
|
90
|
+
const calls = new Map();
|
|
91
|
+
const agents = new Map();
|
|
92
|
+
|
|
93
|
+
// --- painting ----------------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
const [sidebarLines, setLines] = createSignal([]);
|
|
96
|
+
const [dialogRows, setDialogRows] = createSignal([]);
|
|
97
|
+
let block;
|
|
98
|
+
let drawnAt = 0;
|
|
99
|
+
let said = "";
|
|
100
|
+
const sidebarWidth = () => {
|
|
101
|
+
/** The container the host gave the block, as Subagents measures it: the block's own width follows its rows. */
|
|
102
|
+
const parent = block?.parent?.width ?? 0;
|
|
103
|
+
const own = block?.width ?? 0;
|
|
104
|
+
const measured = parent >= 12 ? Math.min(parent, own >= 12 ? own : parent) : own;
|
|
105
|
+
return measured >= 12 ? measured : Math.max(20, Math.min(40, Math.floor(api.renderer.width / 4) - 2));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The dialog: the activity `/trust` opens on, and the ledger behind `l`. Each keeps its own cursor
|
|
110
|
+
* by key, so a rule revoked or a family folded moves the rows about, not the cursor.
|
|
111
|
+
*/
|
|
112
|
+
const dialog = {
|
|
113
|
+
open: false,
|
|
114
|
+
view: "activity",
|
|
115
|
+
/** `?`: every key, in the body's place. */
|
|
116
|
+
keys: false,
|
|
117
|
+
notice: undefined,
|
|
118
|
+
activity: undefined,
|
|
119
|
+
node: undefined,
|
|
120
|
+
/** Families opened, and those whose tail is listed too; every family starts folded. */
|
|
121
|
+
opened: new Set(),
|
|
122
|
+
full: new Set(),
|
|
123
|
+
filter: "",
|
|
124
|
+
/** `/` pressed: the text typed so far, until enter or esc. */
|
|
125
|
+
typing: undefined,
|
|
126
|
+
/** `tab` into the card: the focused button. */
|
|
127
|
+
button: undefined
|
|
128
|
+
};
|
|
129
|
+
/** What was drawn last: actions and clicks act on what is on screen. */
|
|
130
|
+
let shown = {};
|
|
131
|
+
const reading = () => ({
|
|
132
|
+
state: engine.state,
|
|
133
|
+
settings,
|
|
134
|
+
now: Date.now(),
|
|
135
|
+
history: engine.history
|
|
136
|
+
});
|
|
137
|
+
const project = directory.split(/[\\/]/).filter(Boolean).at(-1) ?? "";
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Off by default (core/config.ts): the sidebar is crowded, and Trust answers the same without it.
|
|
141
|
+
* The palette flips it for the session; config decides where it starts. Not remembered across
|
|
142
|
+
* restarts — remembered UI state makes a command look dead (docs/opencode/gotchas.md).
|
|
143
|
+
*/
|
|
144
|
+
let inSidebar = settings.sidebar;
|
|
145
|
+
const paint = () => {
|
|
146
|
+
drawnAt = sidebarWidth();
|
|
147
|
+
/** Hidden, the block says nothing — except trouble: a failure always speaks. */
|
|
148
|
+
const next = !inSidebar && !trouble ? [] : sidebarRows({
|
|
149
|
+
width: drawnAt,
|
|
150
|
+
recent: engine.recent(),
|
|
151
|
+
count: engine.count(),
|
|
152
|
+
pending: engine.pending(),
|
|
153
|
+
state: engine.state,
|
|
154
|
+
limit: settings.sidebarRows,
|
|
155
|
+
shown: inSidebar,
|
|
156
|
+
...(inSidebar ? {
|
|
157
|
+
project: tally(engine.state, settings, Date.now())
|
|
158
|
+
} : {}),
|
|
159
|
+
...(trouble ? {
|
|
160
|
+
trouble
|
|
161
|
+
} : {})
|
|
162
|
+
});
|
|
163
|
+
/** Only when they changed: new rows rebuild every line of the block. */
|
|
164
|
+
const text = JSON.stringify(next);
|
|
165
|
+
if (text !== said) {
|
|
166
|
+
said = text;
|
|
167
|
+
setLines(next);
|
|
168
|
+
}
|
|
169
|
+
if (dialog.open) {
|
|
170
|
+
const height = api.renderer.height;
|
|
171
|
+
const size = {
|
|
172
|
+
width: Math.max(40, Math.min(DIALOG_COLUMNS, api.renderer.width - 2)),
|
|
173
|
+
height: Math.max(11, height - Math.floor(height / 4) * 2),
|
|
174
|
+
project,
|
|
175
|
+
...reading(),
|
|
176
|
+
...(dialog.notice ? {
|
|
177
|
+
notice: dialog.notice
|
|
178
|
+
} : {}),
|
|
179
|
+
...(dialog.keys ? {
|
|
180
|
+
keys: true
|
|
181
|
+
} : {})
|
|
182
|
+
};
|
|
183
|
+
if (dialog.view === "activity") {
|
|
184
|
+
const view = activityRows({
|
|
185
|
+
...size,
|
|
186
|
+
...(dialog.activity !== undefined ? {
|
|
187
|
+
selected: dialog.activity
|
|
188
|
+
} : {})
|
|
189
|
+
});
|
|
190
|
+
dialog.activity = view.item?.key;
|
|
191
|
+
shown = {
|
|
192
|
+
activity: view
|
|
193
|
+
};
|
|
194
|
+
setDialogRows(view.rows);
|
|
195
|
+
} else {
|
|
196
|
+
const view = explorerRows({
|
|
197
|
+
...size,
|
|
198
|
+
open: dialog.opened,
|
|
199
|
+
full: dialog.full,
|
|
200
|
+
filter: dialog.filter,
|
|
201
|
+
...(dialog.node !== undefined ? {
|
|
202
|
+
selected: dialog.node
|
|
203
|
+
} : {}),
|
|
204
|
+
...(dialog.button !== undefined ? {
|
|
205
|
+
focus: {
|
|
206
|
+
button: dialog.button
|
|
207
|
+
}
|
|
208
|
+
} : {}),
|
|
209
|
+
...(dialog.typing !== undefined ? {
|
|
210
|
+
typing: dialog.typing
|
|
211
|
+
} : {})
|
|
212
|
+
});
|
|
213
|
+
dialog.node = view.node?.key;
|
|
214
|
+
if (dialog.button !== undefined && view.buttons.length > 0) dialog.button = Math.min(dialog.button, view.buttons.length - 1);else dialog.button = undefined;
|
|
215
|
+
shown = {
|
|
216
|
+
ledger: view
|
|
217
|
+
};
|
|
218
|
+
setDialogRows(view.rows);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
api.renderer.requestRender();
|
|
222
|
+
};
|
|
223
|
+
/** Several changes in one turn are one paint (Shell's painter). */
|
|
224
|
+
let scheduled = false;
|
|
225
|
+
const draw = () => {
|
|
226
|
+
if (scheduled) return;
|
|
227
|
+
scheduled = true;
|
|
228
|
+
setTimeout(() => {
|
|
229
|
+
scheduled = false;
|
|
230
|
+
try {
|
|
231
|
+
paint();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
log.error("paint failed", {
|
|
234
|
+
error
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}, 0);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// --- the ledger file ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
/** Everything new in the file — this window's events and every other's — into the state. */
|
|
243
|
+
const sync = () => journal.read().then(({
|
|
244
|
+
events,
|
|
245
|
+
reset
|
|
246
|
+
}) => {
|
|
247
|
+
if (events.length === 0 && !reset) return;
|
|
248
|
+
engine.load(events, {
|
|
249
|
+
reset
|
|
250
|
+
});
|
|
251
|
+
draw();
|
|
252
|
+
}).catch(error => {
|
|
253
|
+
log.error("ledger unreadable", {
|
|
254
|
+
file: paths.events,
|
|
255
|
+
error
|
|
256
|
+
});
|
|
257
|
+
trouble = "ledger unreadable — see cockpit.log";
|
|
258
|
+
draw();
|
|
259
|
+
});
|
|
260
|
+
const write = events => {
|
|
261
|
+
if (events.length === 0) return;
|
|
262
|
+
journal.append(events).then(() => {
|
|
263
|
+
if (trouble?.startsWith("ledger not saved")) trouble = undefined;
|
|
264
|
+
return sync();
|
|
265
|
+
}).catch(error => {
|
|
266
|
+
log.error("ledger not saved", {
|
|
267
|
+
file: paths.events,
|
|
268
|
+
error
|
|
269
|
+
});
|
|
270
|
+
trouble = `ledger not saved: ${error.code ?? "error"}`;
|
|
271
|
+
draw();
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// --- requests ----------------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
const loadRules = async () => {
|
|
278
|
+
try {
|
|
279
|
+
opencodeConfig = await feed.config();
|
|
280
|
+
rulesReady = true;
|
|
281
|
+
if (trouble?.startsWith("OpenCode's config")) trouble = undefined;
|
|
282
|
+
log.debug("rules", {
|
|
283
|
+
rules: rulesFrom(opencodeConfig).length
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
/** Without the rules there is no knowing what you asked to be asked about: Trust stays out. */
|
|
287
|
+
rulesReady = false;
|
|
288
|
+
log.error("config unreadable", {
|
|
289
|
+
error
|
|
290
|
+
});
|
|
291
|
+
trouble = "OpenCode's config unreadable — not answering";
|
|
292
|
+
}
|
|
293
|
+
draw();
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Bash requests whose call has not said its command line yet, by call id. Measured on 1.18.32: the
|
|
298
|
+
* call's `running` update — the one carrying `command` — arrived *after* `permission.asked` for
|
|
299
|
+
* three requests in four (the first call of a turn was the exception). So a request without its
|
|
300
|
+
* line waits for it, briefly; one that never gets it is decided without, which means asked.
|
|
301
|
+
*/
|
|
302
|
+
const parked = new Map();
|
|
303
|
+
const lineOf = request => request.call ? calls.get(request.call) ?? feed.call(request) : undefined;
|
|
304
|
+
const asked = (request, at) => {
|
|
305
|
+
/**
|
|
306
|
+
* Decided as soon as the request can be read, never after a file read: OpenCode's `--auto`
|
|
307
|
+
* answers in 15–22ms, and a decision that waited on the disk could answer a request already gone.
|
|
308
|
+
*/
|
|
309
|
+
if (!rulesReady) return;
|
|
310
|
+
const call = lineOf(request);
|
|
311
|
+
if (request.permission === "bash" && call?.line === undefined && request.call && !parked.has(request.call)) {
|
|
312
|
+
const key = request.call;
|
|
313
|
+
parked.set(key, {
|
|
314
|
+
request,
|
|
315
|
+
at,
|
|
316
|
+
timer: setTimeout(() => unpark(key), PARK_MS)
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
decideNow(request, at, call);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/** The call's line arrived, a reply came first, or the wait ran out: decide with what is known. */
|
|
324
|
+
const unpark = key => {
|
|
325
|
+
const waiting = parked.get(key);
|
|
326
|
+
if (!waiting) return;
|
|
327
|
+
parked.delete(key);
|
|
328
|
+
clearTimeout(waiting.timer);
|
|
329
|
+
if (!lineOf(waiting.request)) log.debug("no command line", {
|
|
330
|
+
request: waiting.request.id,
|
|
331
|
+
call: key
|
|
332
|
+
});
|
|
333
|
+
decideNow(waiting.request, waiting.at, lineOf(waiting.request));
|
|
334
|
+
};
|
|
335
|
+
const decideNow = (request, at, call) => {
|
|
336
|
+
const agent = agents.get(request.sessionID) ?? feed.agent(request.sessionID, request.messageID) ?? "unknown";
|
|
337
|
+
const {
|
|
338
|
+
judgement,
|
|
339
|
+
event
|
|
340
|
+
} = engine.ask({
|
|
341
|
+
request,
|
|
342
|
+
context: {
|
|
343
|
+
...call,
|
|
344
|
+
root: directory
|
|
345
|
+
},
|
|
346
|
+
agent,
|
|
347
|
+
rules: rulesFrom(opencodeConfig, agent),
|
|
348
|
+
at
|
|
349
|
+
});
|
|
350
|
+
log.debug("asked", {
|
|
351
|
+
request: request.id,
|
|
352
|
+
permission: request.permission,
|
|
353
|
+
agent,
|
|
354
|
+
why: judgement.why
|
|
355
|
+
});
|
|
356
|
+
write([event]);
|
|
357
|
+
draw();
|
|
358
|
+
if (!judgement.answer) return;
|
|
359
|
+
feed.approve(request).then(() => {
|
|
360
|
+
const auto = engine.answered(request.id, Date.now());
|
|
361
|
+
if (auto) write([auto]);
|
|
362
|
+
log.info("auto", {
|
|
363
|
+
request: request.id,
|
|
364
|
+
session: request.sessionID,
|
|
365
|
+
call: request.call,
|
|
366
|
+
permission: request.permission,
|
|
367
|
+
agent,
|
|
368
|
+
subjects: judgement.items.map(item => item.subject),
|
|
369
|
+
why: judgement.why,
|
|
370
|
+
ms: Date.now() - at
|
|
371
|
+
});
|
|
372
|
+
if (trouble?.startsWith("an answer failed")) trouble = undefined;
|
|
373
|
+
draw();
|
|
374
|
+
}).catch(error => {
|
|
375
|
+
/** The prompt is still there and yours: your answer to it counts as any other. */
|
|
376
|
+
engine.failed(request.id);
|
|
377
|
+
log.warn("auto reply failed", {
|
|
378
|
+
request: request.id,
|
|
379
|
+
error
|
|
380
|
+
});
|
|
381
|
+
trouble = `an answer failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
382
|
+
draw();
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
const feed = createSource(api, log, seen => {
|
|
386
|
+
const at = Date.now();
|
|
387
|
+
for (const each of seen) {
|
|
388
|
+
switch (each.type) {
|
|
389
|
+
case "call":
|
|
390
|
+
{
|
|
391
|
+
calls.set(each.call, commandOf(each.input));
|
|
392
|
+
if (calls.size > CALLS_MAX) calls.delete(calls.keys().next().value);
|
|
393
|
+
if (parked.has(each.call) && calls.get(each.call)?.line !== undefined) unpark(each.call);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
case "agent":
|
|
397
|
+
agents.set(each.sessionID, each.agent);
|
|
398
|
+
break;
|
|
399
|
+
case "config":
|
|
400
|
+
void loadRules();
|
|
401
|
+
break;
|
|
402
|
+
case "asked":
|
|
403
|
+
asked(each.request, at);
|
|
404
|
+
break;
|
|
405
|
+
case "replied":
|
|
406
|
+
{
|
|
407
|
+
/** Answered while it waited for its line: decided first, so the answer counts against it. */
|
|
408
|
+
for (const [key, waiting] of parked) if (waiting.request.id === each.requestID) unpark(key);
|
|
409
|
+
const {
|
|
410
|
+
events,
|
|
411
|
+
credit
|
|
412
|
+
} = engine.replied({
|
|
413
|
+
requestID: each.requestID,
|
|
414
|
+
reply: each.reply,
|
|
415
|
+
at
|
|
416
|
+
});
|
|
417
|
+
if (credit.kind === "ignored") log.debug("reply not counted", {
|
|
418
|
+
request: each.requestID,
|
|
419
|
+
why: credit.why
|
|
420
|
+
});
|
|
421
|
+
write(events);
|
|
422
|
+
draw();
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
/** Requests already waiting when Trust started, and any the events never told us were answered. */
|
|
430
|
+
const reconcile = async adopt => {
|
|
431
|
+
const listed = await feed.pending().catch(error => {
|
|
432
|
+
log.debug("pending list failed", {
|
|
433
|
+
error
|
|
434
|
+
});
|
|
435
|
+
return undefined;
|
|
436
|
+
});
|
|
437
|
+
if (!listed) return;
|
|
438
|
+
const ids = new Set();
|
|
439
|
+
for (const each of listed) {
|
|
440
|
+
if (each.type !== "asked") continue;
|
|
441
|
+
ids.add(each.request.id);
|
|
442
|
+
/** Its asking was not seen, so "now" stands in for it: a person answers later still. */
|
|
443
|
+
if (adopt) asked(each.request, Date.now());
|
|
444
|
+
}
|
|
445
|
+
engine.reconcile(ids, Date.now());
|
|
446
|
+
draw();
|
|
447
|
+
};
|
|
448
|
+
const boot = async () => {
|
|
449
|
+
await sync();
|
|
450
|
+
await loadRules();
|
|
451
|
+
await reconcile(true);
|
|
452
|
+
log.info("ready", {
|
|
453
|
+
ledger: paths.events,
|
|
454
|
+
threshold: settings.threshold,
|
|
455
|
+
dangerExtra: settings.dangerExtra,
|
|
456
|
+
expireDays: settings.expireDays,
|
|
457
|
+
rules: rulesReady
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
void boot();
|
|
461
|
+
let ticking = setInterval(() => {
|
|
462
|
+
void sync();
|
|
463
|
+
if (engine.pending().length > 0) void reconcile(false);
|
|
464
|
+
/** The sidebar was laid out, or resized, since the rows were drawn. */
|
|
465
|
+
if (sidebarWidth() !== drawnAt) draw();
|
|
466
|
+
}, SYNC_MS);
|
|
467
|
+
|
|
468
|
+
// --- the ledger dialog -------------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
const notice = (text, tone = "muted") => {
|
|
471
|
+
dialog.notice = {
|
|
472
|
+
text,
|
|
473
|
+
tone
|
|
474
|
+
};
|
|
475
|
+
draw();
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
/** What `x`, `w` and `c` act on: the cursor's item or node, as last drawn. */
|
|
479
|
+
const selected = () => {
|
|
480
|
+
if (dialog.view === "activity") {
|
|
481
|
+
const view = shown.activity;
|
|
482
|
+
if (!view?.item) return undefined;
|
|
483
|
+
return {
|
|
484
|
+
target: targetOf(view.item, view.model),
|
|
485
|
+
families: view.model.families
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const view = shown.ledger;
|
|
489
|
+
if (!view?.node) return undefined;
|
|
490
|
+
return {
|
|
491
|
+
target: nodeTarget(view.node),
|
|
492
|
+
families: view.model.families
|
|
493
|
+
};
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
/** What `w` and `x` decided, appended and said. Nothing is ever rewritten in the ledger. */
|
|
497
|
+
const act = (name, outcome) => {
|
|
498
|
+
write(outcome.events);
|
|
499
|
+
if (outcome.events.length > 0) log.info(name, {
|
|
500
|
+
events: outcome.events.map(event => event.type === "revoked" ? {
|
|
501
|
+
type: event.type,
|
|
502
|
+
agent: event.agent,
|
|
503
|
+
subject: event.subject
|
|
504
|
+
} : event.type === "widened" || event.type === "unwidened" ? {
|
|
505
|
+
type: event.type,
|
|
506
|
+
agent: event.agent,
|
|
507
|
+
family: event.family
|
|
508
|
+
} : {
|
|
509
|
+
type: event.type
|
|
510
|
+
})
|
|
511
|
+
});
|
|
512
|
+
notice(outcome.notice.text, outcome.notice.tone);
|
|
513
|
+
};
|
|
514
|
+
const revokeSelected = () => {
|
|
515
|
+
const now = selected();
|
|
516
|
+
if (now) act("revoked", revoke(now.target, reading(), Date.now()));
|
|
517
|
+
};
|
|
518
|
+
const widenSelected = () => {
|
|
519
|
+
const now = selected();
|
|
520
|
+
if (now) act("widen", widen(widenScope(now.target, now.families), now.families, Date.now()));
|
|
521
|
+
};
|
|
522
|
+
const copy = () => {
|
|
523
|
+
const now = selected();
|
|
524
|
+
if (!now) return;
|
|
525
|
+
const snippet = configSnippet(now.target);
|
|
526
|
+
const ok = api.renderer.copyToClipboardOSC52?.(snippet.text) ?? false;
|
|
527
|
+
notice(ok ? `Copied ${snippet.text} — paste it into opencode.json${snippet.note ? `; ${snippet.note}` : ""}.` : `This terminal refused the clipboard. The rule: ${snippet.text}`, ok ? "success" : "warning");
|
|
528
|
+
};
|
|
529
|
+
const togglePause = () => {
|
|
530
|
+
const paused = !engine.state.paused;
|
|
531
|
+
write([{
|
|
532
|
+
v: 1,
|
|
533
|
+
at: Date.now(),
|
|
534
|
+
type: paused ? "paused" : "resumed"
|
|
535
|
+
}]);
|
|
536
|
+
log.info(paused ? "paused" : "resumed", {
|
|
537
|
+
directory
|
|
538
|
+
});
|
|
539
|
+
if (dialog.open) notice(paused ? "Paused in this project: Trust keeps counting, and answers nothing." : "Resumed.");else api.ui.toast({
|
|
540
|
+
variant: "info",
|
|
541
|
+
title: "Trust",
|
|
542
|
+
message: paused ? "Paused in this project." : "Answering again in this project."
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
/** Every key but `?` acts on the screen: from the key list it goes back to the screen first. */
|
|
547
|
+
const listed = run => () => {
|
|
548
|
+
dialog.keys = false;
|
|
549
|
+
run();
|
|
550
|
+
draw();
|
|
551
|
+
};
|
|
552
|
+
const move = by => {
|
|
553
|
+
dialog.notice = undefined;
|
|
554
|
+
if (dialog.view === "activity") {
|
|
555
|
+
const items = shown.activity?.model.items ?? [];
|
|
556
|
+
const at = Math.max(0, items.findIndex(item => item.key === dialog.activity));
|
|
557
|
+
dialog.activity = items[Math.max(0, Math.min(items.length - 1, at + by))]?.key;
|
|
558
|
+
} else {
|
|
559
|
+
const nodes = shown.ledger?.model.nodes ?? [];
|
|
560
|
+
const at = Math.max(0, nodes.findIndex(node => node.key === dialog.node));
|
|
561
|
+
dialog.node = nodes[Math.max(0, Math.min(nodes.length - 1, at + by))]?.key;
|
|
562
|
+
dialog.button = undefined;
|
|
563
|
+
}
|
|
564
|
+
draw();
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
/** Into the ledger, on `key` when given; the activity keeps its cursor for the way back. */
|
|
568
|
+
const toLedger = key => {
|
|
569
|
+
dialog.view = "ledger";
|
|
570
|
+
dialog.button = undefined;
|
|
571
|
+
dialog.notice = undefined;
|
|
572
|
+
if (key !== undefined) dialog.node = key;
|
|
573
|
+
draw();
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
/** `enter` on the activity: why — the card of the rule that answered, or that is close, in the ledger. */
|
|
577
|
+
const why = () => {
|
|
578
|
+
const view = shown.activity;
|
|
579
|
+
const item = view?.item;
|
|
580
|
+
if (!view || !item) return toLedger();
|
|
581
|
+
if (item.kind === "always") return toLedger(ALWAYS_KEY);
|
|
582
|
+
const subject = item.kind === "answer" ? {
|
|
583
|
+
permission: item.answer.permission,
|
|
584
|
+
subject: item.answer.items[0]?.subject ?? ""
|
|
585
|
+
} : {
|
|
586
|
+
permission: item.command.permission,
|
|
587
|
+
subject: item.command.subject
|
|
588
|
+
};
|
|
589
|
+
toLedger(reveal({
|
|
590
|
+
open: dialog.opened,
|
|
591
|
+
full: dialog.full
|
|
592
|
+
}, view.model.families, subject));
|
|
593
|
+
};
|
|
594
|
+
const node = () => shown.ledger?.node;
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* `space`/`enter` on a heading opens or folds it; on `+ N more` it lists the rest; on a command in a
|
|
598
|
+
* family it folds the family, the cursor going to its heading. `→` only opens, `←` only folds.
|
|
599
|
+
*/
|
|
600
|
+
const fold = (way = "toggle") => {
|
|
601
|
+
const at = node();
|
|
602
|
+
if (!at || at.kind === "always") return;
|
|
603
|
+
dialog.notice = undefined;
|
|
604
|
+
const key = at.family.key;
|
|
605
|
+
if (at.kind === "more") {
|
|
606
|
+
if (way !== "close") dialog.full.add(key);else {
|
|
607
|
+
dialog.opened.delete(key);
|
|
608
|
+
dialog.node = `f:${key}`;
|
|
609
|
+
}
|
|
610
|
+
} else if (at.kind === "command") {
|
|
611
|
+
if (!at.nested || way === "open") return;
|
|
612
|
+
dialog.opened.delete(key);
|
|
613
|
+
dialog.full.delete(key);
|
|
614
|
+
dialog.node = `f:${key}`;
|
|
615
|
+
} else if (dialog.opened.has(key) && way !== "open") {
|
|
616
|
+
dialog.opened.delete(key);
|
|
617
|
+
dialog.full.delete(key);
|
|
618
|
+
} else if (!dialog.opened.has(key) && way !== "close") dialog.opened.add(key);
|
|
619
|
+
draw();
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
/** The card's buttons: `tab` in and out, `←`/`→` between them, `enter` presses the focused one. */
|
|
623
|
+
const buttons = () => shown.ledger?.buttons ?? [];
|
|
624
|
+
const press = action => {
|
|
625
|
+
if (action === "revoke") revokeSelected();else if (action === "widen") widenSelected();else if (action === "copy") copy();else if (action === "ledger") toLedger();
|
|
626
|
+
};
|
|
627
|
+
const enter = () => {
|
|
628
|
+
if (dialog.view === "activity") return why();
|
|
629
|
+
if (dialog.button !== undefined) {
|
|
630
|
+
const button = buttons()[dialog.button];
|
|
631
|
+
if (button && !button.off) press(button.action);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const at = node();
|
|
635
|
+
if (at?.kind === "command" || at?.kind === "always") {
|
|
636
|
+
if (buttons().length > 0) dialog.button = 0;
|
|
637
|
+
return draw();
|
|
638
|
+
}
|
|
639
|
+
fold();
|
|
640
|
+
};
|
|
641
|
+
const sideways = by => {
|
|
642
|
+
if (dialog.view !== "ledger") return;
|
|
643
|
+
if (dialog.button !== undefined) {
|
|
644
|
+
const count = buttons().length;
|
|
645
|
+
dialog.button = count > 0 ? (dialog.button + by + count) % count : undefined;
|
|
646
|
+
return draw();
|
|
647
|
+
}
|
|
648
|
+
fold(by > 0 ? "open" : "close");
|
|
649
|
+
};
|
|
650
|
+
const tab = () => {
|
|
651
|
+
if (dialog.view !== "ledger") return;
|
|
652
|
+
dialog.button = dialog.button === undefined && buttons().length > 0 ? 0 : undefined;
|
|
653
|
+
draw();
|
|
654
|
+
};
|
|
655
|
+
const filter = () => {
|
|
656
|
+
if (dialog.view === "activity") dialog.view = "ledger";
|
|
657
|
+
dialog.typing = dialog.filter;
|
|
658
|
+
dialog.button = undefined;
|
|
659
|
+
draw();
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
/** A click: a button presses, a row takes the cursor. */
|
|
663
|
+
const click = (x, y) => {
|
|
664
|
+
const hits = (dialog.view === "activity" ? shown.activity?.hits : shown.ledger?.hits) ?? [];
|
|
665
|
+
const on = hits.find(hit => hit.y === y && (hit.x0 === undefined || x >= hit.x0) && (hit.x1 === undefined || x < hit.x1));
|
|
666
|
+
if (!on) return;
|
|
667
|
+
dialog.keys = false;
|
|
668
|
+
if (on.kind === "button") {
|
|
669
|
+
dialog.notice = undefined;
|
|
670
|
+
return press(on.action);
|
|
671
|
+
}
|
|
672
|
+
dialog.notice = undefined;
|
|
673
|
+
if (dialog.view === "activity") dialog.activity = on.key;else {
|
|
674
|
+
dialog.node = on.key;
|
|
675
|
+
dialog.button = undefined;
|
|
676
|
+
}
|
|
677
|
+
draw();
|
|
678
|
+
};
|
|
679
|
+
const dialogLayer = () => ({
|
|
680
|
+
priority: 100,
|
|
681
|
+
commands: [{
|
|
682
|
+
name: "cockpit.trust.down",
|
|
683
|
+
title: "Next",
|
|
684
|
+
run: listed(() => move(1))
|
|
685
|
+
}, {
|
|
686
|
+
name: "cockpit.trust.up",
|
|
687
|
+
title: "Previous",
|
|
688
|
+
run: listed(() => move(-1))
|
|
689
|
+
}, {
|
|
690
|
+
name: "cockpit.trust.enter",
|
|
691
|
+
title: "Why, open, or press",
|
|
692
|
+
run: listed(() => enter())
|
|
693
|
+
}, {
|
|
694
|
+
name: "cockpit.trust.fold",
|
|
695
|
+
title: "Open or fold a family",
|
|
696
|
+
run: listed(() => fold())
|
|
697
|
+
}, {
|
|
698
|
+
name: "cockpit.trust.right",
|
|
699
|
+
title: "Open a family, or the next button",
|
|
700
|
+
run: listed(() => sideways(1))
|
|
701
|
+
}, {
|
|
702
|
+
name: "cockpit.trust.left",
|
|
703
|
+
title: "Fold a family, or the previous button",
|
|
704
|
+
run: listed(() => sideways(-1))
|
|
705
|
+
}, {
|
|
706
|
+
name: "cockpit.trust.l",
|
|
707
|
+
title: "Open the ledger, or a family in it",
|
|
708
|
+
run: listed(() => dialog.view === "activity" ? toLedger() : sideways(1))
|
|
709
|
+
}, {
|
|
710
|
+
name: "cockpit.trust.tab",
|
|
711
|
+
title: "Into the card and back",
|
|
712
|
+
run: listed(() => tab())
|
|
713
|
+
}, {
|
|
714
|
+
name: "cockpit.trust.filter",
|
|
715
|
+
title: "Filter the ledger",
|
|
716
|
+
run: listed(() => filter())
|
|
717
|
+
}, {
|
|
718
|
+
name: "cockpit.trust.keys",
|
|
719
|
+
title: "Show or hide every key",
|
|
720
|
+
run: () => {
|
|
721
|
+
dialog.keys = !dialog.keys;
|
|
722
|
+
dialog.notice = undefined;
|
|
723
|
+
draw();
|
|
724
|
+
}
|
|
725
|
+
}, {
|
|
726
|
+
name: "cockpit.trust.revoke",
|
|
727
|
+
title: "Revoke, or forget a count",
|
|
728
|
+
run: listed(() => revokeSelected())
|
|
729
|
+
}, {
|
|
730
|
+
name: "cockpit.trust.widen",
|
|
731
|
+
title: "Trust the whole family, or undo it",
|
|
732
|
+
run: listed(() => widenSelected())
|
|
733
|
+
}, {
|
|
734
|
+
name: "cockpit.trust.copy",
|
|
735
|
+
title: "Copy as config",
|
|
736
|
+
run: listed(() => copy())
|
|
737
|
+
}, {
|
|
738
|
+
name: "cockpit.trust.togglePause",
|
|
739
|
+
title: "Pause or resume",
|
|
740
|
+
run: listed(() => togglePause())
|
|
741
|
+
}, {
|
|
742
|
+
name: "cockpit.trust.close",
|
|
743
|
+
title: "Close",
|
|
744
|
+
run: () => api.ui.dialog.clear()
|
|
745
|
+
}],
|
|
746
|
+
bindings: [{
|
|
747
|
+
key: "j,down",
|
|
748
|
+
cmd: "cockpit.trust.down"
|
|
749
|
+
}, {
|
|
750
|
+
key: "k,up",
|
|
751
|
+
cmd: "cockpit.trust.up"
|
|
752
|
+
}, {
|
|
753
|
+
key: "return",
|
|
754
|
+
cmd: "cockpit.trust.enter"
|
|
755
|
+
}, {
|
|
756
|
+
key: "space",
|
|
757
|
+
cmd: "cockpit.trust.fold"
|
|
758
|
+
}, {
|
|
759
|
+
key: "right",
|
|
760
|
+
cmd: "cockpit.trust.right"
|
|
761
|
+
}, {
|
|
762
|
+
key: "h,left",
|
|
763
|
+
cmd: "cockpit.trust.left"
|
|
764
|
+
}, {
|
|
765
|
+
key: "l",
|
|
766
|
+
cmd: "cockpit.trust.l"
|
|
767
|
+
}, {
|
|
768
|
+
key: "tab",
|
|
769
|
+
cmd: "cockpit.trust.tab"
|
|
770
|
+
}, {
|
|
771
|
+
key: "/",
|
|
772
|
+
cmd: "cockpit.trust.filter"
|
|
773
|
+
}, {
|
|
774
|
+
key: "?,shift+/",
|
|
775
|
+
cmd: "cockpit.trust.keys"
|
|
776
|
+
}, {
|
|
777
|
+
key: "x",
|
|
778
|
+
cmd: "cockpit.trust.revoke"
|
|
779
|
+
}, {
|
|
780
|
+
key: "w",
|
|
781
|
+
cmd: "cockpit.trust.widen"
|
|
782
|
+
}, {
|
|
783
|
+
key: "c",
|
|
784
|
+
cmd: "cockpit.trust.copy"
|
|
785
|
+
}, {
|
|
786
|
+
key: "p",
|
|
787
|
+
cmd: "cockpit.trust.togglePause"
|
|
788
|
+
}, {
|
|
789
|
+
key: "q",
|
|
790
|
+
cmd: "cockpit.trust.close"
|
|
791
|
+
}]
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Ahead of the keymap, because the host's dialog takes `esc` before any layer hears it (Shell's
|
|
796
|
+
* search does the same): the filter being typed gets every key; and `esc` steps back one thing at
|
|
797
|
+
* a time — the key list, the card's buttons, the filter, the ledger — before the host closes the
|
|
798
|
+
* dialog from the activity, as it always has.
|
|
799
|
+
*/
|
|
800
|
+
api.lifecycle.onDispose(api.keymap.intercept(ctx => {
|
|
801
|
+
if (!dialog.open) return;
|
|
802
|
+
const event = ctx.event;
|
|
803
|
+
if (dialog.typing !== undefined) {
|
|
804
|
+
ctx.consume({
|
|
805
|
+
preventDefault: true,
|
|
806
|
+
stopPropagation: true
|
|
807
|
+
});
|
|
808
|
+
if (event.name === "escape") dialog.typing = undefined;else if (event.name === "return" || event.name === "enter") {
|
|
809
|
+
dialog.filter = dialog.typing.trim();
|
|
810
|
+
dialog.typing = undefined;
|
|
811
|
+
dialog.node = undefined;
|
|
812
|
+
} else if (event.name === "backspace") dialog.typing = dialog.typing.slice(0, -1);else if (event.sequence && !event.ctrl && !event.meta && event.sequence >= " ") dialog.typing += event.sequence;
|
|
813
|
+
return draw();
|
|
814
|
+
}
|
|
815
|
+
if (event.name !== "escape") return;
|
|
816
|
+
if (dialog.keys) dialog.keys = false;else if (dialog.view === "activity") return;else if (dialog.button !== undefined) dialog.button = undefined;else if (dialog.filter !== "") {
|
|
817
|
+
dialog.filter = "";
|
|
818
|
+
dialog.node = undefined;
|
|
819
|
+
} else dialog.view = "activity";
|
|
820
|
+
ctx.consume({
|
|
821
|
+
preventDefault: true,
|
|
822
|
+
stopPropagation: true
|
|
823
|
+
});
|
|
824
|
+
dialog.notice = undefined;
|
|
825
|
+
draw();
|
|
826
|
+
}, {
|
|
827
|
+
priority: 10_000
|
|
828
|
+
}));
|
|
829
|
+
const openLedger = () => {
|
|
830
|
+
/** Config may have changed since: what the dialog says about "ask" rules should be today's. */
|
|
831
|
+
void loadRules();
|
|
832
|
+
Object.assign(dialog, {
|
|
833
|
+
open: true,
|
|
834
|
+
view: "activity",
|
|
835
|
+
keys: false,
|
|
836
|
+
notice: undefined,
|
|
837
|
+
activity: undefined,
|
|
838
|
+
typing: undefined,
|
|
839
|
+
button: undefined
|
|
840
|
+
});
|
|
841
|
+
paint();
|
|
842
|
+
api.ui.dialog.replace(() => _$createComponent(Dialog, {
|
|
843
|
+
api: api,
|
|
844
|
+
rows: dialogRows,
|
|
845
|
+
keys: dialogLayer,
|
|
846
|
+
onScroll: by => listed(() => move(by))(),
|
|
847
|
+
onClick: (x, y) => click(x, y)
|
|
848
|
+
}), () => {
|
|
849
|
+
dialog.open = false;
|
|
850
|
+
dialog.typing = undefined;
|
|
851
|
+
});
|
|
852
|
+
api.ui.dialog.setSize("xlarge");
|
|
853
|
+
log.debug("trust: open", {
|
|
854
|
+
items: shown.activity?.model.items.length ?? 0
|
|
855
|
+
});
|
|
856
|
+
};
|
|
857
|
+
api.keymap.registerLayer({
|
|
858
|
+
commands: [{
|
|
859
|
+
name: "cockpit.trust.ledger",
|
|
860
|
+
title: "Show what Trust answers for you",
|
|
861
|
+
category: "Cockpit · Trust",
|
|
862
|
+
namespace: "palette",
|
|
863
|
+
slashName: "trust",
|
|
864
|
+
run: () => openLedger()
|
|
865
|
+
}, {
|
|
866
|
+
name: "cockpit.trust.sidebar",
|
|
867
|
+
title: "Show or hide Trust in the sidebar",
|
|
868
|
+
desc: "for this session",
|
|
869
|
+
category: "Cockpit · Trust",
|
|
870
|
+
namespace: "palette",
|
|
871
|
+
run: () => {
|
|
872
|
+
inSidebar = !inSidebar;
|
|
873
|
+
log.debug("sidebar", {
|
|
874
|
+
shown: inSidebar
|
|
875
|
+
});
|
|
876
|
+
paint();
|
|
877
|
+
/** Said as well as drawn: on the home screen there is no sidebar to show it in. */
|
|
878
|
+
api.ui.toast({
|
|
879
|
+
variant: "info",
|
|
880
|
+
title: "Trust",
|
|
881
|
+
message: inSidebar ? "Shown in the sidebar." : "Hidden from the sidebar."
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}, {
|
|
885
|
+
name: "cockpit.trust.pause",
|
|
886
|
+
title: "Pause or resume Trust in this project",
|
|
887
|
+
category: "Cockpit · Trust",
|
|
888
|
+
namespace: "palette",
|
|
889
|
+
run: () => togglePause()
|
|
890
|
+
}],
|
|
891
|
+
bindings: keys.gather("cockpit", Object.keys(DEFAULT_KEYS))
|
|
892
|
+
});
|
|
893
|
+
api.slots.register({
|
|
894
|
+
/** Between Subagents (150) and the shells (170) by default; lower draws first. */
|
|
895
|
+
order: sidebarOrder("trust", 160, config.sidebarOrder, {
|
|
896
|
+
directory
|
|
897
|
+
}),
|
|
898
|
+
slots: {
|
|
899
|
+
sidebar_content: () => _$createComponent(Rows, {
|
|
900
|
+
api: api,
|
|
901
|
+
rows: sidebarLines,
|
|
902
|
+
onReady: box => {
|
|
903
|
+
block = box;
|
|
904
|
+
draw();
|
|
905
|
+
}
|
|
906
|
+
})
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
api.lifecycle.onDispose(() => {
|
|
910
|
+
for (const waiting of parked.values()) clearTimeout(waiting.timer);
|
|
911
|
+
parked.clear();
|
|
912
|
+
clearInterval(ticking);
|
|
913
|
+
ticking = undefined;
|
|
914
|
+
feed.dispose();
|
|
915
|
+
});
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** One entry for both OpenCodes: v1 calls `tui`, v2 calls `setup` (docs/opencode/v2.md). */
|
|
920
|
+
export default dualTui("opencode-cockpit.trust", createTrustTui());
|