@fungi.computer/shiitake-extensions 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +262 -2
- package/dist/almanac/contract.d.ts +213 -0
- package/dist/almanac/contract.js +77 -0
- package/dist/almanac/host.d.ts +3 -0
- package/dist/almanac/host.js +171 -0
- package/dist/goal/client.d.ts +10 -0
- package/dist/goal/client.js +88 -0
- package/dist/goal/contract.d.ts +172 -0
- package/dist/goal/contract.js +76 -0
- package/dist/goal/host.d.ts +5 -0
- package/dist/goal/host.js +542 -0
- package/dist/home/contract.d.ts +33 -0
- package/dist/home/contract.js +25 -0
- package/dist/home/host.d.ts +18 -0
- package/dist/home/host.js +136 -0
- package/dist/home/routing.d.ts +3 -0
- package/dist/home/routing.js +80 -0
- package/dist/internal/catalog.d.ts +20 -0
- package/dist/internal/catalog.js +33 -0
- package/dist/internal/cron.d.ts +5 -0
- package/dist/internal/cron.js +14 -0
- package/dist/internal/history.d.ts +29 -0
- package/dist/internal/history.js +41 -0
- package/dist/loop/client.d.ts +10 -0
- package/dist/loop/client.js +89 -0
- package/dist/loop/contract.d.ts +342 -0
- package/dist/loop/contract.js +94 -0
- package/dist/loop/host.d.ts +4 -0
- package/dist/loop/host.js +637 -0
- package/dist/plan/client.d.ts +13 -0
- package/dist/plan/client.js +51 -0
- package/dist/plan/contract.d.ts +105 -0
- package/dist/plan/contract.js +38 -0
- package/dist/plan/host.d.ts +3 -0
- package/dist/plan/host.js +154 -0
- package/dist/question/client.d.ts +10 -0
- package/dist/question/client.js +28 -0
- package/dist/question/contract.d.ts +251 -0
- package/dist/question/contract.js +114 -0
- package/dist/question/host.d.ts +3 -0
- package/dist/question/host.js +997 -0
- package/dist/roster.d.ts +9 -0
- package/dist/roster.js +80 -0
- package/dist/skills/host.d.ts +7 -0
- package/dist/skills/host.js +223 -0
- package/dist/skills/source.d.ts +28 -0
- package/dist/skills/source.js +1 -0
- package/dist/subagents/client.d.ts +10 -0
- package/dist/subagents/client.js +53 -0
- package/dist/subagents/contract.d.ts +54 -0
- package/dist/subagents/contract.js +38 -0
- package/dist/subagents/host.d.ts +50 -0
- package/dist/subagents/host.js +176 -0
- package/package.json +117 -3
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Knapsack } from "@fungi.computer/knapsack";
|
|
2
|
+
import { defineCollection, defineExtension, ExtensionCommandError, } from "@fungi.computer/shiitake/extensions";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Option from "effect/Option";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import { ask, catalog, done } from "../internal/catalog.js";
|
|
7
|
+
import { Home, homeContract } from "./contract.js";
|
|
8
|
+
import { homeFilesRouting } from "./routing.js";
|
|
9
|
+
/** Hosts admit Home explicitly; no other extension receives the Files slot. */
|
|
10
|
+
export const homeGrant = {
|
|
11
|
+
id: homeContract.id,
|
|
12
|
+
capabilities: ["state", "files", "prompt", "projection", "commands"],
|
|
13
|
+
};
|
|
14
|
+
const config = {
|
|
15
|
+
promptLimit: 4_000,
|
|
16
|
+
guide: "Home supplies read, list, glob and grep over your default Computer. Bare paths and computer://home/ use Home; computer://<name>/ uses that captured attachment. apply_patch, when offered, uses Home's native patch engine and path jail. Only a person changes Home with /home <name>; ask them when no Home is available.",
|
|
17
|
+
};
|
|
18
|
+
const collections = {
|
|
19
|
+
choice: defineCollection(Home, { key: () => "default" }),
|
|
20
|
+
};
|
|
21
|
+
/** Agent-scope choice with per-run tools and prompt over one captured table. */
|
|
22
|
+
export function homeExtension(options) {
|
|
23
|
+
// Core retains one RunIdentity for Files and every prompt request. Weak keys
|
|
24
|
+
// release the captured prose with the run, including failure and cancellation.
|
|
25
|
+
const prompts = new WeakMap();
|
|
26
|
+
return defineExtension({ id: homeContract.id, version: "1.0.0" })
|
|
27
|
+
.config(config)
|
|
28
|
+
.state({ version: 1, migrations: {}, collections })
|
|
29
|
+
.define({
|
|
30
|
+
catalog: catalog([
|
|
31
|
+
{
|
|
32
|
+
id: "home",
|
|
33
|
+
title: "Home Computer",
|
|
34
|
+
category: "Settings",
|
|
35
|
+
description: "Choose the default Computer",
|
|
36
|
+
projections: {
|
|
37
|
+
help: { order: 5 },
|
|
38
|
+
palette: { order: 5 },
|
|
39
|
+
slash: {
|
|
40
|
+
aliases: ["home"],
|
|
41
|
+
order: 5,
|
|
42
|
+
arguments: {
|
|
43
|
+
hint: "<name>",
|
|
44
|
+
text: { field: "name", arguments: {} },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
]),
|
|
50
|
+
projection: {
|
|
51
|
+
schema: Schema.NullOr(Home),
|
|
52
|
+
read: (ctx) => Option.some(Option.getOrNull(ctx.state.choice.peek("default"))),
|
|
53
|
+
},
|
|
54
|
+
files: (ctx, run) => {
|
|
55
|
+
const computers = [...options.computers.read(run)];
|
|
56
|
+
const choice = ctx.state.update((state) => {
|
|
57
|
+
const recorded = Option.getOrNull(state.choice.get("default"));
|
|
58
|
+
if (recorded !== null)
|
|
59
|
+
return recorded;
|
|
60
|
+
const sole = computers.length === 1 ? computers[0] : undefined;
|
|
61
|
+
if (sole === undefined)
|
|
62
|
+
return null;
|
|
63
|
+
const adopted = { attachmentId: sole.attachmentId };
|
|
64
|
+
state.choice.put(adopted);
|
|
65
|
+
return adopted;
|
|
66
|
+
});
|
|
67
|
+
const selected = computers.find((computer) => computer.attachmentId === choice?.attachmentId);
|
|
68
|
+
const status = selected !== undefined
|
|
69
|
+
? `Home: ${selected.name}.`
|
|
70
|
+
: choice?.attachmentId == null
|
|
71
|
+
? "No Home selected. Ask a person to choose /home <name>."
|
|
72
|
+
: "Home's default Computer is detached. Ask a person to choose /home <name>; there is no fallback.";
|
|
73
|
+
const mounts = computers
|
|
74
|
+
.map((computer) => `computer://${encodeURIComponent(computer.name)}/`)
|
|
75
|
+
.join(", ");
|
|
76
|
+
const prompt = `${ctx.config.guide}\n${status}\nCaptured mounts: ${mounts || "none"}.`;
|
|
77
|
+
prompts.set(run, prompt.slice(0, ctx.config.promptLimit));
|
|
78
|
+
if (selected === undefined)
|
|
79
|
+
return null;
|
|
80
|
+
return Knapsack.files({
|
|
81
|
+
port: homeFilesRouting(new Map(computers.map(({ name, port }) => [name, port])), selected.port),
|
|
82
|
+
pathInstructions: "Bare paths and computer://home/ select Home. computer://<name>/ selects another captured attachment. apply_patch uses Home only.",
|
|
83
|
+
}).tools;
|
|
84
|
+
},
|
|
85
|
+
prompt: (_ctx, run) => [
|
|
86
|
+
{
|
|
87
|
+
name: "home",
|
|
88
|
+
text: prompts.get(run) ??
|
|
89
|
+
"Home file tools were not captured. Ask the person to check Home's Files grant.",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
commands: {
|
|
93
|
+
specs: homeContract.commands,
|
|
94
|
+
handlers: (ctx) => ({
|
|
95
|
+
home: ({ input, sessionId }) => Effect.gen(function* () {
|
|
96
|
+
const computers = options.computers.read({ sessionId });
|
|
97
|
+
if (input.name === undefined) {
|
|
98
|
+
if (computers.length === 0)
|
|
99
|
+
return done({
|
|
100
|
+
type: "info",
|
|
101
|
+
message: "No Computers attached. Attach one before choosing Home.",
|
|
102
|
+
});
|
|
103
|
+
return ask({
|
|
104
|
+
type: "choice",
|
|
105
|
+
message: "Choose this Agent's Home Computer",
|
|
106
|
+
options: computers.map(({ name }) => ({
|
|
107
|
+
value: name,
|
|
108
|
+
label: name,
|
|
109
|
+
})),
|
|
110
|
+
}, {}, "name");
|
|
111
|
+
}
|
|
112
|
+
const selected = computers.find(({ name }) => name === input.name);
|
|
113
|
+
if (selected === undefined)
|
|
114
|
+
return yield* Effect.fail(new ExtensionCommandError({
|
|
115
|
+
reason: "invalid",
|
|
116
|
+
message: `Computer is not attached: ${input.name}`,
|
|
117
|
+
}));
|
|
118
|
+
yield* ctx.state.choice.put({
|
|
119
|
+
attachmentId: selected.attachmentId,
|
|
120
|
+
});
|
|
121
|
+
yield* ctx.observe(sessionId);
|
|
122
|
+
return done({
|
|
123
|
+
type: "success",
|
|
124
|
+
message: `Home: ${selected.name}`,
|
|
125
|
+
});
|
|
126
|
+
}),
|
|
127
|
+
seed: ({ input }) => Effect.gen(function* () {
|
|
128
|
+
if (Option.isSome(yield* ctx.state.choice.get("default")))
|
|
129
|
+
return { seeded: false };
|
|
130
|
+
yield* ctx.state.choice.put(input);
|
|
131
|
+
return { seeded: true };
|
|
132
|
+
}),
|
|
133
|
+
}),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const computerLocator = z
|
|
3
|
+
.url({ protocol: /^computer$/ })
|
|
4
|
+
.transform((value) => new URL(value))
|
|
5
|
+
.refine((value) => !value.username &&
|
|
6
|
+
!value.password &&
|
|
7
|
+
!value.port &&
|
|
8
|
+
!value.search &&
|
|
9
|
+
!value.hash, "Computer file locators cannot contain credentials, ports, queries or fragments");
|
|
10
|
+
/** URI names select captured ports; they cannot create or widen authority. */
|
|
11
|
+
export function homeFilesRouting(connections, home) {
|
|
12
|
+
function resolve(locator) {
|
|
13
|
+
const explicit = /^[A-Za-z][A-Za-z0-9+.-]*:/.test(locator);
|
|
14
|
+
const uri = explicit ? computerLocator.parse(locator) : null;
|
|
15
|
+
const mount = uri === null ? "home" : decodeURIComponent(uri.hostname);
|
|
16
|
+
const port = mount === "home" ? home : connections.get(mount);
|
|
17
|
+
if (port === undefined)
|
|
18
|
+
throw new Error(`Files connection unavailable: ${mount}`);
|
|
19
|
+
// A URI's leading slash separates the mount from its root-relative path.
|
|
20
|
+
// The selected port still owns containment and its filesystem root.
|
|
21
|
+
const path = uri === null ? locator : decodeURIComponent(uri.pathname.slice(1)) || ".";
|
|
22
|
+
const locate = (resultPath) => {
|
|
23
|
+
if (uri === null)
|
|
24
|
+
return resultPath;
|
|
25
|
+
const result = new URL(uri);
|
|
26
|
+
result.pathname = resultPath.split("/").map(encodeURIComponent).join("/");
|
|
27
|
+
return result.href;
|
|
28
|
+
};
|
|
29
|
+
return { port, path, locate };
|
|
30
|
+
}
|
|
31
|
+
const routing = {
|
|
32
|
+
read: async (input, context) => {
|
|
33
|
+
const { port, path } = resolve(input.path);
|
|
34
|
+
const result = await port.read({ ...input, path }, context);
|
|
35
|
+
return { ...result, path: input.path };
|
|
36
|
+
},
|
|
37
|
+
list: async (input, context) => {
|
|
38
|
+
const { port, path, locate } = resolve(input.path);
|
|
39
|
+
const result = await port.list({ ...input, path }, context);
|
|
40
|
+
return {
|
|
41
|
+
...result,
|
|
42
|
+
path: input.path,
|
|
43
|
+
entries: result.entries.map((entry) => ({
|
|
44
|
+
...entry,
|
|
45
|
+
path: locate(entry.path),
|
|
46
|
+
})),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
glob: async (input, context) => {
|
|
50
|
+
const { port, path, locate } = resolve(input.path);
|
|
51
|
+
const result = await port.glob({ ...input, path }, context);
|
|
52
|
+
return {
|
|
53
|
+
...result,
|
|
54
|
+
path: input.path,
|
|
55
|
+
matches: result.matches.map(locate),
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
grep: async (input, context) => {
|
|
59
|
+
const { port, path, locate } = resolve(input.path);
|
|
60
|
+
const result = await port.grep({ ...input, path }, context);
|
|
61
|
+
return {
|
|
62
|
+
...result,
|
|
63
|
+
path: input.path,
|
|
64
|
+
matches: result.matches.map((match) => ({
|
|
65
|
+
...match,
|
|
66
|
+
path: locate(match.path),
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
replace: async (input, context) => {
|
|
71
|
+
const { port, path } = resolve(input.path);
|
|
72
|
+
const result = await port.replace({ ...input, path }, context);
|
|
73
|
+
return { ...result, path: input.path };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
// Patches use the selected Home's native engine and jail. URI routing does
|
|
77
|
+
// not introduce a second patch parser.
|
|
78
|
+
const patch = home.patch?.bind(home);
|
|
79
|
+
return patch === undefined ? routing : { ...routing, patch };
|
|
80
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WhistleCatalogStepV1, WhistleCatalogV1 } from "@fungi.computer/whistle/catalog";
|
|
2
|
+
import type { WhistleJsonObject, WhistleNotice, WhistlePrompt } from "@fungi.computer/whistle";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import { type Json } from "@fungi.computer/shiitake/extensions";
|
|
6
|
+
/**
|
|
7
|
+
* The encoded reply of an extension's human command: one `whistle.catalog/v1`
|
|
8
|
+
* step. Clients parse it again with Whistle's own step parser.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CatalogStep: Schema.declare<WhistleCatalogStepV1, WhistleCatalogStepV1>;
|
|
11
|
+
/** Finish a human command with one closing notice. */
|
|
12
|
+
export declare function done(notice: WhistleNotice): WhistleCatalogStepV1;
|
|
13
|
+
/** Finish without acting on the human's input, so a composer keeps their draft. */
|
|
14
|
+
export declare function declined(notice: WhistleNotice): WhistleCatalogStepV1;
|
|
15
|
+
/** Ask one question; the answer fills `answer` of the next call's arguments. */
|
|
16
|
+
export declare function ask(prompt: WhistlePrompt, next: WhistleJsonObject, answer: string): WhistleCatalogStepV1;
|
|
17
|
+
/** A fresh request id for a flow that starts without one. */
|
|
18
|
+
export declare const freshRequestId: Effect.Effect<string, never, never>;
|
|
19
|
+
/** Declare an extension's advertised catalog with Whistle's own types. */
|
|
20
|
+
export declare function catalog(commands: WhistleCatalogV1["commands"]): Json;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { WhistleCatalogStepSchema } from "@fungi.computer/whistle/catalog";
|
|
2
|
+
import { nanoid } from "@fungi.computer/cairn";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import { parseExtensionJson, } from "@fungi.computer/shiitake/extensions";
|
|
6
|
+
/**
|
|
7
|
+
* The encoded reply of an extension's human command: one `whistle.catalog/v1`
|
|
8
|
+
* step. Clients parse it again with Whistle's own step parser.
|
|
9
|
+
*/
|
|
10
|
+
export const CatalogStep = Schema.declare((input) => WhistleCatalogStepSchema.safeParse(input).success, {
|
|
11
|
+
representation: {
|
|
12
|
+
id: "@fungi.computer/whistle/catalog-step/v1",
|
|
13
|
+
payload: null,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
/** Finish a human command with one closing notice. */
|
|
17
|
+
export function done(notice) {
|
|
18
|
+
return { type: "done", notice };
|
|
19
|
+
}
|
|
20
|
+
/** Finish without acting on the human's input, so a composer keeps their draft. */
|
|
21
|
+
export function declined(notice) {
|
|
22
|
+
return { type: "done", notice, result: { cancelled: true } };
|
|
23
|
+
}
|
|
24
|
+
/** Ask one question; the answer fills `answer` of the next call's arguments. */
|
|
25
|
+
export function ask(prompt, next, answer) {
|
|
26
|
+
return { type: "prompt", prompt, next: { arguments: next, answer } };
|
|
27
|
+
}
|
|
28
|
+
/** A fresh request id for a flow that starts without one. */
|
|
29
|
+
export const freshRequestId = Effect.sync(() => nanoid());
|
|
30
|
+
/** Declare an extension's advertised catalog with Whistle's own types. */
|
|
31
|
+
export function catalog(commands) {
|
|
32
|
+
return parseExtensionJson(JSON.stringify({ protocol: "whistle.catalog/v1", commands }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { nextOccurrence as coreNextOccurrence } from "@fungi.computer/almanac";
|
|
2
|
+
import { ExtensionCommandError } from "@fungi.computer/shiitake/extensions";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
/** The first occurrence strictly after `after`; missed occurrences coalesce. */
|
|
5
|
+
export declare function nextOccurrence(cadence: Parameters<typeof coreNextOccurrence>[0], after: number): Effect.Effect<number, ExtensionCommandError>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { nextOccurrence as coreNextOccurrence } from "@fungi.computer/almanac";
|
|
2
|
+
import { ExtensionCommandError } from "@fungi.computer/shiitake/extensions";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Result from "effect/Result";
|
|
5
|
+
/** The first occurrence strictly after `after`; missed occurrences coalesce. */
|
|
6
|
+
export function nextOccurrence(cadence, after) {
|
|
7
|
+
const next = coreNextOccurrence(cadence, after);
|
|
8
|
+
return Result.isFailure(next)
|
|
9
|
+
? Effect.fail(new ExtensionCommandError({
|
|
10
|
+
reason: "invalid",
|
|
11
|
+
message: next.failure.message,
|
|
12
|
+
}))
|
|
13
|
+
: Effect.succeed(next.success);
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
/** Producer state: committed event identity survives reconnection and trimming. */
|
|
3
|
+
export declare function historySchema<E, EI>(event: Schema.Codec<E, EI>): Schema.Struct<{
|
|
4
|
+
readonly nextSequence: Schema.Int;
|
|
5
|
+
readonly omitted: Schema.Int;
|
|
6
|
+
readonly events: Schema.$Array<Schema.Struct<{
|
|
7
|
+
readonly id: Schema.String;
|
|
8
|
+
readonly sequence: Schema.Int;
|
|
9
|
+
readonly at: Schema.Int;
|
|
10
|
+
readonly commandId: Schema.NullOr<Schema.String>;
|
|
11
|
+
readonly event: Schema.Codec<E, EI, never, never>;
|
|
12
|
+
}>>;
|
|
13
|
+
}>;
|
|
14
|
+
/** The extension's own persisted history, not a core cursor. */
|
|
15
|
+
export type History<E> = Readonly<{
|
|
16
|
+
nextSequence: number;
|
|
17
|
+
omitted: number;
|
|
18
|
+
events: readonly Readonly<{
|
|
19
|
+
id: string;
|
|
20
|
+
sequence: number;
|
|
21
|
+
at: number;
|
|
22
|
+
commandId: string | null;
|
|
23
|
+
event: E;
|
|
24
|
+
}>[];
|
|
25
|
+
}>;
|
|
26
|
+
/** A new producer counter; callers persist it alongside their Session state. */
|
|
27
|
+
export declare function emptyHistory<E>(): History<E>;
|
|
28
|
+
/** Append once inside the owner's state transaction; retention never rejects work. */
|
|
29
|
+
export declare function appendHistory<E>(previous: History<E>, event: E, commandId: string | null, at: number): History<E>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
/** Producer state: committed event identity survives reconnection and trimming. */
|
|
3
|
+
export function historySchema(event) {
|
|
4
|
+
return Schema.Struct({
|
|
5
|
+
nextSequence: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
6
|
+
omitted: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
7
|
+
events: Schema.Array(Schema.Struct({
|
|
8
|
+
id: Schema.String,
|
|
9
|
+
sequence: Schema.Int,
|
|
10
|
+
at: Schema.Int,
|
|
11
|
+
commandId: Schema.NullOr(Schema.String),
|
|
12
|
+
event,
|
|
13
|
+
})),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/** A new producer counter; callers persist it alongside their Session state. */
|
|
17
|
+
export function emptyHistory() {
|
|
18
|
+
return { nextSequence: 1, omitted: 0, events: [] };
|
|
19
|
+
}
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
/** Append once inside the owner's state transaction; retention never rejects work. */
|
|
22
|
+
export function appendHistory(previous, event, commandId, at) {
|
|
23
|
+
const sequence = previous.nextSequence;
|
|
24
|
+
const events = [
|
|
25
|
+
...previous.events,
|
|
26
|
+
{ id: String(sequence), sequence, at, commandId, event },
|
|
27
|
+
];
|
|
28
|
+
const history = {
|
|
29
|
+
nextSequence: sequence + 1,
|
|
30
|
+
omitted: previous.omitted,
|
|
31
|
+
events,
|
|
32
|
+
};
|
|
33
|
+
while (events.length > 32 ||
|
|
34
|
+
encoder.encode(JSON.stringify(history)).length > 32 * 1024) {
|
|
35
|
+
if (events.length === 0)
|
|
36
|
+
break;
|
|
37
|
+
events.shift();
|
|
38
|
+
history.omitted += 1;
|
|
39
|
+
}
|
|
40
|
+
return history;
|
|
41
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ClientContributionV1 } from "@fungi.computer/shiitake/client-entry";
|
|
2
|
+
/** Loop supplies its own absolute due copy and advertised stop intent. */
|
|
3
|
+
declare const _default: Readonly<{
|
|
4
|
+
protocol: "shiitake.client/v1";
|
|
5
|
+
id: import("@fungi.computer/shiitake/extensions").ExtensionId;
|
|
6
|
+
correlatedPrompts?: "note";
|
|
7
|
+
attach?: (context: import("@fungi.computer/shiitake/client-entry").ClientExtensionContextV1) => import("effect/Effect").Effect<void, Error, import("effect/Scope").Scope>;
|
|
8
|
+
contribute?: (slot: import("@fungi.computer/shiitake").ExtensionSlotV0 | undefined) => ClientContributionV1;
|
|
9
|
+
}>;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { defineClientEntry, } from "@fungi.computer/shiitake/client-entry";
|
|
2
|
+
import { loopContract } from "./contract.js";
|
|
3
|
+
const tone = (loop) => loop.state === "failed"
|
|
4
|
+
? "error"
|
|
5
|
+
: loop.state === "active"
|
|
6
|
+
? "running"
|
|
7
|
+
: loop.state === "paused"
|
|
8
|
+
? "muted"
|
|
9
|
+
: "settled";
|
|
10
|
+
const due = (at) => new Intl.DateTimeFormat(undefined, {
|
|
11
|
+
year: "numeric",
|
|
12
|
+
month: "short",
|
|
13
|
+
day: "numeric",
|
|
14
|
+
hour: "numeric",
|
|
15
|
+
minute: "2-digit",
|
|
16
|
+
timeZoneName: "short",
|
|
17
|
+
}).format(at);
|
|
18
|
+
/** Loop supplies its own absolute due copy and advertised stop intent. */
|
|
19
|
+
export default defineClientEntry(loopContract, {
|
|
20
|
+
contribute: (view) => {
|
|
21
|
+
if (view.status !== "running" || view.value === null)
|
|
22
|
+
return {
|
|
23
|
+
chips: [],
|
|
24
|
+
panel: [],
|
|
25
|
+
timeline: { cells: [], omitted: 0 },
|
|
26
|
+
waiting: null,
|
|
27
|
+
};
|
|
28
|
+
const { loop, history } = view.value;
|
|
29
|
+
const waiting = loop !== null && loop.state === "active" && loop.nextDueAt !== null;
|
|
30
|
+
return {
|
|
31
|
+
chips: loop === null || (loop.state !== "active" && loop.state !== "paused")
|
|
32
|
+
? []
|
|
33
|
+
: [
|
|
34
|
+
{
|
|
35
|
+
id: "loop",
|
|
36
|
+
order: 2,
|
|
37
|
+
row: {
|
|
38
|
+
id: "loop",
|
|
39
|
+
title: `Loop ${loop.mode === "self_paced" ? "self-paced" : "fixed"}${waiting ? " (waiting)" : ""}`,
|
|
40
|
+
tone: tone(loop),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
panel: loop === null
|
|
45
|
+
? []
|
|
46
|
+
: [
|
|
47
|
+
{
|
|
48
|
+
id: "loop",
|
|
49
|
+
title: `Loop · ${loop.state} · ${loop.mode === "self_paced" ? "self-paced" : `${loop.cron?.expression ?? "fixed"} ${loop.cron?.timeZone ?? ""}`}${loop.nextDueAt === null ? "" : ` · Next ${due(loop.nextDueAt)}`}`,
|
|
50
|
+
detail: loop.prompt.slice(0, 1024),
|
|
51
|
+
tone: tone(loop),
|
|
52
|
+
},
|
|
53
|
+
...(loop.lastError === null
|
|
54
|
+
? []
|
|
55
|
+
: [
|
|
56
|
+
{
|
|
57
|
+
id: "error",
|
|
58
|
+
title: loop.lastError.slice(0, 4096),
|
|
59
|
+
tone: "error",
|
|
60
|
+
},
|
|
61
|
+
]),
|
|
62
|
+
],
|
|
63
|
+
timeline: {
|
|
64
|
+
omitted: history.omitted,
|
|
65
|
+
cells: history.events.map(({ event, ...cell }) => ({
|
|
66
|
+
...cell,
|
|
67
|
+
rows: [
|
|
68
|
+
{
|
|
69
|
+
id: "loop",
|
|
70
|
+
title: `Loop ${event.state}`,
|
|
71
|
+
detail: (event.lastError ?? event.reason ?? event.prompt).slice(0, 1024),
|
|
72
|
+
tone: tone(event),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
})),
|
|
76
|
+
},
|
|
77
|
+
waiting: waiting
|
|
78
|
+
? {
|
|
79
|
+
summary: `Waiting until ${due(loop.nextDueAt)}`,
|
|
80
|
+
cancel: {
|
|
81
|
+
id: "loop",
|
|
82
|
+
label: "stop loop",
|
|
83
|
+
input: { action: "stop" },
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
: null,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
});
|