@sigrun/shared 0.0.0-stage → 0.1.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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +9 -2
- package/dist/access.d.ts +55 -0
- package/dist/access.js +72 -0
- package/dist/ambient.d.ts +111 -0
- package/dist/ambient.js +23 -0
- package/dist/commands.d.ts +75 -0
- package/dist/commands.js +25 -0
- package/dist/conversations.d.ts +31 -0
- package/dist/conversations.js +94 -0
- package/dist/interactions.d.ts +91 -0
- package/dist/interactions.js +21 -0
- package/dist/jobs.d.ts +100 -0
- package/dist/jobs.js +19 -0
- package/dist/json.d.ts +20 -0
- package/dist/json.js +13 -0
- package/dist/session-meta.d.ts +103 -0
- package/dist/session-meta.js +13 -0
- package/dist/settings.d.ts +401 -0
- package/dist/settings.js +19 -0
- package/dist/themes.d.ts +50 -0
- package/dist/themes.js +156 -0
- package/dist/turns.d.ts +102 -0
- package/dist/turns.js +166 -0
- package/dist/view-delta.d.ts +54 -0
- package/dist/view-delta.js +135 -0
- package/dist/views.d.ts +742 -0
- package/dist/views.js +38 -0
- package/package.json +82 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-10-21
|
|
11
|
+
### Added
|
|
12
|
+
- First published release of the shared leaf (workspace package of
|
|
13
|
+
[@sigrun/sigrun](https://github.com/sigrun-ai/sigrun)): theme tokens,
|
|
14
|
+
conversation/turn shaping, the channel wire types, session-meta,
|
|
15
|
+
commands, interactions, ambient records, access, settings, views,
|
|
16
|
+
view-delta, and jobs — the surfaces the webui, TUI, and server share.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael MacLeod
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sigrun/shared
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The pure client-shared leaf of [sigrun](https://github.com/sigrun-ai/sigrun):
|
|
4
|
+
theme tokens, turn/conversation shaping, and the channel wire types —
|
|
5
|
+
consumed by the webui, the TUI, and the server. Zero runtime dependencies;
|
|
6
|
+
every subpath is a types+code pair (`@sigrun/shared/themes`, `/conversations`,
|
|
7
|
+
`/turns`, …) exported from `dist`.
|
|
8
|
+
|
|
9
|
+
Part of the sigrun workspace; published from the repository root as
|
|
10
|
+
`@sigrun/shared` (see the root `CHANGELOG.md` for release history).
|
package/dist/access.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-resource sharing and content access (core hardening W1 Task 5, spec
|
|
3
|
+
* §5.9 ruling 6/10: per-resource sharing; groups cut).
|
|
4
|
+
*
|
|
5
|
+
* Part of the shared leaf package (`@sigrun/shared`): the server computes
|
|
6
|
+
* access through `accessOf`, the clients render the `sharing` state — one
|
|
7
|
+
* source of truth, no drift. PURE module: no imports at all (the
|
|
8
|
+
* views-purity guard enforces this across `shared/src`).
|
|
9
|
+
*
|
|
10
|
+
* The rules, in order (first match wins):
|
|
11
|
+
*
|
|
12
|
+
* 1. superuser → write;
|
|
13
|
+
* 2. ownerless resource → household write (everyone);
|
|
14
|
+
* 3. user === owner → write;
|
|
15
|
+
* 4. household sharing with a defined user → the shared level;
|
|
16
|
+
* 5. people sharing including the user → the shared level;
|
|
17
|
+
* 6. otherwise none (hidden: 404 on every :id route, absent from lists).
|
|
18
|
+
*
|
|
19
|
+
* Anonymous callers (a principal with no user) get nothing from a shared
|
|
20
|
+
* resource: sharing widens access for identified users only (rule 4/5
|
|
21
|
+
* require a defined user), while ownerless stays open to everyone by
|
|
22
|
+
* rule 2.
|
|
23
|
+
*/
|
|
24
|
+
/** What a conversation's owner grants beyond themselves. */
|
|
25
|
+
export type Sharing = {
|
|
26
|
+
readonly scope: "private";
|
|
27
|
+
} | {
|
|
28
|
+
readonly scope: "household";
|
|
29
|
+
readonly level: "read" | "write";
|
|
30
|
+
} | {
|
|
31
|
+
readonly scope: "people";
|
|
32
|
+
readonly users: string[];
|
|
33
|
+
readonly level: "read" | "write";
|
|
34
|
+
};
|
|
35
|
+
/** The resolved access level of a principal to one resource. */
|
|
36
|
+
export type Access = "none" | "read" | "write";
|
|
37
|
+
/** What accessOf needs of the caller (the server's Principal projects here). */
|
|
38
|
+
export interface AccessPrincipal {
|
|
39
|
+
readonly user?: string;
|
|
40
|
+
readonly superuser?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve one principal's access to one resource. Rule order per the
|
|
44
|
+
* module doc; the first matching rule wins.
|
|
45
|
+
*/
|
|
46
|
+
export declare function accessOf(p: AccessPrincipal, r: {
|
|
47
|
+
readonly owner?: string;
|
|
48
|
+
readonly sharing?: Sharing;
|
|
49
|
+
}): Access;
|
|
50
|
+
/**
|
|
51
|
+
* Type guard for the wire value: every scope shape checked strictly
|
|
52
|
+
* (wrong field types are not a Sharing). An empty `people.users` list is
|
|
53
|
+
* valid (shares with nobody yet) — validation is about shape, not policy.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isSharing(value: unknown): value is Sharing;
|
package/dist/access.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-resource sharing and content access (core hardening W1 Task 5, spec
|
|
3
|
+
* §5.9 ruling 6/10: per-resource sharing; groups cut).
|
|
4
|
+
*
|
|
5
|
+
* Part of the shared leaf package (`@sigrun/shared`): the server computes
|
|
6
|
+
* access through `accessOf`, the clients render the `sharing` state — one
|
|
7
|
+
* source of truth, no drift. PURE module: no imports at all (the
|
|
8
|
+
* views-purity guard enforces this across `shared/src`).
|
|
9
|
+
*
|
|
10
|
+
* The rules, in order (first match wins):
|
|
11
|
+
*
|
|
12
|
+
* 1. superuser → write;
|
|
13
|
+
* 2. ownerless resource → household write (everyone);
|
|
14
|
+
* 3. user === owner → write;
|
|
15
|
+
* 4. household sharing with a defined user → the shared level;
|
|
16
|
+
* 5. people sharing including the user → the shared level;
|
|
17
|
+
* 6. otherwise none (hidden: 404 on every :id route, absent from lists).
|
|
18
|
+
*
|
|
19
|
+
* Anonymous callers (a principal with no user) get nothing from a shared
|
|
20
|
+
* resource: sharing widens access for identified users only (rule 4/5
|
|
21
|
+
* require a defined user), while ownerless stays open to everyone by
|
|
22
|
+
* rule 2.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Resolve one principal's access to one resource. Rule order per the
|
|
26
|
+
* module doc; the first matching rule wins.
|
|
27
|
+
*/
|
|
28
|
+
export function accessOf(p, r) {
|
|
29
|
+
// 1. The superuser always writes.
|
|
30
|
+
if (p.superuser === true)
|
|
31
|
+
return "write";
|
|
32
|
+
// 2. Ownerless is household-write for everyone.
|
|
33
|
+
if (r.owner === undefined)
|
|
34
|
+
return "write";
|
|
35
|
+
// 3. The owner always writes.
|
|
36
|
+
if (p.user !== undefined && p.user === r.owner)
|
|
37
|
+
return "write";
|
|
38
|
+
// 4./5. Sharing widens identified users only — an anonymous caller
|
|
39
|
+
// (no user) falls straight through to none.
|
|
40
|
+
if (p.user !== undefined && r.sharing !== undefined) {
|
|
41
|
+
if (r.sharing.scope === "household")
|
|
42
|
+
return r.sharing.level;
|
|
43
|
+
if (r.sharing.scope === "people" && r.sharing.users.includes(p.user)) {
|
|
44
|
+
return r.sharing.level;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// 6. Hidden.
|
|
48
|
+
return "none";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Type guard for the wire value: every scope shape checked strictly
|
|
52
|
+
* (wrong field types are not a Sharing). An empty `people.users` list is
|
|
53
|
+
* valid (shares with nobody yet) — validation is about shape, not policy.
|
|
54
|
+
*/
|
|
55
|
+
export function isSharing(value) {
|
|
56
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const record = value;
|
|
60
|
+
switch (record.scope) {
|
|
61
|
+
case "private":
|
|
62
|
+
return true;
|
|
63
|
+
case "household":
|
|
64
|
+
return record.level === "read" || record.level === "write";
|
|
65
|
+
case "people":
|
|
66
|
+
return (Array.isArray(record.users) &&
|
|
67
|
+
record.users.every((user) => typeof user === "string") &&
|
|
68
|
+
(record.level === "read" || record.level === "write"));
|
|
69
|
+
default:
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ambient seam TYPES (spec: docs/approval-ux-seams.md, "Ambient
|
|
3
|
+
* surfaces"): the channel-neutral record shapes for persistent
|
|
4
|
+
* read-mostly state — statusline slots and panel widgets — written by
|
|
5
|
+
* extensions through the host context and rendered by channels
|
|
6
|
+
* generically.
|
|
7
|
+
*
|
|
8
|
+
* The CANONICAL type declarations live here (the shared leaf package);
|
|
9
|
+
* `src/harness/ambient.ts` imports them (type-only) for the doc
|
|
10
|
+
* definitions and validation runtime. The records ride the §5 wire
|
|
11
|
+
* AS-IS (envelope + body + details — never projected), so these shapes
|
|
12
|
+
* are the channel contract too (see `@sigrun/shared/views`).
|
|
13
|
+
*
|
|
14
|
+
* Lifecycle difference from interactions: an ambient surface is replaced
|
|
15
|
+
* repeatedly (write = FULL state replacement, never a delta) and never
|
|
16
|
+
* blocks. One slot key is owned by exactly one extension (allocation,
|
|
17
|
+
* not silent composition — the pi-zentui chrome-skin lesson).
|
|
18
|
+
*
|
|
19
|
+
* Layering mirrors the interaction seam: strict envelope (key, place,
|
|
20
|
+
* budget, refresh, expires) around a body composed only from the closed
|
|
21
|
+
* ambient primitives (line, rows, gauge, ledger), with opaque `details`.
|
|
22
|
+
*/
|
|
23
|
+
import type { JsonObject } from "./json.js";
|
|
24
|
+
/** Where a channel places the surface. */
|
|
25
|
+
export type AmbientPlace = "status" | "footer" | "panel";
|
|
26
|
+
/** One widget row (primitive B). */
|
|
27
|
+
export type AmbientRow = {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly label: string;
|
|
30
|
+
/** Channel-schemed glyph state (todo statuses, agent run states). */
|
|
31
|
+
readonly status?: string;
|
|
32
|
+
/** Severity ordering/color: 0 = severe .. 2 = quiet. */
|
|
33
|
+
readonly severity?: 0 | 1 | 2;
|
|
34
|
+
/** Folded away by default (warden's quiet verdicts). */
|
|
35
|
+
readonly quiet?: boolean;
|
|
36
|
+
readonly detail?: string;
|
|
37
|
+
/** Nesting depth for tree rendering (pi-goal-x task tree). */
|
|
38
|
+
readonly depth?: number;
|
|
39
|
+
/** Dependency ids for marker rendering (rpiv-todo blockedBy). */
|
|
40
|
+
readonly deps?: string[];
|
|
41
|
+
/** Present-continuous spinner label while the row is active. */
|
|
42
|
+
readonly spinnerLabel?: string;
|
|
43
|
+
readonly metrics?: AmbientMetrics;
|
|
44
|
+
};
|
|
45
|
+
/** Numeric metrics with an optional derived-format hint. */
|
|
46
|
+
export type AmbientMetrics = {
|
|
47
|
+
readonly tokens?: number;
|
|
48
|
+
readonly cost?: number;
|
|
49
|
+
readonly current?: number;
|
|
50
|
+
readonly total?: number;
|
|
51
|
+
readonly completed?: number;
|
|
52
|
+
/** Derived display hint for numeric fields ("2.5k", currency) — the
|
|
53
|
+
* host-reuse lesson from every footer extension. */
|
|
54
|
+
readonly format?: "compact" | "usd" | "percent";
|
|
55
|
+
};
|
|
56
|
+
/** The `line` primitive: one keyed statusline/footer line, optionally segmented. */
|
|
57
|
+
export type AmbientLine = {
|
|
58
|
+
readonly text: string;
|
|
59
|
+
/** Segmented composition for powerline-style slots (pi-powerline,
|
|
60
|
+
* pi-zentui): sub-segments with placement zones. */
|
|
61
|
+
readonly segments?: {
|
|
62
|
+
readonly id: string;
|
|
63
|
+
readonly text: string;
|
|
64
|
+
readonly place?: "left" | "middle" | "right";
|
|
65
|
+
}[];
|
|
66
|
+
};
|
|
67
|
+
/** The `gauge` primitive: live progress with optional check rows. */
|
|
68
|
+
export type AmbientGauge = {
|
|
69
|
+
readonly label: string;
|
|
70
|
+
readonly percent?: number;
|
|
71
|
+
readonly phase?: string;
|
|
72
|
+
readonly elapsedMs?: number;
|
|
73
|
+
readonly recentOutput?: string[];
|
|
74
|
+
/** Check rows (pi-goal-x auditor): row-shaped, same schema as rows[]. */
|
|
75
|
+
readonly checks?: AmbientRow[];
|
|
76
|
+
};
|
|
77
|
+
/** One ledger entry (primitive D): append-style feed with per-entry amend. */
|
|
78
|
+
export type AmbientLedgerEntry = {
|
|
79
|
+
readonly id: string;
|
|
80
|
+
readonly at: number;
|
|
81
|
+
readonly kind: string;
|
|
82
|
+
readonly summary: string;
|
|
83
|
+
readonly detail?: string[];
|
|
84
|
+
};
|
|
85
|
+
/** The ambient body: ONLY these keys, each from the closed primitives. */
|
|
86
|
+
export type AmbientBody = {
|
|
87
|
+
readonly line?: AmbientLine;
|
|
88
|
+
readonly rows?: AmbientRow[];
|
|
89
|
+
readonly gauge?: AmbientGauge;
|
|
90
|
+
readonly ledger?: AmbientLedgerEntry[];
|
|
91
|
+
};
|
|
92
|
+
/** The strict envelope + closed-primitive body of one ambient surface. */
|
|
93
|
+
export type AmbientRecord = {
|
|
94
|
+
/** The keyed slot id ("goal", "rpiv-todos", "fabric"). */
|
|
95
|
+
readonly key: string;
|
|
96
|
+
/** Which extension owns this key (set by the host context, not the author). */
|
|
97
|
+
readonly owner: string;
|
|
98
|
+
readonly place: AmbientPlace;
|
|
99
|
+
/** Panel row budget / line-slot width guidance (channel advisory). */
|
|
100
|
+
readonly maxRows?: number;
|
|
101
|
+
/** Overflow policy for panels (rpiv-todo's drop-completed-first rule). */
|
|
102
|
+
readonly overflow?: "drop" | "wrap" | "summarize";
|
|
103
|
+
/** Channel re-render hint between data writes (countdowns, clocks), ms. */
|
|
104
|
+
readonly refreshMs?: number;
|
|
105
|
+
/** Auto-clear rule: "never" stays until replaced with an empty body. */
|
|
106
|
+
readonly expires: "never" | "idle";
|
|
107
|
+
readonly updatedAt: number;
|
|
108
|
+
readonly body?: AmbientBody;
|
|
109
|
+
/** Opaque extension-owned payload; rendered collapsed, never interpreted. */
|
|
110
|
+
readonly details?: JsonObject;
|
|
111
|
+
};
|
package/dist/ambient.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ambient seam TYPES (spec: docs/approval-ux-seams.md, "Ambient
|
|
3
|
+
* surfaces"): the channel-neutral record shapes for persistent
|
|
4
|
+
* read-mostly state — statusline slots and panel widgets — written by
|
|
5
|
+
* extensions through the host context and rendered by channels
|
|
6
|
+
* generically.
|
|
7
|
+
*
|
|
8
|
+
* The CANONICAL type declarations live here (the shared leaf package);
|
|
9
|
+
* `src/harness/ambient.ts` imports them (type-only) for the doc
|
|
10
|
+
* definitions and validation runtime. The records ride the §5 wire
|
|
11
|
+
* AS-IS (envelope + body + details — never projected), so these shapes
|
|
12
|
+
* are the channel contract too (see `@sigrun/shared/views`).
|
|
13
|
+
*
|
|
14
|
+
* Lifecycle difference from interactions: an ambient surface is replaced
|
|
15
|
+
* repeatedly (write = FULL state replacement, never a delta) and never
|
|
16
|
+
* blocks. One slot key is owned by exactly one extension (allocation,
|
|
17
|
+
* not silent composition — the pi-zentui chrome-skin lesson).
|
|
18
|
+
*
|
|
19
|
+
* Layering mirrors the interaction seam: strict envelope (key, place,
|
|
20
|
+
* budget, refresh, expires) around a body composed only from the closed
|
|
21
|
+
* ambient primitives (line, rows, gauge, ledger), with opaque `details`.
|
|
22
|
+
*/
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slash-command WIRE types (Task 7, pi parity) — the client-facing
|
|
3
|
+
* half of `src/server/commands.ts` (which owns the catalog runtime).
|
|
4
|
+
*
|
|
5
|
+
* This module is part of the shared leaf package (`@sigrun/shared`): the
|
|
6
|
+
* tui and webui import these shapes from here, never from server source
|
|
7
|
+
* (the load-bearing decoupling — the tui is a pure client of the wire).
|
|
8
|
+
*
|
|
9
|
+
* PURE module: no imports at all (the views-purity guard enforces this
|
|
10
|
+
* across `shared/src`).
|
|
11
|
+
*
|
|
12
|
+
* Registry header (from the server module — the catalog itself lives
|
|
13
|
+
* there): a THIN registry of command METADATA — name, description, args
|
|
14
|
+
* hint, and the §5 endpoint each command drives. Deliberately NO
|
|
15
|
+
* server-side `execute` indirection: the composer owns the client calls,
|
|
16
|
+
* the registry only tells it what exists and where it goes. Scope
|
|
17
|
+
* (controller rulings): ONLY commands mapping to EXISTING endpoints.
|
|
18
|
+
* `/login`/`/logout` are core-spec §6.4 exclusions (credentials never
|
|
19
|
+
* cross the API); `/skill:name` and prompt templates are Task 2c's
|
|
20
|
+
* `kind: "expand"` entries (no endpoint — the server expands the text
|
|
21
|
+
* at admission). `/model` and `/thinking` ARE included: both map
|
|
22
|
+
* cleanly onto `POST .../configure`'s existing
|
|
23
|
+
* `{model?, thinking?}` body (each sends exactly one of the two fields).
|
|
24
|
+
*/
|
|
25
|
+
/** The HTTP method + literal route path a command drives. */
|
|
26
|
+
export interface SlashCommandEndpoint {
|
|
27
|
+
readonly method: "GET" | "POST" | "PATCH";
|
|
28
|
+
/**
|
|
29
|
+
* The REGISTERED route path (Fastify `:param` form, e.g.
|
|
30
|
+
* `/v1/conversations/:id/fork`) — the composer substitutes the active
|
|
31
|
+
* conversation id; `server-commands.test.ts` asserts every entry
|
|
32
|
+
* resolves against the live route table.
|
|
33
|
+
*/
|
|
34
|
+
readonly path: string;
|
|
35
|
+
}
|
|
36
|
+
/** One slash command's catalog entry. */
|
|
37
|
+
export interface SlashCommand {
|
|
38
|
+
/** The bare name (no leading slash) — the `Map` key. */
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly description: string;
|
|
41
|
+
/** Argument shape shown in the menu/usage errors; absent = no args. */
|
|
42
|
+
readonly argsHint?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The contributing extension's slug (W5 Task 3); ABSENT on core
|
|
45
|
+
* commands. An extension entry's `endpoint` is its invoke route
|
|
46
|
+
* (`POST /v1/conversations/:id/commands/<name>`) — the client POSTs
|
|
47
|
+
* there with `{args}` and renders a returned `notice`.
|
|
48
|
+
*/
|
|
49
|
+
readonly extension?: string;
|
|
50
|
+
/**
|
|
51
|
+
* How the composer satisfies the command. ABSENT means `"endpoint"`
|
|
52
|
+
* (every pre-existing entry — the composer calls `endpoint`);
|
|
53
|
+
* `"expand"` (Task 2c) means the client inserts `/name ` into the
|
|
54
|
+
* composer and the SERVER expands the submitted text at admission
|
|
55
|
+
* (`/skill:name` and prompt templates) — no endpoint exists and none
|
|
56
|
+
* is needed.
|
|
57
|
+
*/
|
|
58
|
+
readonly kind?: "endpoint" | "expand";
|
|
59
|
+
/** The endpoint an `endpoint`-kind entry drives; absent when `kind === "expand"`. */
|
|
60
|
+
readonly endpoint?: SlashCommandEndpoint;
|
|
61
|
+
}
|
|
62
|
+
/** The `GET /v1/commands` response (spec §5 response conventions). */
|
|
63
|
+
export interface CommandsResponse {
|
|
64
|
+
readonly commands: readonly SlashCommand[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The `POST /v1/conversations/:id/commands/:name` response (W5 Task 3,
|
|
68
|
+
* extension commands): the command's optional human-facing notice —
|
|
69
|
+
* the client renders it as a transient banner (webui) / status-bar
|
|
70
|
+
* flash (tui). An empty body means the command returned nothing to
|
|
71
|
+
* say.
|
|
72
|
+
*/
|
|
73
|
+
export interface CommandRunResponse {
|
|
74
|
+
readonly notice?: string;
|
|
75
|
+
}
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slash-command WIRE types (Task 7, pi parity) — the client-facing
|
|
3
|
+
* half of `src/server/commands.ts` (which owns the catalog runtime).
|
|
4
|
+
*
|
|
5
|
+
* This module is part of the shared leaf package (`@sigrun/shared`): the
|
|
6
|
+
* tui and webui import these shapes from here, never from server source
|
|
7
|
+
* (the load-bearing decoupling — the tui is a pure client of the wire).
|
|
8
|
+
*
|
|
9
|
+
* PURE module: no imports at all (the views-purity guard enforces this
|
|
10
|
+
* across `shared/src`).
|
|
11
|
+
*
|
|
12
|
+
* Registry header (from the server module — the catalog itself lives
|
|
13
|
+
* there): a THIN registry of command METADATA — name, description, args
|
|
14
|
+
* hint, and the §5 endpoint each command drives. Deliberately NO
|
|
15
|
+
* server-side `execute` indirection: the composer owns the client calls,
|
|
16
|
+
* the registry only tells it what exists and where it goes. Scope
|
|
17
|
+
* (controller rulings): ONLY commands mapping to EXISTING endpoints.
|
|
18
|
+
* `/login`/`/logout` are core-spec §6.4 exclusions (credentials never
|
|
19
|
+
* cross the API); `/skill:name` and prompt templates are Task 2c's
|
|
20
|
+
* `kind: "expand"` entries (no endpoint — the server expands the text
|
|
21
|
+
* at admission). `/model` and `/thinking` ARE included: both map
|
|
22
|
+
* cleanly onto `POST .../configure`'s existing
|
|
23
|
+
* `{model?, thinking?}` body (each sends exactly one of the two fields).
|
|
24
|
+
*/
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation list tree (Task B1, UI refresh): sidebar shaping —
|
|
3
|
+
* fork nesting, recency ordering, pinned grouping and age rendering.
|
|
4
|
+
*
|
|
5
|
+
* PURE module: no imports at all (test/views-purity.test.ts enforces
|
|
6
|
+
* this). `ListRow` is structural — both clients' wire row types fit —
|
|
7
|
+
* and the only fields read are the ones the list endpoint ships.
|
|
8
|
+
*/
|
|
9
|
+
export interface ListRow {
|
|
10
|
+
readonly id: number;
|
|
11
|
+
readonly name?: string;
|
|
12
|
+
readonly lastActivityAt?: string;
|
|
13
|
+
readonly parent?: {
|
|
14
|
+
readonly id: number;
|
|
15
|
+
};
|
|
16
|
+
readonly archived: boolean;
|
|
17
|
+
readonly pinned: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface TreeNode<R extends ListRow> {
|
|
20
|
+
readonly row: R;
|
|
21
|
+
readonly children: readonly TreeNode<R>[];
|
|
22
|
+
/** Max epoch ms over the whole subtree; 0 when nothing was ever used. */
|
|
23
|
+
readonly newestActivity: number;
|
|
24
|
+
}
|
|
25
|
+
export type AgeGroup = "Pinned" | "Today" | "Yesterday" | "Earlier";
|
|
26
|
+
export declare function buildConversationTree<R extends ListRow>(rows: readonly R[], options: {
|
|
27
|
+
includeArchived: boolean;
|
|
28
|
+
}): TreeNode<R>[];
|
|
29
|
+
export declare function ageGroup(node: TreeNode<ListRow>, now: Date): AgeGroup;
|
|
30
|
+
export declare function formatAge(iso: string | undefined, now: Date): string;
|
|
31
|
+
export declare function displayName(row: ListRow): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation list tree (Task B1, UI refresh): sidebar shaping —
|
|
3
|
+
* fork nesting, recency ordering, pinned grouping and age rendering.
|
|
4
|
+
*
|
|
5
|
+
* PURE module: no imports at all (test/views-purity.test.ts enforces
|
|
6
|
+
* this). `ListRow` is structural — both clients' wire row types fit —
|
|
7
|
+
* and the only fields read are the ones the list endpoint ships.
|
|
8
|
+
*/
|
|
9
|
+
/** Sort: newest first, ties by id descending. */
|
|
10
|
+
function byRecency(a, b) {
|
|
11
|
+
return b.newestActivity - a.newestActivity || b.row.id - a.row.id;
|
|
12
|
+
}
|
|
13
|
+
/** Epoch ms of a row's activity stamp; 0 when absent or unparsable. */
|
|
14
|
+
function epoch(iso) {
|
|
15
|
+
if (iso === undefined)
|
|
16
|
+
return 0;
|
|
17
|
+
const parsed = Date.parse(iso);
|
|
18
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
19
|
+
}
|
|
20
|
+
export function buildConversationTree(rows, options) {
|
|
21
|
+
const nodes = new Map();
|
|
22
|
+
for (const row of rows) {
|
|
23
|
+
// `parent` is fork lineage — an ancestor by construction (set once,
|
|
24
|
+
// at fork time), so the resulting forest is acyclic.
|
|
25
|
+
if (options.includeArchived || !row.archived) {
|
|
26
|
+
nodes.set(row.id, { row, children: [] });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const roots = [];
|
|
30
|
+
for (const node of nodes.values()) {
|
|
31
|
+
const parent = node.row.parent;
|
|
32
|
+
const parentNode = parent === undefined ? undefined : nodes.get(parent.id);
|
|
33
|
+
if (parentNode === undefined || parentNode === node) {
|
|
34
|
+
roots.push(node);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
parentNode.children.push(node);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const toNode = (node) => {
|
|
41
|
+
const children = node.children.map(toNode).sort(byRecency);
|
|
42
|
+
const own = epoch(node.row.lastActivityAt);
|
|
43
|
+
const newestActivity = children.reduce((max, child) => Math.max(max, child.newestActivity), own);
|
|
44
|
+
return { row: node.row, children, newestActivity };
|
|
45
|
+
};
|
|
46
|
+
return roots
|
|
47
|
+
.map(toNode)
|
|
48
|
+
.sort((a, b) => Number(b.row.pinned) - Number(a.row.pinned) || byRecency(a, b));
|
|
49
|
+
}
|
|
50
|
+
/** Local-calendar-day distance between two instants. */
|
|
51
|
+
function dayDistance(then, now) {
|
|
52
|
+
const midnight = (value) => {
|
|
53
|
+
const date = new Date(value);
|
|
54
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
55
|
+
};
|
|
56
|
+
// `round` absorbs DST's 23/25-hour days around the midnight anchor.
|
|
57
|
+
return Math.round((midnight(now) - midnight(then)) / 86_400_000);
|
|
58
|
+
}
|
|
59
|
+
export function ageGroup(node, now) {
|
|
60
|
+
if (node.row.pinned)
|
|
61
|
+
return "Pinned";
|
|
62
|
+
const days = dayDistance(node.newestActivity, now);
|
|
63
|
+
if (days <= 0)
|
|
64
|
+
return "Today";
|
|
65
|
+
if (days === 1)
|
|
66
|
+
return "Yesterday";
|
|
67
|
+
return "Earlier";
|
|
68
|
+
}
|
|
69
|
+
const MINUTE = 60_000;
|
|
70
|
+
const HOUR = 3_600_000;
|
|
71
|
+
const DAY = 86_400_000;
|
|
72
|
+
export function formatAge(iso, now) {
|
|
73
|
+
if (iso === undefined)
|
|
74
|
+
return "";
|
|
75
|
+
const at = Date.parse(iso);
|
|
76
|
+
if (Number.isNaN(at))
|
|
77
|
+
return "";
|
|
78
|
+
const age = now.getTime() - at;
|
|
79
|
+
if (age < MINUTE)
|
|
80
|
+
return "now";
|
|
81
|
+
if (age < HOUR)
|
|
82
|
+
return `${Math.floor(age / MINUTE)}m`;
|
|
83
|
+
if (age < DAY)
|
|
84
|
+
return `${Math.floor(age / HOUR)}h`;
|
|
85
|
+
if (age < 7 * DAY)
|
|
86
|
+
return `${Math.floor(age / DAY)}d`;
|
|
87
|
+
return new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric" }).format(at);
|
|
88
|
+
}
|
|
89
|
+
export function displayName(row) {
|
|
90
|
+
const flattened = row.name?.replace(/[\r\n]+/g, " ").trim();
|
|
91
|
+
return flattened === undefined || flattened.length === 0
|
|
92
|
+
? "New conversation"
|
|
93
|
+
: flattened;
|
|
94
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interaction seam TYPES (spec: docs/approval-ux-seams.md): the
|
|
3
|
+
* channel-neutral record shapes for "this conversation is waiting on a
|
|
4
|
+
* human decision", written by extensions through the host context and
|
|
5
|
+
* answered by channels through one generic route.
|
|
6
|
+
*
|
|
7
|
+
* The CANONICAL type declarations live here (the shared leaf package);
|
|
8
|
+
* `src/harness/interactions.ts` imports them (type-only) for the doc
|
|
9
|
+
* definitions and validation runtime. The records ride the §5 wire
|
|
10
|
+
* AS-IS (envelope + body + details — never projected), so these shapes
|
|
11
|
+
* are the channel contract too (see `@sigrun/shared/views`).
|
|
12
|
+
*
|
|
13
|
+
* Layering (strict envelope / closed primitives / opaque details):
|
|
14
|
+
*
|
|
15
|
+
* - envelope: kind, callId, status, timestamps, answer — load-bearing for
|
|
16
|
+
* joining tool slots and lifecycle arbitration. Strictly validated.
|
|
17
|
+
* - body: composed ONLY from the closed primitives (text, options,
|
|
18
|
+
* fields, diff). Structurally validated; unknown body keys quarantine.
|
|
19
|
+
* - details: opaque extension-owned JSON. Never interpreted.
|
|
20
|
+
*/
|
|
21
|
+
import type { JsonObject } from "./json.js";
|
|
22
|
+
/** Lifecycle of one interaction record (spec statuses). */
|
|
23
|
+
export type InteractionStatus = "pending" | "answered" | "skipped" | "expired" | "cancelled";
|
|
24
|
+
/** One choice in the `options` primitive. */
|
|
25
|
+
export type InteractionOption = {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly label: string;
|
|
28
|
+
/** Second line under the label (pi-advisor presets, rpiv options). */
|
|
29
|
+
readonly description?: string;
|
|
30
|
+
/** Channel-schemed color/severity hint: "danger" | "warn" | "ok" | "neutral". */
|
|
31
|
+
readonly tone?: "danger" | "warn" | "ok" | "neutral";
|
|
32
|
+
/** Pre-selected by the extension's recommendation (pi-interview conviction). */
|
|
33
|
+
readonly recommended?: boolean;
|
|
34
|
+
};
|
|
35
|
+
/** One light-form field in the `fields` primitive. */
|
|
36
|
+
export type InteractionField = {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly type: "string" | "number" | "boolean" | "secret";
|
|
39
|
+
readonly label?: string;
|
|
40
|
+
readonly required?: boolean;
|
|
41
|
+
};
|
|
42
|
+
/** The `diff` primitive: before/after content, optionally path-scoped. */
|
|
43
|
+
export type InteractionDiff = {
|
|
44
|
+
readonly before: string;
|
|
45
|
+
readonly after: string;
|
|
46
|
+
readonly path?: string;
|
|
47
|
+
readonly lang?: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The interaction body: ONLY these four keys, each built only from the
|
|
51
|
+
* closed primitives. Everything else belongs in `details`.
|
|
52
|
+
*/
|
|
53
|
+
export type InteractionBody = {
|
|
54
|
+
/** Prose prompt body (markdown). */
|
|
55
|
+
readonly text?: string;
|
|
56
|
+
readonly options?: InteractionOption[];
|
|
57
|
+
readonly fields?: InteractionField[];
|
|
58
|
+
readonly diff?: InteractionDiff;
|
|
59
|
+
};
|
|
60
|
+
/** What an answer may carry: chosen ids, form values, and/or free text. */
|
|
61
|
+
export type InteractionAnswer = {
|
|
62
|
+
readonly ids?: string[];
|
|
63
|
+
readonly values?: Readonly<Record<string, string | number | boolean>>;
|
|
64
|
+
readonly text?: string;
|
|
65
|
+
};
|
|
66
|
+
/** The strict envelope + closed-primitive body of one interaction. */
|
|
67
|
+
export type InteractionRecord = {
|
|
68
|
+
/** Extension-declared kind id (e.g. "gating.confirm", "questions.form"). */
|
|
69
|
+
readonly kind: string;
|
|
70
|
+
/** Stable join key to the pi.live tool slot (`ToolCall.id`). */
|
|
71
|
+
readonly callId: string;
|
|
72
|
+
readonly status: InteractionStatus;
|
|
73
|
+
/** Epoch ms when seeded. */
|
|
74
|
+
readonly createdAt: number;
|
|
75
|
+
/** Epoch ms after which channels may show it as expired. */
|
|
76
|
+
readonly expiresAt?: number;
|
|
77
|
+
/** Epoch ms when a terminal status landed. */
|
|
78
|
+
readonly settledAt?: number;
|
|
79
|
+
readonly answer?: InteractionAnswer;
|
|
80
|
+
/** Who answered: "human" via the route, or the extension itself. */
|
|
81
|
+
readonly answeredBy?: "human" | "extension";
|
|
82
|
+
/**
|
|
83
|
+
* The answering principal's directory username (spec §5.9): stamped by
|
|
84
|
+
* the answer route from `request.principal.user` — host-assigned, never
|
|
85
|
+
* model-asserted. Absent on the naive single-operator path (no user).
|
|
86
|
+
*/
|
|
87
|
+
readonly answeredByUser?: string;
|
|
88
|
+
readonly body?: InteractionBody;
|
|
89
|
+
/** Opaque extension-owned payload; rendered collapsed, never interpreted. */
|
|
90
|
+
readonly details?: JsonObject;
|
|
91
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interaction seam TYPES (spec: docs/approval-ux-seams.md): the
|
|
3
|
+
* channel-neutral record shapes for "this conversation is waiting on a
|
|
4
|
+
* human decision", written by extensions through the host context and
|
|
5
|
+
* answered by channels through one generic route.
|
|
6
|
+
*
|
|
7
|
+
* The CANONICAL type declarations live here (the shared leaf package);
|
|
8
|
+
* `src/harness/interactions.ts` imports them (type-only) for the doc
|
|
9
|
+
* definitions and validation runtime. The records ride the §5 wire
|
|
10
|
+
* AS-IS (envelope + body + details — never projected), so these shapes
|
|
11
|
+
* are the channel contract too (see `@sigrun/shared/views`).
|
|
12
|
+
*
|
|
13
|
+
* Layering (strict envelope / closed primitives / opaque details):
|
|
14
|
+
*
|
|
15
|
+
* - envelope: kind, callId, status, timestamps, answer — load-bearing for
|
|
16
|
+
* joining tool slots and lifecycle arbitration. Strictly validated.
|
|
17
|
+
* - body: composed ONLY from the closed primitives (text, options,
|
|
18
|
+
* fields, diff). Structurally validated; unknown body keys quarantine.
|
|
19
|
+
* - details: opaque extension-owned JSON. Never interpreted.
|
|
20
|
+
*/
|
|
21
|
+
export {};
|