@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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `opencode.json` says about a permission, read the way OpenCode reads it.
|
|
3
|
+
*
|
|
4
|
+
* Trust only ever sees the gap config left open (docs/opencode/permissions.md): `deny` never emits an
|
|
5
|
+
* event and `allow` never needs one. Inside that gap there are two kinds of "ask", and telling them
|
|
6
|
+
* apart is this file's job:
|
|
7
|
+
*
|
|
8
|
+
* - **a catch-all** — `"bash": "ask"`, `{ "bash": { "*": "ask" } }`, or no rule at all — is a default:
|
|
9
|
+
* "I have not decided about these". Trust may fill it.
|
|
10
|
+
* - **a specific pattern set to ask** — `"git push *": "ask"` — is a decision: "always ask me about
|
|
11
|
+
* this one". Trust never answers it.
|
|
12
|
+
*
|
|
13
|
+
* Matching is OpenCode's own: its wildcard (`*` any run, `?` one character, and a trailing ` *` that
|
|
14
|
+
* also matches nothing, so `ls *` covers `ls`), evaluated over the ordered rules, last match wins.
|
|
15
|
+
* The merge across files is OpenCode's too — v1 hands over the merged config; v2 hands over its
|
|
16
|
+
* documents in priority order, and concatenating their rules in that order keeps "last match wins"
|
|
17
|
+
* meaning what it meant.
|
|
18
|
+
*/
|
|
19
|
+
export type Action = "allow" | "deny" | "ask";
|
|
20
|
+
export interface ConfigRule {
|
|
21
|
+
/** Permission name, canonical (`bash`, never `shell`); may be a wildcard such as `*`. */
|
|
22
|
+
permission: string;
|
|
23
|
+
pattern: string;
|
|
24
|
+
action: Action;
|
|
25
|
+
}
|
|
26
|
+
export declare const canonical: (permission: string) => string;
|
|
27
|
+
/** OpenCode's `Wildcard.match`, as in `util/wildcard.ts`. */
|
|
28
|
+
export declare function match(text: string, pattern: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The rules in force for an agent, from whatever `config.get` returned: v1's merged config, or v2's
|
|
31
|
+
* list of documents (`{ type: "document", info }`, lowest priority first). The agent's rules come last
|
|
32
|
+
* because OpenCode lays them over the global ones.
|
|
33
|
+
*/
|
|
34
|
+
export declare function rulesFrom(config: unknown, agent?: string): ConfigRule[];
|
|
35
|
+
/** The rule that decides `text` for `permission`, as OpenCode picks it: the last that matches. */
|
|
36
|
+
export declare function evaluate(rules: readonly ConfigRule[], permission: string, text: string): ConfigRule | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* What config means for one pattern of a request.
|
|
39
|
+
*
|
|
40
|
+
* `open` — nothing decided it, or a catch-all said ask: Trust's to fill.
|
|
41
|
+
* `allowed` — config allows it; the request asked because of something else in it.
|
|
42
|
+
* `held` — you asked to be asked (a specific `ask`), or config denies it: Trust stays out.
|
|
43
|
+
*/
|
|
44
|
+
export type Gate = {
|
|
45
|
+
kind: "open";
|
|
46
|
+
} | {
|
|
47
|
+
kind: "allowed";
|
|
48
|
+
rule: ConfigRule;
|
|
49
|
+
} | {
|
|
50
|
+
kind: "held";
|
|
51
|
+
rule: ConfigRule;
|
|
52
|
+
};
|
|
53
|
+
export declare function gate(rules: readonly ConfigRule[], permission: string, text: string): Gate;
|
|
54
|
+
/** A rule as you would have written it, for a reason shown to you. */
|
|
55
|
+
export declare function describeRule(rule: ConfigRule): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample worlds for the preview and the grid test: the states a design gets wrong
|
|
3
|
+
* (docs/building/testing.md) — nothing at all, a first week, a busy project, a paused one, one
|
|
4
|
+
* where something broke, and one grouped into families with one of them widened by hand.
|
|
5
|
+
*
|
|
6
|
+
* Each is built by running the real engine over real requests, so the preview shows what the
|
|
7
|
+
* engine actually produces rather than what a hand-written state hoped it would.
|
|
8
|
+
*/
|
|
9
|
+
import { type Engine } from "./engine.ts";
|
|
10
|
+
export declare const SAMPLE_ROOT = "/work/app";
|
|
11
|
+
export declare const SAMPLE_NOW = 1790300000000;
|
|
12
|
+
export declare const SAMPLE_SETTINGS: {
|
|
13
|
+
threshold: number;
|
|
14
|
+
dangerExtra: number;
|
|
15
|
+
expireDays: number;
|
|
16
|
+
keep: number;
|
|
17
|
+
};
|
|
18
|
+
export interface Sample {
|
|
19
|
+
engine: Engine;
|
|
20
|
+
trouble?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const SAMPLES: Record<string, () => Sample>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A command line, read the way Trust needs it: split into the commands it runs, each as words.
|
|
3
|
+
*
|
|
4
|
+
* Not a shell. It has one job and one direction to fail in: anything it cannot read *for certain* —
|
|
5
|
+
* an expansion, a substitution, a subshell, a heredoc, a program that runs a string it was handed —
|
|
6
|
+
* comes back `opaque`, and an opaque line is always asked. A reader that guesses would let the one
|
|
7
|
+
* line it misread be approved by the approvals of another.
|
|
8
|
+
*
|
|
9
|
+
* Why not OpenCode's tree-sitter: one runtime dependency per package (docs/building/a-new-bay.md),
|
|
10
|
+
* and a grammar still has to be told what "cannot be known statically" means — which is this file.
|
|
11
|
+
*/
|
|
12
|
+
export interface Command {
|
|
13
|
+
/** `NAME=value` words before the program. They change what it does, so they are part of it. */
|
|
14
|
+
env: string[];
|
|
15
|
+
/** The program and its arguments, quotes removed. */
|
|
16
|
+
argv: string[];
|
|
17
|
+
/** Where it runs when an earlier `cd` on the same line moved it; absolute or relative as written. */
|
|
18
|
+
cwd?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Which `argv` words are redirections the shell acts on (`>`, `2>&1`), as opposed to the same
|
|
21
|
+
* characters quoted as an argument: `echo '>' x` writes nothing, `echo > x` writes a file, and by
|
|
22
|
+
* text alone the two were one command. Absent on a command built by hand: then the text decides.
|
|
23
|
+
*/
|
|
24
|
+
redirects?: number[];
|
|
25
|
+
}
|
|
26
|
+
export type Parsed = {
|
|
27
|
+
kind: "commands";
|
|
28
|
+
commands: Command[];
|
|
29
|
+
} | {
|
|
30
|
+
kind: "opaque";
|
|
31
|
+
reason: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Reads a command line into the commands it runs.
|
|
35
|
+
*
|
|
36
|
+
* `cd` is not returned as a command — it asks nothing by itself, as in OpenCode — but it moves every
|
|
37
|
+
* command after it, and that is kept: `cd build && rm -rf *` and `cd /tmp && rm -rf *` are different
|
|
38
|
+
* lines and must never share an approval.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parse(line: string): Parsed;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an approval is an approval *of*.
|
|
3
|
+
*
|
|
4
|
+
* A signature is the command exactly as it will run — environment, program, every argument in order,
|
|
5
|
+
* and where it runs — with quoting normalised so that `echo 'a b'` and `echo "a b"` are one thing.
|
|
6
|
+
* Nothing is generalised away. OpenCode's own "always" generalises, and measured on its own arity
|
|
7
|
+
* table, `docker compose -p cockpit up -d` and `docker compose -p prod down -v` both became
|
|
8
|
+
* `docker compose -p *` (docs/opencode/permissions.md). A signature can only ever match the command
|
|
9
|
+
* that earned it; making one cover more is something a person does, in the ledger, on purpose.
|
|
10
|
+
*/
|
|
11
|
+
import type { Command } from "./shell.ts";
|
|
12
|
+
export declare function quote(word: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Where a command runs, said relative to the project: `cd /path/to/project && git status` is
|
|
15
|
+
* `git status`. A directory outside the project stays absolute, so it cannot pass for one inside.
|
|
16
|
+
*/
|
|
17
|
+
export declare function place(cwd: string | undefined, root: string | undefined): string | undefined;
|
|
18
|
+
export declare function signature(command: Command, root?: string): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `x`, `w` and `c` do, on whatever is selected — a command, a family, an answer in the feed, or
|
|
3
|
+
* OpenCode's own "always". Pure: each returns the events to append and the sentence to say. Nothing
|
|
4
|
+
* in the ledger is ever changed in place.
|
|
5
|
+
*/
|
|
6
|
+
import type { Answer } from "../history.ts";
|
|
7
|
+
import type { Event } from "../ledger.ts";
|
|
8
|
+
import { type AlwaysGroup, type Command, type Family, type Reading } from "./model.ts";
|
|
9
|
+
import type { Tone } from "./rows.ts";
|
|
10
|
+
export type Target = {
|
|
11
|
+
kind: "command";
|
|
12
|
+
command: Command;
|
|
13
|
+
family?: Family;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "family";
|
|
16
|
+
family: Family;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "answer";
|
|
19
|
+
answer: Answer;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "always";
|
|
22
|
+
groups: AlwaysGroup[];
|
|
23
|
+
};
|
|
24
|
+
export interface Outcome {
|
|
25
|
+
/** To append to the ledger. */
|
|
26
|
+
events: Event[];
|
|
27
|
+
notice: {
|
|
28
|
+
text: string;
|
|
29
|
+
tone: Tone;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** What a widening never covers, in words — the three `family.outside` holds back. */
|
|
33
|
+
export declare const EXCEPT = "except dangerous ones, ones that write a file and ones that run another program";
|
|
34
|
+
export declare const NOT_COVERED = "dangerous ones, and any that write a file or run another program";
|
|
35
|
+
/** What `x` is called on a target: revoke what answers, forget what is only counting. */
|
|
36
|
+
export declare function revokeLabel(target: Target): {
|
|
37
|
+
label: string;
|
|
38
|
+
off: boolean;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* `x`. A command loses its standing for every agent. A family loses every command in it and its
|
|
42
|
+
* widenings. An answer in the feed stops what gave it: the rule's own count, or the widening.
|
|
43
|
+
* OpenCode's own "always" is OpenCode's: Trust cannot take it back, and says so.
|
|
44
|
+
*/
|
|
45
|
+
export declare function revoke(target: Target, reading: Reading, at: number): Outcome;
|
|
46
|
+
/** The family `w` acts on, and for which agent. */
|
|
47
|
+
export interface WidenScope {
|
|
48
|
+
permission: string;
|
|
49
|
+
family: string;
|
|
50
|
+
/** The agent `w` would widen it for. */
|
|
51
|
+
agent?: string;
|
|
52
|
+
/** Agents it is widened for now, of those `w` would act on: non-empty means `w` undoes. */
|
|
53
|
+
undo: string[];
|
|
54
|
+
}
|
|
55
|
+
export declare function widenScope(target: Target, families: readonly Family[]): WidenScope | undefined;
|
|
56
|
+
/** The family's name in a button: commands stay lowercase, where it runs is left to the card. */
|
|
57
|
+
export declare function familyLabel(permission: string, family: string): string;
|
|
58
|
+
/** What `w` is called: `Trust any head`, `Undo any head`, or dimmed when the family never widens. */
|
|
59
|
+
export declare function widenLabel(scope: WidenScope | undefined, named?: boolean): {
|
|
60
|
+
label: string;
|
|
61
|
+
off: boolean;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* `w`: trust a whole family for one agent, or take that back. Never automatic — this is the only place
|
|
65
|
+
* a `widened` event is made, and only a person reaches it. A dangerous family is refused, with why.
|
|
66
|
+
*/
|
|
67
|
+
export declare function widen(scope: WidenScope | undefined, families: readonly Family[], at: number): Outcome;
|
|
68
|
+
/**
|
|
69
|
+
* The target as `opencode.json` would say it, for you to paste — Trust never writes OpenCode's
|
|
70
|
+
* config. A family is a wildcard (`"ls *": "allow"`), and config says less than a widening: not which
|
|
71
|
+
* agent, and not the commands a widening still asks about.
|
|
72
|
+
*/
|
|
73
|
+
export declare function configSnippet(target: Target): {
|
|
74
|
+
text: string;
|
|
75
|
+
note?: string;
|
|
76
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The screen `/trust` opens on: what Trust did for you, newest first, then what it is about to do.
|
|
3
|
+
*
|
|
4
|
+
* Trust · opencode-cockpit ● answering
|
|
5
|
+
*
|
|
6
|
+
* TODAY Trust answered 4 prompts for you ▁▁▃▁▅▂█ last 7 days
|
|
7
|
+
* ▌09:41 ✓ git status --short build trusted since yesterday, 3 in a row
|
|
8
|
+
* 09:40 ✓ ls -la general in a family you widened: ls
|
|
9
|
+
*
|
|
10
|
+
* ALMOST THERE one more approval and Trust answers these
|
|
11
|
+
* ○ bun --version build ▰▰▱ 2 of 3
|
|
12
|
+
* ○ git push origin feat/trust build ▰▰▰▰▰▱▱▱ 5 of 8 dangerous
|
|
13
|
+
*
|
|
14
|
+
* ! WATCH OUT OpenCode's own "always" approves more than it looks, until it restarts
|
|
15
|
+
* ! find * sed * general [enter] what it covers
|
|
16
|
+
*
|
|
17
|
+
* RULES 11 trusted · 14 learning · 101 seen once l Open the ledger
|
|
18
|
+
*
|
|
19
|
+
* [enter] Why [x] Revoke [w] Trust Family [l] Ledger [p] Pause [?] Keys [esc] Close
|
|
20
|
+
*
|
|
21
|
+
* Transparency is the point of Trust, so it leads with it: the question a person opens it with is
|
|
22
|
+
* "what did it do while I was not looking?", and the first line answers it. What is close comes
|
|
23
|
+
* next, because that is what will happen next; OpenCode's own broad approvals get a band of their
|
|
24
|
+
* own, so they cannot be mistaken for Trust's rules; and managing rules is one key away, in the
|
|
25
|
+
* ledger (explorer.ts). Every list is a window that follows the cursor, so a short dialog still
|
|
26
|
+
* reaches everything.
|
|
27
|
+
*/
|
|
28
|
+
import { type Answer, type History } from "../history.ts";
|
|
29
|
+
import { type Target } from "./actions.ts";
|
|
30
|
+
import { type AlwaysGroup, type Command, type Counts, type Family, type Reading } from "./model.ts";
|
|
31
|
+
import { type Hit, type KeyLine } from "./parts.ts";
|
|
32
|
+
import { type Row, type Run } from "./rows.ts";
|
|
33
|
+
export type ActivityItem =
|
|
34
|
+
/** An answer Trust gave; `count` answers of the same line in a row are one item. */
|
|
35
|
+
{
|
|
36
|
+
kind: "answer";
|
|
37
|
+
key: string;
|
|
38
|
+
answer: Answer;
|
|
39
|
+
count: number;
|
|
40
|
+
} | {
|
|
41
|
+
kind: "almost";
|
|
42
|
+
key: string;
|
|
43
|
+
command: Command;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "always";
|
|
46
|
+
key: string;
|
|
47
|
+
group: AlwaysGroup;
|
|
48
|
+
};
|
|
49
|
+
export interface ActivityModel {
|
|
50
|
+
/** Every item the cursor moves over, in screen order. */
|
|
51
|
+
items: ActivityItem[];
|
|
52
|
+
feed: ActivityItem[];
|
|
53
|
+
almost: ActivityItem[];
|
|
54
|
+
always: ActivityItem[];
|
|
55
|
+
/** Answers today; when there are none, the feed is the latest from earlier days. */
|
|
56
|
+
today: number;
|
|
57
|
+
week: number[];
|
|
58
|
+
counts: Counts;
|
|
59
|
+
commands: Command[];
|
|
60
|
+
families: Family[];
|
|
61
|
+
}
|
|
62
|
+
export declare function activityModel(input: Reading & {
|
|
63
|
+
history: History;
|
|
64
|
+
}): ActivityModel;
|
|
65
|
+
/** What the cursor's item is, for `x`, `w` and `c`. */
|
|
66
|
+
export declare function targetOf(item: ActivityItem, model: ActivityModel): Target;
|
|
67
|
+
/**
|
|
68
|
+
* Why Trust answered, in a few muted words: the approvals that earned it and when, or the family you
|
|
69
|
+
* widened. Read from the rule's own moments (history.ts); when the window no longer reaches back that
|
|
70
|
+
* far, the reason the answering window logged.
|
|
71
|
+
*/
|
|
72
|
+
export declare function answerWhy(answer: Answer, reading: Reading & {
|
|
73
|
+
history: History;
|
|
74
|
+
}): string;
|
|
75
|
+
export interface ActivityInput extends Reading {
|
|
76
|
+
width: number;
|
|
77
|
+
height: number;
|
|
78
|
+
history: History;
|
|
79
|
+
/** The project's folder name, for the header. */
|
|
80
|
+
project: string;
|
|
81
|
+
/** The key of the item under the cursor; the first item when it is not there. */
|
|
82
|
+
selected?: string;
|
|
83
|
+
notice?: {
|
|
84
|
+
text: string;
|
|
85
|
+
tone: Run["tone"];
|
|
86
|
+
};
|
|
87
|
+
/** `?`: every key, in the body's place. */
|
|
88
|
+
keys?: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface ActivityView {
|
|
91
|
+
rows: Row[];
|
|
92
|
+
/** The item under the cursor, as drawn: the caller's actions act on it. */
|
|
93
|
+
item?: ActivityItem;
|
|
94
|
+
model: ActivityModel;
|
|
95
|
+
/** Where a click lands: a row selects, the ledger button opens the ledger. */
|
|
96
|
+
hits: Hit[];
|
|
97
|
+
}
|
|
98
|
+
/** The shortest dialog drawn: a header, a little of each list, the rules line and a way out. */
|
|
99
|
+
export declare const MIN_HEIGHT = 11;
|
|
100
|
+
export declare const ACTIVITY_KEYS: readonly KeyLine[];
|
|
101
|
+
export declare function activityRows(input: ActivityInput): ActivityView;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ledger, behind `l`: every rule Trust holds, as a tree of families, and a card that explains the
|
|
3
|
+
* one selected — always on screen, so there is no details key to find.
|
|
4
|
+
*
|
|
5
|
+
* Trust · opencode-cockpit 11 trusted · 14 learning · 101 seen once ● answering
|
|
6
|
+
* ────────────────────────────────────────┬─────────────────────────────────────────────────────
|
|
7
|
+
* FAMILIES / filter │ head -30
|
|
8
|
+
* ▾ head 1 ✓ 5 ○ │ ✓ Trusted for general · ready, not used yet
|
|
9
|
+
* ▌ head -30 ✓ trusted │
|
|
10
|
+
* head -40 ▰▰▱ 2 of 3 │ Exactly head -30
|
|
11
|
+
* + 3 more │ Still asks head -31 · head -30 > out.txt
|
|
12
|
+
* ▸ git status 1 ✓ 1 ○ │ History ✓ 9h ✓ 9h ✓ 9h → trusted
|
|
13
|
+
* ls -la ✓ trusted 1× │ 3 approvals in a row, all yours
|
|
14
|
+
* git push … ! ▰▰▰▰▰▱▱▱ 5 of 8 │ Family head · 6 commands, 1 trusted
|
|
15
|
+
* ! OpenCode always 2 broad rules │ x Revoke w Trust any head c Copy rule
|
|
16
|
+
* ────────────────────────────────────────┴─────────────────────────────────────────────────────
|
|
17
|
+
* [↑/↓] Move [←/→] Fold [tab] Card [/] Filter [p] Pause [?] Keys [esc] Back
|
|
18
|
+
*
|
|
19
|
+
* **A command is one row.** The list it replaced split a family into what answers and what learns, so
|
|
20
|
+
* `head -30` was in one section while `head` was in the other, and the details repeated. Here a family
|
|
21
|
+
* is one fold, like a folder in an editor, and a command one row in it whatever its agents say; the
|
|
22
|
+
* card lists each agent's standing.
|
|
23
|
+
*
|
|
24
|
+
* **The card is the explanation.** The command whole, on a raised panel; where each agent stands; the
|
|
25
|
+
* exact text and what still asks; the approvals that earned it (history.ts); its family and what `w`
|
|
26
|
+
* would do; and the actions as buttons you can click or reach with `tab`.
|
|
27
|
+
*
|
|
28
|
+
* Below about ninety columns the card moves under the tree, the selection kept in view above it.
|
|
29
|
+
*/
|
|
30
|
+
import { type History, type Mark } from "../history.ts";
|
|
31
|
+
import { type Target } from "./actions.ts";
|
|
32
|
+
import { type AlwaysGroup, type Command, type Counts, type Family, type Reading } from "./model.ts";
|
|
33
|
+
import { type Hit, type KeyLine } from "./parts.ts";
|
|
34
|
+
import { type Row, type Run, type Tone } from "./rows.ts";
|
|
35
|
+
export type Node = {
|
|
36
|
+
kind: "family";
|
|
37
|
+
key: string;
|
|
38
|
+
family: Family;
|
|
39
|
+
open: boolean;
|
|
40
|
+
} | {
|
|
41
|
+
kind: "command";
|
|
42
|
+
key: string;
|
|
43
|
+
command: Command;
|
|
44
|
+
family: Family;
|
|
45
|
+
nested: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** The folded tail of an open family: `+ 3 more`. */
|
|
48
|
+
| {
|
|
49
|
+
kind: "more";
|
|
50
|
+
key: string;
|
|
51
|
+
family: Family;
|
|
52
|
+
hidden: number;
|
|
53
|
+
} | {
|
|
54
|
+
kind: "always";
|
|
55
|
+
key: string;
|
|
56
|
+
groups: AlwaysGroup[];
|
|
57
|
+
};
|
|
58
|
+
export interface Tree {
|
|
59
|
+
/** Families opened, by `Family.key`. */
|
|
60
|
+
open: ReadonlySet<string>;
|
|
61
|
+
/** Families whose tail is shown too, by `Family.key`. */
|
|
62
|
+
full: ReadonlySet<string>;
|
|
63
|
+
/** Only what matches this text, every family with a match open. Empty: everything. */
|
|
64
|
+
filter: string;
|
|
65
|
+
}
|
|
66
|
+
export interface ExplorerModel {
|
|
67
|
+
nodes: Node[];
|
|
68
|
+
families: Family[];
|
|
69
|
+
commands: Command[];
|
|
70
|
+
counts: Counts;
|
|
71
|
+
}
|
|
72
|
+
/** Commands an open family lists before folding the rest into `+ N more`. */
|
|
73
|
+
export declare const TAIL = 3;
|
|
74
|
+
export declare const familyNodeKey: (family: Family) => string;
|
|
75
|
+
export declare const commandNodeKey: (command: Command) => string;
|
|
76
|
+
export declare const ALWAYS_KEY = "o:always";
|
|
77
|
+
export declare function explorerModel(input: Reading & Tree): ExplorerModel;
|
|
78
|
+
/** What `x`, `w` and `c` act on, for a node. */
|
|
79
|
+
export declare function nodeTarget(node: Node): Target;
|
|
80
|
+
/** The tree state that shows `command` with its family open, its tail too when it is folded there. */
|
|
81
|
+
export declare function reveal(tree: {
|
|
82
|
+
open: Set<string>;
|
|
83
|
+
full: Set<string>;
|
|
84
|
+
}, families: readonly Family[], subject: {
|
|
85
|
+
permission: string;
|
|
86
|
+
subject: string;
|
|
87
|
+
}): string | undefined;
|
|
88
|
+
/** One labelled fact on the card: each line wraps on its own. `keep`: higher stays when room is short. */
|
|
89
|
+
interface Fact {
|
|
90
|
+
label: string;
|
|
91
|
+
lines: Run[][];
|
|
92
|
+
keep: number;
|
|
93
|
+
/** The first line is a row of moments: it keeps its newest end on one row rather than wrapping. */
|
|
94
|
+
newest?: boolean;
|
|
95
|
+
}
|
|
96
|
+
export interface Button {
|
|
97
|
+
key: string;
|
|
98
|
+
label: string;
|
|
99
|
+
off: boolean;
|
|
100
|
+
action: "revoke" | "widen" | "copy";
|
|
101
|
+
}
|
|
102
|
+
interface CardParts {
|
|
103
|
+
title: Run[];
|
|
104
|
+
standing: Run[][];
|
|
105
|
+
facts: Fact[];
|
|
106
|
+
buttons: Button[];
|
|
107
|
+
}
|
|
108
|
+
export interface CardReading extends Reading {
|
|
109
|
+
history: History;
|
|
110
|
+
families: readonly Family[];
|
|
111
|
+
}
|
|
112
|
+
/** The buttons a node offers: `x`, `w` where a family can widen, `c`. */
|
|
113
|
+
export declare function buttonsOf(node: Node, families: readonly Family[]): Button[];
|
|
114
|
+
/**
|
|
115
|
+
* The moments that made a rule what it is, oldest to newest, the newest kept when they do not all fit:
|
|
116
|
+
* `✓ 9h ✓ 9h ✓ 9h → trusted answered 4×`.
|
|
117
|
+
*/
|
|
118
|
+
export declare function historyRuns(marks: readonly Mark[], need: number, expireMs: number, now: number): Run[];
|
|
119
|
+
export declare function cardOf(node: Node, reading: CardReading): CardParts;
|
|
120
|
+
export interface ExplorerInput extends Reading, Tree {
|
|
121
|
+
width: number;
|
|
122
|
+
height: number;
|
|
123
|
+
history: History;
|
|
124
|
+
project: string;
|
|
125
|
+
/** The node under the cursor, by key; the first node when it is not there. */
|
|
126
|
+
selected?: string;
|
|
127
|
+
/** `tab` moved into the card: which button is focused. */
|
|
128
|
+
focus?: {
|
|
129
|
+
button: number;
|
|
130
|
+
};
|
|
131
|
+
/** `/` was pressed: what has been typed so far. */
|
|
132
|
+
typing?: string;
|
|
133
|
+
notice?: {
|
|
134
|
+
text: string;
|
|
135
|
+
tone: Tone;
|
|
136
|
+
};
|
|
137
|
+
/** `?`: every key, in the body's place. */
|
|
138
|
+
keys?: boolean;
|
|
139
|
+
}
|
|
140
|
+
export interface ExplorerView {
|
|
141
|
+
rows: Row[];
|
|
142
|
+
node?: Node;
|
|
143
|
+
model: ExplorerModel;
|
|
144
|
+
buttons: Button[];
|
|
145
|
+
hits: Hit[];
|
|
146
|
+
/** Whether the card is beside the tree (true) or under it. */
|
|
147
|
+
wide: boolean;
|
|
148
|
+
}
|
|
149
|
+
/** At this width and wider the card is beside the tree; narrower, under it. */
|
|
150
|
+
export declare const WIDE = 90;
|
|
151
|
+
export declare const MIN_HEIGHT = 11;
|
|
152
|
+
export declare const EXPLORER_KEYS: readonly KeyLine[];
|
|
153
|
+
export declare function explorerRows(input: ExplorerInput): ExplorerView;
|
|
154
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What both Trust screens read: every command Trust has counted, once, with where each agent stands
|
|
3
|
+
* on it; the families they group into; and the three numbers that sum a project up.
|
|
4
|
+
*
|
|
5
|
+
* **One command, one place.** The ledger used to split a family by section, so `head -30` was under
|
|
6
|
+
* Answers while `head` was also under Learning and its details repeated. Here a command is one
|
|
7
|
+
* `Command` whatever its agents say about it — trusted for one, two of three for another — and a
|
|
8
|
+
* family one `Family`. The screens draw each once.
|
|
9
|
+
*/
|
|
10
|
+
import { type Always, type Entry, type State, type Thresholds, type Widened } from "../ledger.ts";
|
|
11
|
+
export interface Reading {
|
|
12
|
+
state: State;
|
|
13
|
+
settings: Thresholds;
|
|
14
|
+
now: number;
|
|
15
|
+
}
|
|
16
|
+
/** Where one agent stands on one command. */
|
|
17
|
+
export type Stand = {
|
|
18
|
+
kind: "trusted";
|
|
19
|
+
}
|
|
20
|
+
/** Not trusted by its own count, answered through a family you widened. */
|
|
21
|
+
| {
|
|
22
|
+
kind: "widened";
|
|
23
|
+
family: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "counting";
|
|
26
|
+
have: number;
|
|
27
|
+
need: number;
|
|
28
|
+
expired: boolean;
|
|
29
|
+
};
|
|
30
|
+
export declare function standOf(entry: Entry, { state, settings, now }: Reading): Stand;
|
|
31
|
+
export declare const answers: (stand: Stand) => boolean;
|
|
32
|
+
/** Approvals still needed; an expired count is as far as it gets. Zero: it answers. */
|
|
33
|
+
export declare const distance: (stand: Stand) => number;
|
|
34
|
+
/** One agent's standing on a command. */
|
|
35
|
+
export interface Standing {
|
|
36
|
+
entry: Entry;
|
|
37
|
+
stand: Stand;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Where a command is, all agents together:
|
|
41
|
+
* - `answering`: Trust answers it for at least one agent;
|
|
42
|
+
* - `learning`: counting, with approvals that matter (two in a row, or answered before);
|
|
43
|
+
* - `once`: approved once and never again — most never come back, so they are kept out of the way.
|
|
44
|
+
*/
|
|
45
|
+
export type Phase = "answering" | "learning" | "once";
|
|
46
|
+
export interface Command {
|
|
47
|
+
/** `[permission, subject]`, the same for every agent. */
|
|
48
|
+
key: string;
|
|
49
|
+
permission: string;
|
|
50
|
+
subject: string;
|
|
51
|
+
family: string;
|
|
52
|
+
/** Every agent's standing, the one that answers first, then the closest to it. */
|
|
53
|
+
standings: Standing[];
|
|
54
|
+
phase: Phase;
|
|
55
|
+
/** Today's reading of why it costs more, as of the last time it was asked. */
|
|
56
|
+
danger?: string;
|
|
57
|
+
/** The last approval or answer, any agent. */
|
|
58
|
+
lastAt: number;
|
|
59
|
+
}
|
|
60
|
+
export declare const commandKey: (permission: string, subject: string) => string;
|
|
61
|
+
/** The standing the command is drawn by: the one that answers, else the closest. */
|
|
62
|
+
export declare const leadOf: (command: Command) => Standing;
|
|
63
|
+
/** Approvals the closest agent still needs. */
|
|
64
|
+
export declare const closeness: (command: Command) => number;
|
|
65
|
+
/**
|
|
66
|
+
* Every command anyone approved or Trust answered, one each. A subject only ever rejected has
|
|
67
|
+
* nothing to show and is left out.
|
|
68
|
+
*/
|
|
69
|
+
export declare function commandsOf(reading: Reading): Command[];
|
|
70
|
+
/** Answering newest first; then learning closest first, a dangerous one after the rest; then once. */
|
|
71
|
+
export declare function byPhase(a: Command, b: Command): number;
|
|
72
|
+
export interface Family {
|
|
73
|
+
/** `[permission, family]`. */
|
|
74
|
+
key: string;
|
|
75
|
+
permission: string;
|
|
76
|
+
family: string;
|
|
77
|
+
/** Its commands, `byPhase`. */
|
|
78
|
+
commands: Command[];
|
|
79
|
+
/** The agents you trusted the whole family for. */
|
|
80
|
+
widened: Widened[];
|
|
81
|
+
lastAt: number;
|
|
82
|
+
}
|
|
83
|
+
export declare const familyKey: (permission: string, family: string) => string;
|
|
84
|
+
/**
|
|
85
|
+
* The commands grouped by family, with every widened family — even one with no command left. Families
|
|
86
|
+
* that answer first (newest first), then those learning (closest first), the dangerous ones after,
|
|
87
|
+
* and families seen only once last.
|
|
88
|
+
*/
|
|
89
|
+
export declare function familiesOf(reading: Reading, commands?: readonly Command[]): Family[];
|
|
90
|
+
/** The project in three numbers, one per command. */
|
|
91
|
+
export interface Counts {
|
|
92
|
+
trusted: number;
|
|
93
|
+
learning: number;
|
|
94
|
+
once: number;
|
|
95
|
+
}
|
|
96
|
+
export declare function countsOf(commands: readonly Command[]): Counts;
|
|
97
|
+
/** OpenCode's own "always" approvals, one group per agent and permission, the newest first. */
|
|
98
|
+
export interface AlwaysGroup {
|
|
99
|
+
key: string;
|
|
100
|
+
permission: string;
|
|
101
|
+
agent: string;
|
|
102
|
+
patterns: string[];
|
|
103
|
+
at: number;
|
|
104
|
+
approvals: Always[];
|
|
105
|
+
}
|
|
106
|
+
export declare function alwaysGroups(state: State): AlwaysGroup[];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pieces both Trust screens are built from: an agent's chip, a tinted badge, a button, a meter,
|
|
3
|
+
* the week's sparkline, the header and the key list.
|
|
4
|
+
*
|
|
5
|
+
* Colour came back to Trust as signal (docs/building/design-system.md): green is what answers, the
|
|
6
|
+
* warning what is close or needs a look, red what is dangerous. Each piece pairs its colour with a
|
|
7
|
+
* word or a shape — a meter beside `2 of 3`, a badge that says `dangerous` — so none of it is read
|
|
8
|
+
* by colour alone.
|
|
9
|
+
*/
|
|
10
|
+
import { type Hint } from "@opencode-cockpit/client/design";
|
|
11
|
+
import { type Row, type Run } from "./rows.ts";
|
|
12
|
+
export declare const muted: (text: string) => Run;
|
|
13
|
+
export declare const plain: (text: string) => Run;
|
|
14
|
+
export declare const key: (name: string) => Run;
|
|
15
|
+
/** An agent, as a chip: its name in the info tone on a faint fill of the same. */
|
|
16
|
+
export declare const chip: (agent: string) => Run;
|
|
17
|
+
/** A word on a tint of its tone: ` ● answering `, ` dangerous `. */
|
|
18
|
+
export declare const badge: (text: string, tone: "success" | "warning" | "error") => Run;
|
|
19
|
+
/**
|
|
20
|
+
* A button: its key, then what it does, on a raised fill — a run you can click as well as a key you
|
|
21
|
+
* can press. `on`: focused, drawn solid in the accent. `off`: nothing to act on here; dimmed rather
|
|
22
|
+
* than removed, because a button that comes and goes cannot be learned.
|
|
23
|
+
*/
|
|
24
|
+
export declare function button(name: string, label: string, state?: {
|
|
25
|
+
on?: boolean;
|
|
26
|
+
off?: boolean;
|
|
27
|
+
}): Run[];
|
|
28
|
+
/** How close: one `▰` per approval that counts, `▱` for each still to go. Dangerous ones in red. */
|
|
29
|
+
export declare function meter(have: number, need: number, danger: boolean): Run[];
|
|
30
|
+
/** A day per cell, scaled to the busiest: a day with none is the empty track, in the border tone. */
|
|
31
|
+
export declare function sparkline(counts: readonly number[]): Run[];
|
|
32
|
+
/** `9h`, `2d`, `now`: a time in a column of times, where `ago` on every one says nothing. */
|
|
33
|
+
export declare const since: (ms: number) => string;
|
|
34
|
+
/** `just now`, `15m ago`. */
|
|
35
|
+
export declare const when: (ms: number) => string;
|
|
36
|
+
export declare const plural: (count: number, one: string, many?: string) => string;
|
|
37
|
+
export declare const agentsText: (agents: readonly string[]) => string;
|
|
38
|
+
/** `09:41` today; `Tue 09:41` this week; `Sep 21` before that. Local time, as a clock on the wall. */
|
|
39
|
+
export declare function clock(at: number, now: number): string;
|
|
40
|
+
/** `today`, `yesterday`, `on Tue`, `on Sep 21`: when, as a day. */
|
|
41
|
+
export declare function dayWord(at: number, now: number): string;
|
|
42
|
+
/**
|
|
43
|
+
* `Trust · project` on the left; on the right whatever the screen adds, then whether Trust is
|
|
44
|
+
* answering — the first thing to know, so it is a badge: `● answering`, or `○ paused`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function headerRow(project: string, paused: boolean, extra: Run[], width: number): Row;
|
|
47
|
+
/** The keys row: a cell of margin at each end, the way out last and never dropped. */
|
|
48
|
+
export declare function footerRow(hints: readonly Hint[], width: number): Row;
|
|
49
|
+
export interface KeyLine {
|
|
50
|
+
keys: string[];
|
|
51
|
+
does: string;
|
|
52
|
+
}
|
|
53
|
+
/** `runs` wrapped at spaces into rows of `width`, at most `max`; the last says `…` when cut. */
|
|
54
|
+
export declare function wrapRuns(runs: readonly Run[], width: number, max?: number): Row[];
|
|
55
|
+
/** Every key a screen takes, one line each, what each does wrapped under its column rather than cut. */
|
|
56
|
+
export declare function keyListRows(list: readonly KeyLine[], width: number): Row[][];
|
|
57
|
+
/** As many key lines as fit in `room` rows; a list cut short says how many keys are below. */
|
|
58
|
+
export declare function keyListBody(list: readonly KeyLine[], width: number, room: number): Row[];
|
|
59
|
+
/** Something on screen a click acts on: a row to select, or a button to press. */
|
|
60
|
+
export type Hit =
|
|
61
|
+
/** `x0`/`x1`: the columns it covers, when it does not take the whole row. */
|
|
62
|
+
{
|
|
63
|
+
kind: "row";
|
|
64
|
+
y: number;
|
|
65
|
+
key: string;
|
|
66
|
+
x0?: number;
|
|
67
|
+
x1?: number;
|
|
68
|
+
} | {
|
|
69
|
+
kind: "button";
|
|
70
|
+
y: number;
|
|
71
|
+
x0: number;
|
|
72
|
+
x1: number;
|
|
73
|
+
action: string;
|
|
74
|
+
};
|
|
75
|
+
/** The buttons in a row of runs, by where each starts and ends. `actions`: one per button, in order. */
|
|
76
|
+
export declare function buttonHits(row: Row, y: number, actions: readonly string[]): Hit[];
|