clavarium-binder 0.1.1 → 0.1.2-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/binder.mjs +4 -1
- package/docs/protocol.md +26 -3
- package/host/binder-host.mjs +4 -0
- package/package.json +1 -1
- package/server/binder.mjs +3 -1
- package/server/store.mjs +39 -4
- package/ui/binder-ui.css +1 -0
- package/ui/binder-ui.js +31 -4
- package/ui/texts/en.json +1 -0
- package/ui/texts/fr.json +1 -0
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ way, universal (nothing in it is specific to Clavarium).
|
|
|
37
37
|
it, or any state after it, only once a person ticked it. An agent proposes,
|
|
38
38
|
and works on what was approved.
|
|
39
39
|
- **See what it was not given.** Access is given per list: read, or change.
|
|
40
|
+
It may make a list of its own from a template (`binder new batches`): given
|
|
41
|
+
to it alone, and seen by a person as any other; a few at most.
|
|
40
42
|
- **Delete.** It archives; a person deletes, and deleted is deleted.
|
|
41
43
|
|
|
42
44
|
## Without an application
|
package/bin/binder.mjs
CHANGED
|
@@ -23,6 +23,8 @@ export const HELP = `binder: the lists you were given, and their records.
|
|
|
23
23
|
binder attach ID KIND REF ["title"] KIND: message, file, asset, branch, link, record
|
|
24
24
|
binder detach ID WHAT
|
|
25
25
|
binder archive ID | binder restore ID
|
|
26
|
+
binder new TEMPLATE ["title"] a list of your own, when none you were given fits
|
|
27
|
+
TEMPLATE: batches, development, customers, people, roadmap, simple
|
|
26
28
|
binder serve [--store DIR] [--port N] [--side FOLDER=NAME]
|
|
27
29
|
keep a binder yourself, with its page (no application)
|
|
28
30
|
|
|
@@ -33,7 +35,7 @@ You never approve: a person ticks what may go on. Talking with them stays in the
|
|
|
33
35
|
The folder used is --dir FOLDER, else $BINDER_DIR, else the nearest ${FOLDER} folder from here up.
|
|
34
36
|
binder ${VERSION}`;
|
|
35
37
|
|
|
36
|
-
const VERBS = ['lists', 'show', 'get', 'search', 'add', 'edit', 'move', 'note', 'attach', 'detach', 'archive', 'restore'];
|
|
38
|
+
const VERBS = ['lists', 'show', 'get', 'search', 'add', 'edit', 'move', 'note', 'attach', 'detach', 'archive', 'restore', 'new'];
|
|
37
39
|
|
|
38
40
|
/* A command line, read: [op, args] or a refusal. */
|
|
39
41
|
export function read(argv) {
|
|
@@ -62,6 +64,7 @@ export function read(argv) {
|
|
|
62
64
|
case 'note': return rest.length >= 2 ? ['note', { id: rest[0], text: rest.slice(1).join(' ') }] : { refused: 'binder note ID "text"' };
|
|
63
65
|
case 'attach': return rest.length >= 3 ? ['attach', { id: rest[0], kind: rest[1], ref: rest[2], title: rest.slice(3).join(' ') }] : { refused: 'binder attach ID KIND REF ["title"]' };
|
|
64
66
|
case 'detach': return rest.length === 2 ? ['detach', { id: rest[0], what: rest[1] }] : { refused: 'binder detach ID WHAT' };
|
|
67
|
+
case 'new': return rest[0] ? ['create-list', { template: rest[0], title: rest.slice(1).join(' ') || undefined }] : { refused: 'binder new TEMPLATE ["title"]' };
|
|
65
68
|
case 'archive': case 'restore': return rest[0] ? ['archive', { id: rest[0], restore: verb === 'restore' }] : { refused: `binder ${verb} ID` };
|
|
66
69
|
default: return { refused: 'not understood' };
|
|
67
70
|
}
|
package/docs/protocol.md
CHANGED
|
@@ -49,9 +49,28 @@ person, what the record is and why; Markdown-ish (paragraphs, `- ` lists,
|
|
|
49
49
|
`**bold**`, `[text](https://...)` and bare http(s) links), 4000 characters at
|
|
50
50
|
most, empty to remove it. It is in the history like any other change.
|
|
51
51
|
|
|
52
|
-
A person's alone (from the screen): `approve`, `delete`, `
|
|
53
|
-
`
|
|
54
|
-
|
|
52
|
+
A person's alone (from the screen): `approve`, `delete`, `edit-list`,
|
|
53
|
+
`grant`, `archive-list`, `delete-list`; a note is changed by whoever wrote it
|
|
54
|
+
(`edit-note`). `create-list` is a person's, and an agent's from a template
|
|
55
|
+
(`binder new TEMPLATE ["title"]`, MCP `new-list`): the list is given to that
|
|
56
|
+
agent alone, to change, and a person sees it as any; per agent, as many such
|
|
57
|
+
lists not archived as the store's setting `agentLists` says.
|
|
58
|
+
|
|
59
|
+
## The store's settings
|
|
60
|
+
|
|
61
|
+
A person's alone to change; anyone reads them. Each change is a line of the
|
|
62
|
+
log, op `edit-settings`, with what changed (`change`), and no list or record.
|
|
63
|
+
|
|
64
|
+
| Setting | What it is | Range | By default |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `agentLists` | lists an agent may have made and not archived | 0 to 50 (0: an agent makes none) | 5 |
|
|
67
|
+
|
|
68
|
+
| Op (keeper) | Args | Gives |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `settings` | none | `{agentLists}` |
|
|
71
|
+
| `edit-settings` | `{agentLists}` | `{agentLists}`; an agent is refused |
|
|
72
|
+
|
|
73
|
+
An agent at the limit is refused, and told the number.
|
|
55
74
|
|
|
56
75
|
## The application's side
|
|
57
76
|
|
|
@@ -64,6 +83,10 @@ whoever wrote it (`edit-note`).
|
|
|
64
83
|
|
|
65
84
|
And the screen: `mount(element, { call, words, lang, people, picture, openAttached })`
|
|
66
85
|
from `clavarium-binder/ui`, with `ui/binder-ui.css` and `ui/texts/*.json`.
|
|
86
|
+
It gives back `refresh()`, `destroy()`, and `open(id)`: one record shown
|
|
87
|
+
whole, in its list, from elsewhere on the page (a list beside a chat);
|
|
88
|
+
it resolves `false`, saying nothing on the screen, for a record not in a
|
|
89
|
+
list shown.
|
|
67
90
|
|
|
68
91
|
What a card shows: what is set, nothing about what is empty. The fields set
|
|
69
92
|
make one line under its title; its state is said unless the card is in the
|
package/host/binder-host.mjs
CHANGED
|
@@ -71,6 +71,8 @@ export function say(op, data, list) {
|
|
|
71
71
|
case 'attach': return 'Attached.';
|
|
72
72
|
case 'detach': return 'Taken off.';
|
|
73
73
|
case 'archive': return data.archived ? `Archived #${data.id}.` : `Restored #${data.id}.`;
|
|
74
|
+
case 'settings': case 'edit-settings': return `Lists an agent may make: ${data.agentLists}.`;
|
|
75
|
+
case 'create-list': return `Made the list ${data.title} [${data.id}], yours to change: binder add ${data.id} "title".`;
|
|
74
76
|
default: return 'Done.';
|
|
75
77
|
}
|
|
76
78
|
}
|
|
@@ -104,6 +106,8 @@ export function keeper(dir, { sides = () => [], now = () => Date.now() } = {}) {
|
|
|
104
106
|
case 'grant': data = store.grant(a.list, a.to, a.access ?? null, who); break;
|
|
105
107
|
case 'archive-list': data = store.archiveList(a.list, a.archived !== false, who); break;
|
|
106
108
|
case 'delete-list': data = store.deleteList(a.list, who); break;
|
|
109
|
+
case 'settings': data = store.settings(who); break;
|
|
110
|
+
case 'edit-settings': data = store.editSettings({ agentLists: a.agentLists }, who); break;
|
|
107
111
|
case 'templates': data = Object.fromEntries(Object.entries(TEMPLATES).map(([k, t]) => [k, t.name])); break;
|
|
108
112
|
default: return { ok: false, text: `there is no ${oneLine(op, 40)}` };
|
|
109
113
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium-binder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2-next.2",
|
|
4
4
|
"description": "Clavarium Binder: lists of records kept together by people and AI agents, each with its id (a command and an MCP server for agents, a keeper and a screen for the application). No dependency.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/clavarium/clavarium-binder#readme",
|
package/server/binder.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
23
23
|
import { randomUUID } from 'node:crypto';
|
|
24
24
|
import { createInterface } from 'node:readline';
|
|
25
25
|
|
|
26
|
-
export const VERSION = '0.1.
|
|
26
|
+
export const VERSION = '0.1.2-next.2';
|
|
27
27
|
export const FOLDER = '.binder';
|
|
28
28
|
/* How long an ask waits for its answer. The keeper answers within a second or two. */
|
|
29
29
|
export const WAIT_MS = 20_000;
|
|
@@ -73,6 +73,7 @@ export const TOOLS = [
|
|
|
73
73
|
{ name: 'note', description: 'Add a short note to a record: a decision, a fact, a result. Talking with the person stays in the chat.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, text: { type: 'string' } }, required: ['id', 'text'] } },
|
|
74
74
|
{ name: 'attach', description: 'Attach something to a record: a message of the chat, a file, an asset, a branch, a link, another record.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, kind: { type: 'string', enum: ['message', 'file', 'asset', 'branch', 'link', 'record'] }, ref: S('what it is: a path, a URL, repository:branch, a message id, a record number'), title: { type: 'string' } }, required: ['id', 'kind', 'ref'] } },
|
|
75
75
|
{ name: 'detach', description: 'Take back something attached to a record.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, what: S('what get shows of it: its ref or its id') }, required: ['id', 'what'] } },
|
|
76
|
+
{ name: 'new-list', description: 'Make a list from a template (batches, development, customers, people, roadmap, simple) when none you were given fits: it is given to you alone, and the person sees it. Batches: the work you propose; the person ticks what you may start.', inputSchema: { type: 'object', properties: { template: S('batches, development, customers, people, roadmap or simple'), title: S('its name; the template\'s by default') }, required: ['template'] } },
|
|
76
77
|
{ name: 'archive', description: 'Archive a record (it leaves the list; found again with archived), or restore it.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, restore: { type: 'boolean' } }, required: ['id'] } },
|
|
77
78
|
];
|
|
78
79
|
/* A tool's arguments as the keeper reads them. */
|
|
@@ -90,6 +91,7 @@ export function asked(name, a = {}) {
|
|
|
90
91
|
case 'attach': return ['attach', { id: a.id, kind: a.kind, ref: a.ref, title: a.title }];
|
|
91
92
|
case 'detach': return ['detach', { id: a.id, what: a.what }];
|
|
92
93
|
case 'archive': return ['archive', { id: a.id, restore: !!a.restore }];
|
|
94
|
+
case 'new-list': return ['create-list', { template: a.template, title: a.title }];
|
|
93
95
|
default: return null;
|
|
94
96
|
}
|
|
95
97
|
}
|
package/server/store.mjs
CHANGED
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
* WHO is { id, kind, name }: kind "person" or "agent". A person may do
|
|
16
16
|
* anything. An agent reads or writes a list only where it was given access, and
|
|
17
17
|
* never approves: a record reaches a gate state (or one after it) only once a
|
|
18
|
-
* person approved it.
|
|
18
|
+
* person approved it. An agent may make a list from a template (as many as a
|
|
19
|
+
* person set, 5 unless they said): it is given that list alone, to change, and
|
|
20
|
+
* a person sees it as any.
|
|
21
|
+
*
|
|
22
|
+
* SETTINGS: the store's own, a person's alone to change, in the history too.
|
|
23
|
+
* agentLists lists an agent may have made and not archived (0 to 50; 0: none)
|
|
19
24
|
*/
|
|
20
25
|
import { readFileSync, writeFileSync, appendFileSync, renameSync, mkdirSync, existsSync, lstatSync } from 'node:fs';
|
|
21
26
|
import { join } from 'node:path';
|
|
@@ -23,6 +28,11 @@ import { randomUUID } from 'node:crypto';
|
|
|
23
28
|
import { seed, TEMPLATES, VIEWS, FIELD_TYPES } from './templates.mjs';
|
|
24
29
|
|
|
25
30
|
export const TITLE_MAX = 200;
|
|
31
|
+
/* Lists an agent may have made and not archived: 5 until a person sets it (settings.agentLists), 0 to 50. */
|
|
32
|
+
export const AGENT_LISTS_DEFAULT = 5;
|
|
33
|
+
export const AGENT_LISTS_TOP = 50;
|
|
34
|
+
/* The default, by its former name. */
|
|
35
|
+
export const AGENT_LISTS_MAX = AGENT_LISTS_DEFAULT;
|
|
26
36
|
export const NOTE_MAX = 2000;
|
|
27
37
|
/* A record's description: a few lines for a person, what it is and why (Markdown-ish: paragraphs, "- " lists, **bold**, links). */
|
|
28
38
|
export const DESCRIPTION_MAX = 4000;
|
|
@@ -114,6 +124,9 @@ export function openStore(dir, { now = () => Date.now() } = {}) {
|
|
|
114
124
|
count: Object.values(s.records).filter((r) => r.list === l.id && !r.archived).length });
|
|
115
125
|
|
|
116
126
|
/* Whether a record's title, description or notes hold `q` (lower case). */
|
|
127
|
+
/* The store's settings, as they are now (a store from before them has the defaults). */
|
|
128
|
+
const settingsNow = () => ({ agentLists: Number.isInteger(s.settings?.agentLists) ? s.settings.agentLists : AGENT_LISTS_DEFAULT });
|
|
129
|
+
|
|
117
130
|
const said = (r, q) => r.title.toLowerCase().includes(q) || (r.description ?? '').toLowerCase().includes(q) || r.notes.some((n) => n.text.toLowerCase().includes(q));
|
|
118
131
|
|
|
119
132
|
const api = {
|
|
@@ -163,16 +176,21 @@ export function openStore(dir, { now = () => Date.now() } = {}) {
|
|
|
163
176
|
return out;
|
|
164
177
|
},
|
|
165
178
|
|
|
166
|
-
/* ---- lists: a person's ---- */
|
|
179
|
+
/* ---- lists: a person's; an agent makes one too, from a template, given to itself alone ---- */
|
|
167
180
|
createList({ template = 'simple', title, lang = 'en' } = {}, who) {
|
|
168
181
|
const w = whoOf(who);
|
|
169
|
-
if (!isPerson(w)) no('only a person makes a list');
|
|
170
182
|
if (Object.keys(s.lists).length >= LISTS_MAX) no(`${LISTS_MAX} lists at most`);
|
|
183
|
+
if (!isPerson(w)) {
|
|
184
|
+
if (!Object.hasOwn(TEMPLATES, String(template))) no(`a list is made from a template: ${Object.keys(TEMPLATES).join(', ')}`);
|
|
185
|
+
const most = settingsNow().agentLists;
|
|
186
|
+
if (most === 0) no('a person set 0 lists an agent may make: use one you were given');
|
|
187
|
+
if (Object.values(s.lists).filter((l) => l.by === w.id && !l.archived).length >= most) no(`${most} lists made by you at most, as a person set: use one of yours`);
|
|
188
|
+
}
|
|
171
189
|
const base = seed(template, lang);
|
|
172
190
|
const name = oneLine(title, TITLE_MAX) || base.title;
|
|
173
191
|
let id = key(name) || 'list';
|
|
174
192
|
for (let i = 2; s.lists[id]; i++) id = `${key(name) || 'list'}-${i}`;
|
|
175
|
-
s.lists[id] = { id, ...base, title: name, access: {}, created: stamp(), by: w.id };
|
|
193
|
+
s.lists[id] = { id, ...base, title: name, access: isPerson(w) ? {} : { [w.id]: 'write' }, created: stamp(), by: w.id };
|
|
176
194
|
save(); log(w, 'create-list', { list: id, change: { title: name, template } });
|
|
177
195
|
return s.lists[id];
|
|
178
196
|
},
|
|
@@ -241,6 +259,23 @@ export function openStore(dir, { now = () => Date.now() } = {}) {
|
|
|
241
259
|
return true;
|
|
242
260
|
},
|
|
243
261
|
|
|
262
|
+
/* ---- the store's settings: anyone reads them, a person changes them ---- */
|
|
263
|
+
settings(who) { whoOf(who); return settingsNow(); },
|
|
264
|
+
editSettings({ agentLists } = {}, who) {
|
|
265
|
+
const w = whoOf(who);
|
|
266
|
+
if (!isPerson(w)) no('only a person changes the settings');
|
|
267
|
+
const change = {};
|
|
268
|
+
if (agentLists !== undefined) {
|
|
269
|
+
const n = typeof agentLists === 'string' && agentLists.trim() !== '' ? Number(agentLists) : agentLists;
|
|
270
|
+
if (!Number.isInteger(n) || n < 0 || n > AGENT_LISTS_TOP) no(`lists an agent may make: a whole number from 0 to ${AGENT_LISTS_TOP}`);
|
|
271
|
+
change.agentLists = n;
|
|
272
|
+
}
|
|
273
|
+
if (!Object.keys(change).length) return settingsNow();
|
|
274
|
+
s.settings = { ...s.settings, ...change };
|
|
275
|
+
save(); log(w, 'edit-settings', { change });
|
|
276
|
+
return settingsNow();
|
|
277
|
+
},
|
|
278
|
+
|
|
244
279
|
/* ---- records ---- */
|
|
245
280
|
add(given, { title, description, state, fields, owners, due, note } = {}, who) {
|
|
246
281
|
const w = whoOf(who);
|
package/ui/binder-ui.css
CHANGED
|
@@ -149,6 +149,7 @@ button.binder-who:hover span:not(.binder-dot) { text-decoration: underline; text
|
|
|
149
149
|
.binder-history { display: flex; flex-direction: column; gap: .3rem; margin-bottom: 1rem; }
|
|
150
150
|
.binder-box { border: 1px solid var(--binder-line); border-radius: var(--binder-radius); background: var(--binder-panel); padding: .7rem .9rem; display: flex; flex-direction: column; gap: .55rem; min-width: 0; }
|
|
151
151
|
.binder-box h3 { font-size: .75rem; letter-spacing: .06em; text-transform: uppercase; color: var(--binder-dim); display: flex; justify-content: space-between; }
|
|
152
|
+
.binder-box input[data-setting] { width: 6em; align-self: flex-start; }
|
|
152
153
|
.binder-log { color: var(--binder-dim); font-size: .85em; }
|
|
153
154
|
.binder-log time { font-variant-numeric: tabular-nums; margin-right: .6em; }
|
|
154
155
|
.binder .binder-warn { color: var(--binder-danger); }
|
package/ui/binder-ui.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* openAttached: (a) => void, a thing attached opened by the page (a message, a file...)
|
|
12
12
|
* });
|
|
13
13
|
* b.refresh(); something changed: read again
|
|
14
|
+
* b.open(id); a record shown whole, in its list (resolves false when it is not in a list shown)
|
|
14
15
|
* b.destroy();
|
|
15
16
|
*
|
|
16
17
|
* Built element by element: nothing a record holds is ever read as HTML.
|
|
@@ -21,7 +22,7 @@ export const KEYS = Object.freeze(['lists', 'new-list', 'new-item', 'no-lists',
|
|
|
21
22
|
'states', 'fields', 'access', 'read', 'write', 'none', 'save', 'empty', 'archived', 'show-archived', 'gate', 'done', 'add-state', 'add-field',
|
|
22
23
|
'kind-message', 'kind-file', 'kind-asset', 'kind-branch', 'kind-link', 'kind-record', 'what', 'name', 'type', 'saved', 'edited', 'by', 'mine', 'all',
|
|
23
24
|
'delete-list', 'delete-list-sure', 'archive-list', 'no-records', 'search', 'description', 'edit', 'more', 'approve', 'add', 'ok',
|
|
24
|
-
'add-what', 'owner', 'attachment', 'tools', 'cover',
|
|
25
|
+
'add-what', 'owner', 'attachment', 'tools', 'cover', 'agent-lists',
|
|
25
26
|
'op-add', 'op-edit', 'op-move', 'op-note', 'op-edit-note', 'op-remove-note', 'op-attach', 'op-detach', 'op-archive', 'op-restore', 'op-delete']);
|
|
26
27
|
const KINDS = ['message', 'file', 'asset', 'branch', 'link', 'record'];
|
|
27
28
|
const ICON = { message: '💬', file: '📄', asset: '🗄', branch: '⑂', link: '🔗', record: '▣' };
|
|
@@ -161,7 +162,7 @@ export function rich(text) {
|
|
|
161
162
|
export function mount(root, { call, words = {}, lang = 'en', people = () => [], picture = () => null, openAttached = null } = {}) {
|
|
162
163
|
const w = (k) => words[k] ?? k;
|
|
163
164
|
/* open: what of a record is being changed ('states', 'who', 'description', 'attach'); draft: what was typed, kept across a repaint. */
|
|
164
|
-
const ui = { lists: [], at: null, data: null, view: null, record: null, mode: 'list', archived: false, mine: false, error: '', open: null, more: false, draft: {}, focus: false };
|
|
165
|
+
const ui = { lists: [], at: null, data: null, view: null, record: null, mode: 'list', archived: false, mine: false, error: '', open: null, more: false, draft: {}, focus: false, settings: null };
|
|
165
166
|
const box = h('div', { class: 'binder' });
|
|
166
167
|
root.replaceChildren(box);
|
|
167
168
|
let alive = true;
|
|
@@ -193,7 +194,7 @@ export function mount(root, { call, words = {}, lang = 'en', people = () => [],
|
|
|
193
194
|
const menu = ui.open === 'tools' ? h('div', { class: 'binder-menu binder-menu-end', role: 'menu' },
|
|
194
195
|
item(ui.mine, w('mine'), 'mine', () => flip('mine')),
|
|
195
196
|
item(ui.archived, w('archived'), 'archived', () => flip('archived')),
|
|
196
|
-
l.access === 'write' && l.grants ? item(null, w('settings'), 'settings', () => { ui.open = null; ui.mode = 'settings'; paint(); }) : null) : null;
|
|
197
|
+
l.access === 'write' && l.grants ? item(null, w('settings'), 'settings', () => { ui.open = null; ui.mode = 'settings'; paint(); void readSettings(); }) : null) : null;
|
|
197
198
|
const chip = (k) => (ui[k] ? h('button', { type: 'button', class: 'binder-chip', 'aria-pressed': 'true', 'data-act': `off-${k}`, onclick: () => flip(k) }, w(k === 'mine' ? 'mine' : 'archived'), h('span', { 'aria-hidden': 'true' }, ' ×')) : null);
|
|
198
199
|
return h('div', { class: 'binder-bar' },
|
|
199
200
|
h('h2', {}, l.title), chip('mine'), chip('archived'),
|
|
@@ -422,6 +423,11 @@ export function mount(root, { call, words = {}, lang = 'en', people = () => [],
|
|
|
422
423
|
attached, addRow, notes, more);
|
|
423
424
|
}
|
|
424
425
|
|
|
426
|
+
/* The store's settings, read when its page opens; asked quietly (a keeper without them shows none). */
|
|
427
|
+
async function readSettings() {
|
|
428
|
+
try { const r = await call('settings', {}); ui.settings = r?.ok ? r.data : null; } catch { ui.settings = null; }
|
|
429
|
+
if (ui.mode === 'settings') paint();
|
|
430
|
+
}
|
|
425
431
|
function settings(l) {
|
|
426
432
|
const title = h('input', { type: 'text', value: l.title, 'aria-label': w('title') });
|
|
427
433
|
const states = l.states.map((s) => ({ ...s }));
|
|
@@ -448,12 +454,18 @@ export function mount(root, { call, words = {}, lang = 'en', people = () => [],
|
|
|
448
454
|
const sure = h('span', { class: 'binder-sure', hidden: true }, w('delete-list-sure'),
|
|
449
455
|
h('button', { type: 'button', class: 'binder-danger', onclick: () => void soft(ask('delete-list', { list: l.id })).then(() => { ui.mode = 'list'; ui.at = null; return refresh(); }) }, w('delete')),
|
|
450
456
|
h('button', { type: 'button', class: 'binder-ghost', onclick: () => { sure.hidden = true; } }, w('cancel')));
|
|
457
|
+
function agentLists() {
|
|
458
|
+
const n = h('input', { type: 'number', min: '0', max: '50', step: '1', inputmode: 'numeric', value: String(ui.settings.agentLists), 'aria-label': w('agent-lists'), 'data-setting': 'agentLists' });
|
|
459
|
+
n.addEventListener('change', () => void soft(ask('edit-settings', { agentLists: n.value })).then((got) => { if (got) ui.settings = got; paint(); }));
|
|
460
|
+
return n;
|
|
461
|
+
}
|
|
451
462
|
return h('div', { class: 'binder-page binder-settings' },
|
|
452
463
|
h('div', { class: 'binder-bar' }, h('button', { type: 'button', class: 'binder-ghost', onclick: () => { ui.mode = 'list'; paint(); } }, `‹ ${l.title}`)),
|
|
453
464
|
h('section', { class: 'binder-box' }, h('h3', {}, w('title')), title),
|
|
454
465
|
h('section', { class: 'binder-box' }, h('h3', {}, w('states')), stateRows),
|
|
455
466
|
h('section', { class: 'binder-box' }, h('h3', {}, w('fields')), fieldRows),
|
|
456
467
|
grants.length ? h('section', { class: 'binder-box' }, h('h3', {}, w('access')), grants) : null,
|
|
468
|
+
ui.settings ? h('section', { class: 'binder-box' }, h('h3', {}, w('agent-lists')), agentLists()) : null,
|
|
457
469
|
h('div', { class: 'binder-actions' },
|
|
458
470
|
h('button', { type: 'button', class: 'binder-ghost', onclick: () => void soft(ask('archive-list', { list: l.id })).then(() => { ui.mode = 'list'; ui.at = null; return refresh(); }) }, w('archive-list')),
|
|
459
471
|
h('button', { type: 'button', class: 'binder-ghost', onclick: () => { sure.hidden = false; } }, w('delete-list')), sure,
|
|
@@ -491,5 +503,20 @@ export function mount(root, { call, words = {}, lang = 'en', people = () => [],
|
|
|
491
503
|
document.addEventListener('pointerdown', away);
|
|
492
504
|
document.addEventListener('keydown', escape);
|
|
493
505
|
void refresh();
|
|
494
|
-
|
|
506
|
+
/* One record opened, from elsewhere on the page (a list beside a chat): its list, then it. */
|
|
507
|
+
async function openRecord(id) {
|
|
508
|
+
if (!alive) return false;
|
|
509
|
+
try {
|
|
510
|
+
/* Asked quietly: a record not found says nothing on the screen. */
|
|
511
|
+
const got = await call('get', { id: String(id ?? '') });
|
|
512
|
+
if (!got?.ok) return false;
|
|
513
|
+
const d = got.data;
|
|
514
|
+
if (!ui.lists.some((l) => l.id === d.list.id)) await load();
|
|
515
|
+
if (!ui.lists.some((l) => l.id === d.list.id)) return false;
|
|
516
|
+
if (ui.at !== d.list.id) { ui.at = d.list.id; ui.view = null; ui.data = await ask('show', { list: ui.at, archived: ui.archived, mine: ui.mine }); }
|
|
517
|
+
leave(); ui.record = d; ui.mode = 'record'; paint();
|
|
518
|
+
return true;
|
|
519
|
+
} catch { return false; }
|
|
520
|
+
}
|
|
521
|
+
return { refresh, open: openRecord, destroy() { alive = false; document.removeEventListener('pointerdown', away); document.removeEventListener('keydown', escape); box.remove(); }, get state() { return { at: ui.at, mode: ui.mode } } };
|
|
495
522
|
}
|
package/ui/texts/en.json
CHANGED