@cmodjs/core 1.5.0 → 1.6.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/docs/commands.md +1 -1
- package/docs/mod.md +8 -5
- package/docs/permissions.md +3 -3
- package/docs/ui.md +1 -1
- package/package.json +1 -1
- package/records.d.ts +3 -1
- package/records.d.ts.map +1 -1
- package/records.js +14 -5
- package/runtime/grants.d.ts +1 -1
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +6 -4
- package/runtime/lifecycle.js +2 -2
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +28 -5
- package/src/records.ts +15 -5
- package/src/runtime/grants.ts +4 -3
- package/src/runtime/lifecycle.ts +2 -2
- package/src/runtime/toaster.ts +27 -5
package/docs/commands.md
CHANGED
|
@@ -251,7 +251,7 @@ Turns one permission of a set-up mod on or off, such as `cmod permission ci-watc
|
|
|
251
251
|
cmod settings <mod> <user|project|local> <json> [--project <folder>]
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
-
Changes Claude Code settings for a set-up mod, as its `settings.user.update`, `settings.project.update`, and `settings.local.update` do ([mod.md](mod.md#settings-claude-code-s-settings-files)), such as `cmod settings
|
|
254
|
+
Changes Claude Code settings for a set-up mod, as its `settings.user.update`, `settings.project.update`, and `settings.local.update` do ([mod.md](mod.md#settings-claude-code-s-settings-files)), such as `cmod settings oneclaude user '{"env":{"ANTHROPIC_BASE_URL":"http://127.0.0.1:8317"}}'`. It merges the JSON object into the user, project, or local settings as a JSON Merge Patch: an object merges into the object already there, `null` removes the key, and an array or any other value replaces it. The project and local settings are those of `--project`, the current folder when it is left out, and `mcpServers` in the project settings goes to `.mcp.json`. Each value needs the mod's `config` permission for its key path or a path above it. cmod keeps every other key and the file's formatting, and `cmod teardown` takes out each value the mod set that is still the same, then each object that leaves empty.
|
|
255
255
|
|
|
256
256
|
### cmod rules
|
|
257
257
|
|
package/docs/mod.md
CHANGED
|
@@ -272,14 +272,17 @@ type Setting = { key: string; label: string; description?: string; kind: 'boolea
|
|
|
272
272
|
`settings.user` is the person's own `~/.claude/settings.json`, `settings.project` is the project's `.claude/settings.json`, and `settings.local` is the project's `.claude/settings.local.json`.
|
|
273
273
|
|
|
274
274
|
- `read` answers the values in that one file, such as `(await settings.user.read())['enabledPlugins']`.
|
|
275
|
-
- `update`
|
|
276
|
-
-
|
|
277
|
-
-
|
|
278
|
-
-
|
|
275
|
+
- `update` merges `values` into that file as a JSON Merge Patch ([RFC 7396](https://www.rfc-editor.org/rfc/rfc7396)). `mcpServers` in `settings.project` goes to the project's `.mcp.json`, where Claude Code reads a project's servers.
|
|
276
|
+
- An object merges into the object already at that key, at every depth, so every key it does not name stays as it was. `null` removes the key at its depth. An array or any other value replaces the value. To replace a whole object, read it and set each key you drop to `null`.
|
|
277
|
+
- The file's comments and formatting stay as they were.
|
|
278
|
+
- Each value needs a `config` permission for its key path or a path above it ([permissions.md](permissions.md)): `"config": ["env.ANTHROPIC_BASE_URL"]` covers that one value, and `"config": ["env"]` covers every key in `env`. `update` rejects with the line to add for a value no grant covers, and changes nothing.
|
|
279
|
+
- Removing the mod takes out each value it set that still equals what it set, then each object that leaves empty. A value the person changed since stays.
|
|
279
280
|
- `fs.write` to a Claude Code settings file rejects and names `update`, because a whole-file write loses the person's own edits and leaves the mod's values behind when it is removed.
|
|
280
281
|
|
|
282
|
+
A mod that points Claude Code at its local proxy sets one variable in `env`, and the person's other variables stay:
|
|
283
|
+
|
|
281
284
|
```ts
|
|
282
|
-
await settings.user.update({
|
|
285
|
+
await settings.user.update({ env: { ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317' } })
|
|
283
286
|
```
|
|
284
287
|
|
|
285
288
|
## model: one question outside the conversation
|
package/docs/permissions.md
CHANGED
|
@@ -24,7 +24,7 @@ They sit in the `package.json` `cmod` key, beside `install` and `keys`, because
|
|
|
24
24
|
| `"model": true` | Ask a model, which uses your plan | `model.complete` |
|
|
25
25
|
| `"tasks": true` | Start tasks that work beside Claude | A session's `startTask`, and a `before(TaskStart)` or `before(TaskEnd)` answer |
|
|
26
26
|
| `"tools": true` | Use and change Claude's tool calls | A session's `callTool`, a `before(ToolCall)` answer, and a listener or a job that changes a tool call's input |
|
|
27
|
-
| `"config": ["
|
|
27
|
+
| `"config": ["env.ANTHROPIC_BASE_URL"]` | Change env.ANTHROPIC_BASE_URL in your Claude Code settings | Each value `settings.user.update`, `settings.project.update`, and `settings.local.update` change. A key path covers its own value and every key below it, so `"env"` covers every key in `env`, and `await settings.user.update({ env: { ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317' } })` needs `"env.ANTHROPIC_BASE_URL"` or `"env"`. `mcpServers` covers the project's `.mcp.json` |
|
|
28
28
|
| `"approve": true` | Approve Claude's tool calls for you | an allow or an ask from the mod's permission rules or hooks, and a `retry`. A deny needs no permission |
|
|
29
29
|
|
|
30
30
|
Reading files, drawing panes and renders, toasts, slash commands, tools of the mod's own, and listeners need no permission. Writing a Claude Code settings file with `fs.write` is refused with every permission, and names the `settings` call to use instead ([mod.md](mod.md#settings-claude-code-s-settings-files)).
|
|
@@ -56,11 +56,11 @@ type Permissions = {
|
|
|
56
56
|
readonly network: { allows(host: string): boolean }
|
|
57
57
|
readonly run: { allows(program: string): boolean }
|
|
58
58
|
readonly files: { allows(path: string): boolean }
|
|
59
|
-
readonly config: { allows(
|
|
59
|
+
readonly config: { allows(keyPath: string): boolean }
|
|
60
60
|
}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`mod.permissions` says whether the person has each permission turned on, so a mod hides a feature instead of failing it. A flag is `true` while it is granted, and `allows` answers for one host, program, path, or settings key
|
|
63
|
+
`mod.permissions` says whether the person has each permission turned on, so a mod hides a feature instead of failing it. A flag is `true` while it is granted, and `allows` answers for one host, program, path, or settings key path, such as `env.ANTHROPIC_BASE_URL`:
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
66
|
if (mod.permissions.model) {
|
package/docs/ui.md
CHANGED
|
@@ -411,7 +411,7 @@ export const indexer = defineMod({
|
|
|
411
411
|
|
|
412
412
|
An error cmod catches for a mod, such as a hook that throws, shows as an error toast that stays, with its buttons on a line under the message:
|
|
413
413
|
|
|
414
|
-
- `Retry` runs the failed step again, where running it again can work: an install, the mod's setup, or a program that stopped.
|
|
414
|
+
- `Retry` runs the failed step again, where running it again can work: an install, the mod's setup, or a program that stopped. The toast stays where it is, and the step's progress shows in place of its description, or of its buttons when it has none, until the try ends.
|
|
415
415
|
- `Report` replaces the toast's buttons with the ways to tell the mod's author, with the keys on the first, and `Back` returns to the buttons. The same failure happening again keeps them open. `Ask Claude to report it` sends this session a prompt to file the report with the person's own tools, and Claude Code asks before each step. `Report it on GitHub (opens your browser)` opens a filled-in issue on the mod's `supportUrl` or `repository` in its `plugin.json`, or `Email the author (opens your mail app)` writes to its `author.email`. `Copy the error` copies the mod's name and version, the `@cmodjs/core` version, the message, the whole cause, and the stack.
|
|
416
416
|
- `Hide` closes the toast and keeps the same failure hidden in later sessions. A new version of the mod, or a different failure, shows again.
|
|
417
417
|
|
package/package.json
CHANGED
package/records.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export declare function readRecord(read: ReadFile, store: string, name: string):
|
|
|
63
63
|
export declare function writeRecord(write: (path: string, text: string) => Promise<void>, store: string, record: InstallRecord): Promise<void>;
|
|
64
64
|
export declare function readSteps(pkg: unknown): Steps | undefined;
|
|
65
65
|
export declare function permissionWords(item: string): string;
|
|
66
|
+
export declare function settingPaths(values: Readonly<Record<string, unknown>>): string[][];
|
|
67
|
+
export declare function isSettingCovered(granted: Iterable<string>, path: string): boolean;
|
|
66
68
|
export declare const settingsPagesMethod = "cmod:settingsPages";
|
|
67
69
|
export declare const changelogPage = "changelog";
|
|
68
70
|
export declare const openPageMethod = "cmod:openPage";
|
|
@@ -72,7 +74,7 @@ export declare function marketplaceOf(root: string, name: string): string | unde
|
|
|
72
74
|
export declare function updatesToTurnOn(marketplace: string | undefined, settings: unknown, known: unknown): string | undefined;
|
|
73
75
|
export declare function consentPath(store: string): string;
|
|
74
76
|
export declare function parseConsent(value: unknown, path: string): Record<string, string[]>;
|
|
75
|
-
export declare const coreVersion = "1.
|
|
77
|
+
export declare const coreVersion = "1.6.0";
|
|
76
78
|
export declare function oldestCmodFor(steps: Steps): string;
|
|
77
79
|
export declare function keyWords(keys: Readonly<Record<string, string>> | undefined): string;
|
|
78
80
|
export declare function scriptPaths(command: string): string[];
|
package/records.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["src/records.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["src/records.ts"],"names":[],"mappings":"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"}
|
package/records.js
CHANGED
|
@@ -4,12 +4,12 @@ export const flagPermissions = ['conversation', 'prompt', 'model', 'tasks', 'too
|
|
|
4
4
|
const permissionNames = [...listPermissions, ...flagPermissions];
|
|
5
5
|
const hostName = /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$/;
|
|
6
6
|
const programName = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
7
|
-
const
|
|
7
|
+
const settingPath = /^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z0-9_@-]+)*$/;
|
|
8
8
|
const listItemHint = {
|
|
9
9
|
network: 'a host alone, such as "api.github.com", or a socket path, such as "/var/run/docker.sock"',
|
|
10
10
|
run: 'a program\'s command, such as "gh"',
|
|
11
11
|
files: 'a path that starts with ~/ or /, such as "~/.zshrc"',
|
|
12
|
-
config: 'a Claude Code setting\'s name, such as "cleanupPeriodDays"',
|
|
12
|
+
config: 'a Claude Code setting\'s name, or a key path inside one, such as "cleanupPeriodDays" or "env.ANTHROPIC_BASE_URL"',
|
|
13
13
|
};
|
|
14
14
|
export const pluginName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
15
15
|
export function storeFolder(env) {
|
|
@@ -141,7 +141,7 @@ function readPermissions(steps) {
|
|
|
141
141
|
throw new Error(`${at} sets "${name}" to ${JSON.stringify(value)}. Write a list, such as "${name}": ${example}.`);
|
|
142
142
|
for (const each of value) {
|
|
143
143
|
const isPath = /^(~\/|\/)/.test(each) && !each.split('/').includes('..');
|
|
144
|
-
const isValid = { network: hostName.test(each) || isPath, run: programName.test(each), files: isPath, config:
|
|
144
|
+
const isValid = { network: hostName.test(each) || isPath, run: programName.test(each), files: isPath, config: settingPath.test(each) }[name];
|
|
145
145
|
if (isValid !== true)
|
|
146
146
|
throw new Error(`${at} lists "${each}" under "${name}". Write ${listItemHint[name]}.`);
|
|
147
147
|
items.push(`${name}:${each}`);
|
|
@@ -177,6 +177,13 @@ export function permissionWords(item) {
|
|
|
177
177
|
return item;
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
+
export function settingPaths(values) {
|
|
181
|
+
return Object.entries(values).flatMap(([key, value]) => (isObject(value) && Object.keys(value).length > 0 ? settingPaths(value).map((path) => [key, ...path]) : [[key]]));
|
|
182
|
+
}
|
|
183
|
+
export function isSettingCovered(granted, path) {
|
|
184
|
+
const item = `config:${path}`;
|
|
185
|
+
return [...granted].some((each) => item === each || item.startsWith(`${each}.`));
|
|
186
|
+
}
|
|
180
187
|
export const settingsPagesMethod = 'cmod:settingsPages';
|
|
181
188
|
export const changelogPage = 'changelog';
|
|
182
189
|
export const openPageMethod = 'cmod:openPage';
|
|
@@ -220,9 +227,11 @@ function readKeys(keys) {
|
|
|
220
227
|
}
|
|
221
228
|
return { keys: commands };
|
|
222
229
|
}
|
|
223
|
-
export const coreVersion = '1.
|
|
230
|
+
export const coreVersion = '1.6.0';
|
|
224
231
|
export function oldestCmodFor(steps) {
|
|
225
|
-
if (steps.permissions?.some((item) => item
|
|
232
|
+
if (steps.permissions?.some((item) => item.startsWith('config:')) === true)
|
|
233
|
+
return '1.6.0';
|
|
234
|
+
if (steps.permissions?.includes('tasks') === true)
|
|
226
235
|
return '1.0.0';
|
|
227
236
|
if (steps.permissions !== undefined)
|
|
228
237
|
return '0.2.0';
|
package/runtime/grants.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare class PermissionRefused extends Error {
|
|
|
16
16
|
export declare function programAddresses(mod: object): Set<string>;
|
|
17
17
|
export declare function checkGrant(grants: Grants, call: string, item: string | undefined): Promise<void>;
|
|
18
18
|
export declare function checkWrite(grants: Grants, call: string, path: string): Promise<void>;
|
|
19
|
-
export declare function checkSettings(grants: Grants, source: 'user' | 'project' | 'local',
|
|
19
|
+
export declare function checkSettings(grants: Grants, source: 'user' | 'project' | 'local', values: Readonly<Record<string, unknown>>): Promise<void>;
|
|
20
20
|
export declare function checkingGrants(claude: Claude, grants: Grants): Claude;
|
|
21
21
|
export declare function itemOf(name: string, value?: string): string;
|
|
22
22
|
export declare function isCovered(granted: ReadonlySet<string>, item: string, home: string | undefined): boolean;
|
package/runtime/grants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/runtime/grants.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAG7C,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,MAAM,CAAA;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,SAAS,MAAM,EAAE,CAAA;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;CAClD,CAAA;AAED,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAI/C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAIzD;AAkBD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAItG;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1F;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/runtime/grants.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAG7C,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,MAAM,CAAA;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,SAAS,MAAM,EAAE,CAAA;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;CAClD,CAAA;AAED,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAI/C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAIzD;AAkBD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAItG;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1F;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAElJ;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAkFrE;AAmBD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAWvG;AAiBD,wBAAgB,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI9G;AA4BD,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1C,CAAA;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAEzD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAa9G;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BxJ"}
|
package/runtime/grants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isObject, permissionWords } from '../records.js';
|
|
1
|
+
import { isObject, isSettingCovered, permissionWords, settingPaths } from '../records.js';
|
|
2
2
|
import { relativePath } from '../utils/paths.js';
|
|
3
3
|
import { cmodPluginName } from './dependencies.js';
|
|
4
4
|
import { actPermission, conversationMethods, isAct, toolPermission } from './sessions.js';
|
|
@@ -34,9 +34,9 @@ export async function checkGrant(grants, call, item) {
|
|
|
34
34
|
export async function checkWrite(grants, call, path) {
|
|
35
35
|
await checkGrant(grants, call, writeItem(grants, path));
|
|
36
36
|
}
|
|
37
|
-
export async function checkSettings(grants, source,
|
|
38
|
-
for (const
|
|
39
|
-
await checkGrant(grants, `settings.${source}.update(${
|
|
37
|
+
export async function checkSettings(grants, source, values) {
|
|
38
|
+
for (const path of settingPaths(values).map((each) => each.join('.')))
|
|
39
|
+
await checkGrant(grants, `settings.${source}.update(${path})`, `config:${path}`);
|
|
40
40
|
}
|
|
41
41
|
export function checkingGrants(claude, grants) {
|
|
42
42
|
const gate = (call, item) => checkGrant(grants, call, item);
|
|
@@ -147,6 +147,8 @@ export function isCovered(granted, item, home) {
|
|
|
147
147
|
if (split === -1)
|
|
148
148
|
return false;
|
|
149
149
|
const name = item.slice(0, split);
|
|
150
|
+
if (name === 'config')
|
|
151
|
+
return isSettingCovered(granted, item.slice(split + 1));
|
|
150
152
|
const target = expanded(item.slice(split + 1), home);
|
|
151
153
|
const values = [...granted].filter((each) => each.startsWith(`${name}:`)).map((each) => expanded(each.slice(name.length + 1), home));
|
|
152
154
|
if (name === 'run')
|
package/runtime/lifecycle.js
CHANGED
|
@@ -301,7 +301,7 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
301
301
|
claude().ui.log(`${name} waits for consent to run ${event.install}. Run cmod install ${name} in a terminal.`);
|
|
302
302
|
return;
|
|
303
303
|
}
|
|
304
|
-
|
|
304
|
+
runtime?.toaster.toast.info('Installing', { id: installId, description: 'Waiting for your answer', duration: Infinity });
|
|
305
305
|
const updates = await updatesToShow().catch((error) => {
|
|
306
306
|
toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error });
|
|
307
307
|
return undefined;
|
|
@@ -645,7 +645,7 @@ async function createMod(definition, runtime) {
|
|
|
645
645
|
const settingsFile = (source) => ({
|
|
646
646
|
read: () => checked.settings.read({ source }),
|
|
647
647
|
async update(values) {
|
|
648
|
-
await checkSettings(grants, source,
|
|
648
|
+
await checkSettings(grants, source, values);
|
|
649
649
|
await claude.cmod.call({ to: cmodPluginName, method: 'updateSettings', input: { mod: definition.name, source, project: modState.root, values } });
|
|
650
650
|
},
|
|
651
651
|
});
|
package/runtime/toaster.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toaster.d.ts","sourceRoot":"","sources":["../src/runtime/toaster.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAS,MAAM,aAAa,CAAA;AACpF,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAgB,SAAS,EAAE,MAAM,WAAW,CAAA;AAIxE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9G,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,OAAO,CAAA;CAAE,CAAA;AAEhK,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAA;IACrC,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CAAA;CACnH,CAAA;
|
|
1
|
+
{"version":3,"file":"toaster.d.ts","sourceRoot":"","sources":["../src/runtime/toaster.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAS,MAAM,aAAa,CAAA;AACpF,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAgB,SAAS,EAAE,MAAM,WAAW,CAAA;AAIxE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9G,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,OAAO,CAAA;CAAE,CAAA;AAEhK,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAA;IACrC,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CAAA;CACnH,CAAA;AAuCD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CA6PtG"}
|
package/runtime/toaster.js
CHANGED
|
@@ -32,7 +32,7 @@ export function createToaster(claude, name, version) {
|
|
|
32
32
|
let showsAll = false;
|
|
33
33
|
let bandId;
|
|
34
34
|
const changed = () => {
|
|
35
|
-
const isLoading = [...entries.values()].some((entry) => entry.type === 'loading');
|
|
35
|
+
const isLoading = [...entries.values()].some((entry) => entry.type === 'loading' || entry.trying !== undefined);
|
|
36
36
|
if (isLoading && spinning === undefined) {
|
|
37
37
|
spinning = claude.clock.every(spinnerMs, () => {
|
|
38
38
|
frame += 1;
|
|
@@ -75,6 +75,11 @@ export function createToaster(claude, name, version) {
|
|
|
75
75
|
const put = (type, message, options, actions, hides) => {
|
|
76
76
|
const id = options.id ?? (lastId += 1);
|
|
77
77
|
const previous = entries.get(id);
|
|
78
|
+
if (previous?.trying !== undefined && type === 'loading') {
|
|
79
|
+
previous.trying = options.description ?? message;
|
|
80
|
+
changed();
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
78
83
|
if (previous !== undefined)
|
|
79
84
|
disarm(previous);
|
|
80
85
|
const reporting = hides !== undefined && previous?.hides === hides ? previous.reporting : undefined;
|
|
@@ -149,7 +154,23 @@ export function createToaster(claude, name, version) {
|
|
|
149
154
|
if ('deny' in focused)
|
|
150
155
|
claude.ui.log(`${name}: the Report choices did not take the keys: ${String(focused.deny)}`, { to: 'debug' });
|
|
151
156
|
};
|
|
152
|
-
const
|
|
157
|
+
const tryAgain = async () => {
|
|
158
|
+
const shown = entries.get(id);
|
|
159
|
+
if (shown === undefined || retry === undefined || shown.trying !== undefined)
|
|
160
|
+
return;
|
|
161
|
+
shown.trying = 'Starting';
|
|
162
|
+
changed();
|
|
163
|
+
try {
|
|
164
|
+
await retry();
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
if (entries.get(id) === shown) {
|
|
168
|
+
shown.trying = undefined;
|
|
169
|
+
changed();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const actions = [...(retry === undefined ? [] : [{ label: 'Retry', onClick: tryAgain }]), { label: 'Report', onClick: startReporting }];
|
|
153
174
|
return put('error', message, { id, ...(description === undefined ? {} : { description }), duration: Infinity }, actions, hides);
|
|
154
175
|
};
|
|
155
176
|
const toggle = (entry) => () => {
|
|
@@ -160,14 +181,16 @@ export function createToaster(claude, name, version) {
|
|
|
160
181
|
const row = (entry) => {
|
|
161
182
|
const { description } = entry.options;
|
|
162
183
|
const isText = typeof description === 'string';
|
|
163
|
-
const icon = entry.type === 'loading' ? { glyph: spinner[frame % spinner.length], color: 'claude' } : icons[entry.type];
|
|
184
|
+
const icon = entry.type === 'loading' || entry.trying !== undefined ? { glyph: spinner[frame % spinner.length], color: 'claude' } : icons[entry.type];
|
|
164
185
|
const message = isText ? Button({ plain: true, key: `toast:${entry.id}:open`, label: entry.message, onPress: toggle(entry) }) : Text({ children: entry.message });
|
|
165
186
|
const details = !isText ? description : Text({ dimColor: true, wrap: entry.isOpen ? 'wrap' : 'truncate-end', children: entry.isOpen ? description : firstLine(description) });
|
|
166
187
|
const fixed = (child) => Box({ flexShrink: 0, children: child });
|
|
167
188
|
const close = Button({ plain: true, dimColor: true, key: `toast:${entry.id}:close`, label: entry.hides === undefined ? '✕' : 'Hide', onPress: () => hide(entry) });
|
|
168
189
|
const actions = entry.actions.map((action, index) => Button({ plain: true, key: `toast:${entry.id}:${index === 0 ? 'action' : `action-${index}`}`, label: action.label, onPress: press(entry, action.onClick) }));
|
|
169
|
-
const
|
|
170
|
-
const
|
|
190
|
+
const buttons = Box({ gap: 2, flexWrap: 'wrap', children: [...actions, close] });
|
|
191
|
+
const trying = entry.trying === undefined ? undefined : Box({ gap: 1, children: [Text({ dimColor: true, children: 'Trying again:' }), typeof entry.trying === 'string' ? Text({ dimColor: true, wrap: 'truncate-end', children: entry.trying }) : entry.trying] });
|
|
192
|
+
const text = details === undefined ? [message] : [message, trying ?? details];
|
|
193
|
+
const under = trying !== undefined && details === undefined ? [trying] : entry.reporting === undefined ? [buttons] : reportChoices(entry, entry.reporting);
|
|
171
194
|
return Box({
|
|
172
195
|
gap: 2,
|
|
173
196
|
children: [
|
package/src/records.ts
CHANGED
|
@@ -26,12 +26,12 @@ export type FlagPermission = (typeof flagPermissions)[number]
|
|
|
26
26
|
const permissionNames: readonly string[] = [...listPermissions, ...flagPermissions]
|
|
27
27
|
const hostName = /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$/
|
|
28
28
|
const programName = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/
|
|
29
|
-
const
|
|
29
|
+
const settingPath = /^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z0-9_@-]+)*$/
|
|
30
30
|
const listItemHint: Readonly<Record<ListPermission, string>> = {
|
|
31
31
|
network: 'a host alone, such as "api.github.com", or a socket path, such as "/var/run/docker.sock"',
|
|
32
32
|
run: 'a program\'s command, such as "gh"',
|
|
33
33
|
files: 'a path that starts with ~/ or /, such as "~/.zshrc"',
|
|
34
|
-
config: 'a Claude Code setting\'s name, such as "cleanupPeriodDays"',
|
|
34
|
+
config: 'a Claude Code setting\'s name, or a key path inside one, such as "cleanupPeriodDays" or "env.ANTHROPIC_BASE_URL"',
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export type ReadFile = (path: string) => Promise<string | undefined>
|
|
@@ -165,7 +165,7 @@ function readPermissions(steps: Record<string, unknown>): string[] {
|
|
|
165
165
|
if (!Array.isArray(value) || value.length === 0 || !value.every((each) => typeof each === 'string')) throw new Error(`${at} sets "${name}" to ${JSON.stringify(value)}. Write a list, such as "${name}": ${example}.`)
|
|
166
166
|
for (const each of value) {
|
|
167
167
|
const isPath = /^(~\/|\/)/.test(each) && !each.split('/').includes('..')
|
|
168
|
-
const isValid = { network: hostName.test(each) || isPath, run: programName.test(each), files: isPath, config:
|
|
168
|
+
const isValid = { network: hostName.test(each) || isPath, run: programName.test(each), files: isPath, config: settingPath.test(each) }[name]
|
|
169
169
|
if (isValid !== true) throw new Error(`${at} lists "${each}" under "${name}". Write ${listItemHint[name as ListPermission]}.`)
|
|
170
170
|
items.push(`${name}:${each}`)
|
|
171
171
|
}
|
|
@@ -202,6 +202,15 @@ export function permissionWords(item: string): string {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
export function settingPaths(values: Readonly<Record<string, unknown>>): string[][] {
|
|
206
|
+
return Object.entries(values).flatMap(([key, value]) => (isObject(value) && Object.keys(value).length > 0 ? settingPaths(value).map((path) => [key, ...path]) : [[key]]))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function isSettingCovered(granted: Iterable<string>, path: string): boolean {
|
|
210
|
+
const item = `config:${path}`
|
|
211
|
+
return [...granted].some((each) => item === each || item.startsWith(`${each}.`))
|
|
212
|
+
}
|
|
213
|
+
|
|
205
214
|
export const settingsPagesMethod = 'cmod:settingsPages'
|
|
206
215
|
|
|
207
216
|
export const changelogPage = 'changelog'
|
|
@@ -250,10 +259,11 @@ function readKeys(keys: unknown): Steps {
|
|
|
250
259
|
return { keys: commands }
|
|
251
260
|
}
|
|
252
261
|
|
|
253
|
-
export const coreVersion = '1.
|
|
262
|
+
export const coreVersion = '1.6.0'
|
|
254
263
|
|
|
255
264
|
export function oldestCmodFor(steps: Steps): string {
|
|
256
|
-
if (steps.permissions?.some((item) => item
|
|
265
|
+
if (steps.permissions?.some((item) => item.startsWith('config:')) === true) return '1.6.0'
|
|
266
|
+
if (steps.permissions?.includes('tasks') === true) return '1.0.0'
|
|
257
267
|
if (steps.permissions !== undefined) return '0.2.0'
|
|
258
268
|
return Object.keys(steps.keys ?? {}).length > 0 ? '0.1.12' : '0.0.0'
|
|
259
269
|
}
|
package/src/runtime/grants.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isObject, permissionWords } from '../records.js'
|
|
1
|
+
import { isObject, isSettingCovered, permissionWords, settingPaths } from '../records.js'
|
|
2
2
|
import { relativePath } from '../utils/paths.js'
|
|
3
3
|
import type { Claude } from './claude.js'
|
|
4
4
|
import { cmodPluginName } from './dependencies.js'
|
|
@@ -53,8 +53,8 @@ export async function checkWrite(grants: Grants, call: string, path: string): Pr
|
|
|
53
53
|
await checkGrant(grants, call, writeItem(grants, path))
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export async function checkSettings(grants: Grants, source: 'user' | 'project' | 'local',
|
|
57
|
-
for (const
|
|
56
|
+
export async function checkSettings(grants: Grants, source: 'user' | 'project' | 'local', values: Readonly<Record<string, unknown>>): Promise<void> {
|
|
57
|
+
for (const path of settingPaths(values).map((each) => each.join('.'))) await checkGrant(grants, `settings.${source}.update(${path})`, `config:${path}`)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function checkingGrants(claude: Claude, grants: Grants): Claude {
|
|
@@ -167,6 +167,7 @@ export function isCovered(granted: ReadonlySet<string>, item: string, home: stri
|
|
|
167
167
|
const split = item.indexOf(':')
|
|
168
168
|
if (split === -1) return false
|
|
169
169
|
const name = item.slice(0, split)
|
|
170
|
+
if (name === 'config') return isSettingCovered(granted, item.slice(split + 1))
|
|
170
171
|
const target = expanded(item.slice(split + 1), home)
|
|
171
172
|
const values = [...granted].filter((each) => each.startsWith(`${name}:`)).map((each) => expanded(each.slice(name.length + 1), home))
|
|
172
173
|
if (name === 'run') return values.includes('*') || values.includes(target)
|
package/src/runtime/lifecycle.ts
CHANGED
|
@@ -367,7 +367,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
367
367
|
claude().ui.log(`${name} waits for consent to run ${event.install}. Run cmod install ${name} in a terminal.`)
|
|
368
368
|
return
|
|
369
369
|
}
|
|
370
|
-
|
|
370
|
+
runtime?.toaster.toast.info('Installing', { id: installId, description: 'Waiting for your answer', duration: Infinity })
|
|
371
371
|
const updates = await updatesToShow().catch((error: unknown) => {
|
|
372
372
|
toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error })
|
|
373
373
|
return undefined
|
|
@@ -685,7 +685,7 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
685
685
|
const settingsFile = (source: 'user' | 'project' | 'local'): SettingsFile => ({
|
|
686
686
|
read: () => checked.settings.read({ source }),
|
|
687
687
|
async update(values) {
|
|
688
|
-
await checkSettings(grants, source,
|
|
688
|
+
await checkSettings(grants, source, values)
|
|
689
689
|
await claude.cmod.call({ to: cmodPluginName, method: 'updateSettings', input: { mod: definition.name, source, project: modState.root, values } })
|
|
690
690
|
},
|
|
691
691
|
})
|
package/src/runtime/toaster.ts
CHANGED
|
@@ -32,6 +32,7 @@ type Entry = {
|
|
|
32
32
|
readonly hides?: string
|
|
33
33
|
isOpen: boolean
|
|
34
34
|
reporting?: Reporting | undefined
|
|
35
|
+
trying?: string | RenderElement | undefined
|
|
35
36
|
timer: Timer | undefined
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -71,7 +72,7 @@ export function createToaster(claude: Claude, name: string, version: () => strin
|
|
|
71
72
|
let bandId: string | undefined
|
|
72
73
|
|
|
73
74
|
const changed = () => {
|
|
74
|
-
const isLoading = [...entries.values()].some((entry) => entry.type === 'loading')
|
|
75
|
+
const isLoading = [...entries.values()].some((entry) => entry.type === 'loading' || entry.trying !== undefined)
|
|
75
76
|
if (isLoading && spinning === undefined) {
|
|
76
77
|
spinning = claude.clock.every(spinnerMs, () => {
|
|
77
78
|
frame += 1
|
|
@@ -113,6 +114,11 @@ export function createToaster(claude: Claude, name: string, version: () => strin
|
|
|
113
114
|
const put = (type: ToastType, message: string, options: ToastOptions, actions: readonly Action[], hides?: string): ToastId => {
|
|
114
115
|
const id = options.id ?? (lastId += 1)
|
|
115
116
|
const previous = entries.get(id)
|
|
117
|
+
if (previous?.trying !== undefined && type === 'loading') {
|
|
118
|
+
previous.trying = options.description ?? message
|
|
119
|
+
changed()
|
|
120
|
+
return id
|
|
121
|
+
}
|
|
116
122
|
if (previous !== undefined) disarm(previous)
|
|
117
123
|
const reporting = hides !== undefined && previous?.hides === hides ? previous.reporting : undefined
|
|
118
124
|
const entry: Entry = { id, type, message, options, actions, ...(hides === undefined ? {} : { hides }), isOpen: previous?.isOpen ?? false, reporting, timer: undefined }
|
|
@@ -192,7 +198,21 @@ export function createToaster(claude: Claude, name: string, version: () => strin
|
|
|
192
198
|
const focused = await claude.ui.focus({ requestId: bandId, key: `toast:${id}:ask-claude` })
|
|
193
199
|
if ('deny' in focused) claude.ui.log(`${name}: the Report choices did not take the keys: ${String(focused.deny)}`, { to: 'debug' })
|
|
194
200
|
}
|
|
195
|
-
const
|
|
201
|
+
const tryAgain = async () => {
|
|
202
|
+
const shown = entries.get(id)
|
|
203
|
+
if (shown === undefined || retry === undefined || shown.trying !== undefined) return
|
|
204
|
+
shown.trying = 'Starting'
|
|
205
|
+
changed()
|
|
206
|
+
try {
|
|
207
|
+
await retry()
|
|
208
|
+
} finally {
|
|
209
|
+
if (entries.get(id) === shown) {
|
|
210
|
+
shown.trying = undefined
|
|
211
|
+
changed()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const actions = [...(retry === undefined ? [] : [{ label: 'Retry', onClick: tryAgain }]), { label: 'Report', onClick: startReporting }]
|
|
196
216
|
return put('error', message, { id, ...(description === undefined ? {} : { description }), duration: Infinity }, actions, hides)
|
|
197
217
|
}
|
|
198
218
|
|
|
@@ -205,14 +225,16 @@ export function createToaster(claude: Claude, name: string, version: () => strin
|
|
|
205
225
|
const row = (entry: Entry): RenderElement => {
|
|
206
226
|
const { description } = entry.options
|
|
207
227
|
const isText = typeof description === 'string'
|
|
208
|
-
const icon = entry.type === 'loading' ? { glyph: spinner[frame % spinner.length] as string, color: 'claude' } : icons[entry.type]
|
|
228
|
+
const icon = entry.type === 'loading' || entry.trying !== undefined ? { glyph: spinner[frame % spinner.length] as string, color: 'claude' } : icons[entry.type]
|
|
209
229
|
const message = isText ? Button({ plain: true, key: `toast:${entry.id}:open`, label: entry.message, onPress: toggle(entry) }) : Text({ children: entry.message })
|
|
210
230
|
const details = !isText ? description : Text({ dimColor: true, wrap: entry.isOpen ? 'wrap' : 'truncate-end', children: entry.isOpen ? description : firstLine(description) })
|
|
211
231
|
const fixed = (child: RenderElement) => Box({ flexShrink: 0, children: child })
|
|
212
232
|
const close = Button({ plain: true, dimColor: true, key: `toast:${entry.id}:close`, label: entry.hides === undefined ? '✕' : 'Hide', onPress: () => hide(entry) })
|
|
213
233
|
const actions = entry.actions.map((action, index) => Button({ plain: true, key: `toast:${entry.id}:${index === 0 ? 'action' : `action-${index}`}`, label: action.label, onPress: press(entry, action.onClick) }))
|
|
214
|
-
const
|
|
215
|
-
const
|
|
234
|
+
const buttons = Box({ gap: 2, flexWrap: 'wrap', children: [...actions, close] })
|
|
235
|
+
const trying = entry.trying === undefined ? undefined : Box({ gap: 1, children: [Text({ dimColor: true, children: 'Trying again:' }), typeof entry.trying === 'string' ? Text({ dimColor: true, wrap: 'truncate-end', children: entry.trying }) : entry.trying] })
|
|
236
|
+
const text = details === undefined ? [message] : [message, trying ?? details]
|
|
237
|
+
const under = trying !== undefined && details === undefined ? [trying] : entry.reporting === undefined ? [buttons] : reportChoices(entry, entry.reporting)
|
|
216
238
|
return Box({
|
|
217
239
|
gap: 2,
|
|
218
240
|
children: [
|