@huseynsnmz/pi-suite 0.0.0-stage → 0.2.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 +28 -0
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/SECURITY.md +5 -0
- package/package.json +112 -4
- package/packages/board/CHANGELOG.md +340 -0
- package/packages/board/CONTRIBUTING.md +7 -0
- package/packages/board/LICENSE +21 -0
- package/packages/board/README.md +88 -0
- package/packages/board/SECURITY.md +19 -0
- package/packages/board/docs/how-it-works.md +31 -0
- package/packages/board/docs/reference.md +278 -0
- package/packages/board/package.json +81 -0
- package/packages/board/runner/job-runner-legacy.mjs +68 -0
- package/packages/board/runner/job-runner.mjs +834 -0
- package/packages/board/runner/pty-runner-legacy.mjs +50 -0
- package/packages/board/runner/pty-runner.mjs +2175 -0
- package/packages/board/runner/state-coordinator.mjs +673 -0
- package/packages/board/runner/state-runner.mjs +203 -0
- package/packages/board/runner/title-runner.mjs +80 -0
- package/packages/board/src/commands/agent-board.ts +394 -0
- package/packages/board/src/commands/attach-decision.mjs +66 -0
- package/packages/board/src/commands/attach-flow.ts +251 -0
- package/packages/board/src/commands/bg.ts +103 -0
- package/packages/board/src/commands/board-model.ts +21 -0
- package/packages/board/src/core/atomic.mjs +184 -0
- package/packages/board/src/core/attach-delete.mjs +24 -0
- package/packages/board/src/core/attach-frame-proof.mjs +870 -0
- package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
- package/packages/board/src/core/attach-frame-stream.mjs +266 -0
- package/packages/board/src/core/attach-view-proof.mjs +709 -0
- package/packages/board/src/core/auto-state.mjs +377 -0
- package/packages/board/src/core/background-runs.mjs +273 -0
- package/packages/board/src/core/code-refs-store.mjs +315 -0
- package/packages/board/src/core/code-refs.mjs +913 -0
- package/packages/board/src/core/config.mjs +77 -0
- package/packages/board/src/core/control-protocol.mjs +482 -0
- package/packages/board/src/core/coordinator-client.mjs +633 -0
- package/packages/board/src/core/coordinator-journal.mjs +282 -0
- package/packages/board/src/core/coordinator-protocol.mjs +14 -0
- package/packages/board/src/core/cwd-stats.mjs +114 -0
- package/packages/board/src/core/dashboard-render.mjs +10 -0
- package/packages/board/src/core/derive.mjs +117 -0
- package/packages/board/src/core/diagnostics.mjs +133 -0
- package/packages/board/src/core/double-press.mjs +26 -0
- package/packages/board/src/core/editor-state-reporter.mjs +112 -0
- package/packages/board/src/core/events.mjs +286 -0
- package/packages/board/src/core/evidence.mjs +245 -0
- package/packages/board/src/core/follow-up-queue.mjs +211 -0
- package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
- package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
- package/packages/board/src/core/frame-witness-update.mjs +35 -0
- package/packages/board/src/core/heuristics.mjs +313 -0
- package/packages/board/src/core/hold-input.mjs +27 -0
- package/packages/board/src/core/host-coordination.mjs +300 -0
- package/packages/board/src/core/host-crash.mjs +79 -0
- package/packages/board/src/core/host-probe.mjs +207 -0
- package/packages/board/src/core/host-protocol.mjs +73 -0
- package/packages/board/src/core/ids.mjs +35 -0
- package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
- package/packages/board/src/core/invocation.mjs +43 -0
- package/packages/board/src/core/launch-options.mjs +418 -0
- package/packages/board/src/core/launch.mjs +157 -0
- package/packages/board/src/core/load-config.mjs +4 -0
- package/packages/board/src/core/locks.mjs +530 -0
- package/packages/board/src/core/paths.mjs +183 -0
- package/packages/board/src/core/pid.mjs +75 -0
- package/packages/board/src/core/prewarm-schedule.mjs +41 -0
- package/packages/board/src/core/process-identity-probe.mjs +228 -0
- package/packages/board/src/core/prompt-transport.mjs +13 -0
- package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
- package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
- package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
- package/packages/board/src/core/pty-attach-render.mjs +229 -0
- package/packages/board/src/core/pty-links.mjs +81 -0
- package/packages/board/src/core/pty-scroll.mjs +522 -0
- package/packages/board/src/core/pty-support.mjs +327 -0
- package/packages/board/src/core/repo.mjs +103 -0
- package/packages/board/src/core/rows.mjs +414 -0
- package/packages/board/src/core/screen-log-gc.mjs +198 -0
- package/packages/board/src/core/screen-log.mjs +160 -0
- package/packages/board/src/core/session-view.mjs +174 -0
- package/packages/board/src/core/slash-commands.mjs +52 -0
- package/packages/board/src/core/state-commands.mjs +757 -0
- package/packages/board/src/core/status-consistency.mjs +98 -0
- package/packages/board/src/core/steering-prompts.mjs +34 -0
- package/packages/board/src/core/steering.mjs +133 -0
- package/packages/board/src/core/stop-latch.mjs +41 -0
- package/packages/board/src/core/store.mjs +523 -0
- package/packages/board/src/core/suspend.mjs +36 -0
- package/packages/board/src/core/terminal-attach-client.mjs +981 -0
- package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
- package/packages/board/src/core/terminal-model.mjs +222 -0
- package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
- package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
- package/packages/board/src/core/terminal-snapshot.mjs +440 -0
- package/packages/board/src/core/test-knobs.mjs +28 -0
- package/packages/board/src/core/title.mjs +43 -0
- package/packages/board/src/core/types.mjs +414 -0
- package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
- package/packages/board/src/core/worktree.mjs +65 -0
- package/packages/board/src/index.ts +303 -0
- package/packages/board/src/runtime/service.mjs +3222 -0
- package/packages/board/src/ui/chrome.ts +20 -0
- package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
- package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
- package/packages/board/src/ui/dashboard.ts +2237 -0
- package/packages/board/src/ui/double-left.ts +30 -0
- package/packages/board/src/ui/external-editor.ts +25 -0
- package/packages/board/src/ui/focus.ts +32 -0
- package/packages/board/src/ui/pty-attach.ts +3184 -0
- package/packages/board/src/ui/waiting.ts +30 -0
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +19 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +109 -0
- package/packages/frame/CHANGELOG.md +113 -0
- package/packages/frame/CONTRIBUTING.md +7 -0
- package/packages/frame/LICENSE +22 -0
- package/packages/frame/README.md +105 -0
- package/packages/frame/SECURITY.md +16 -0
- package/packages/frame/docs/how-it-works.md +53 -0
- package/packages/frame/docs/reference.md +78 -0
- package/packages/frame/package.json +72 -0
- package/packages/frame/src/data/git.ts +90 -0
- package/packages/frame/src/data/github.ts +140 -0
- package/packages/frame/src/data/gitlab.ts +63 -0
- package/packages/frame/src/data/mode.ts +49 -0
- package/packages/frame/src/data/past-prompts.ts +57 -0
- package/packages/frame/src/data/runner.ts +32 -0
- package/packages/frame/src/data/snapshot.ts +223 -0
- package/packages/frame/src/data/start-notes.ts +104 -0
- package/packages/frame/src/data/telemetry.ts +337 -0
- package/packages/frame/src/data/todos.ts +162 -0
- package/packages/frame/src/data/totals.ts +94 -0
- package/packages/frame/src/data/visits.ts +45 -0
- package/packages/frame/src/index.ts +696 -0
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +157 -0
- package/packages/frame/src/render/fit.ts +86 -0
- package/packages/frame/src/render/format.ts +108 -0
- package/packages/frame/src/render/frame.ts +106 -0
- package/packages/frame/src/render/glyphs.ts +189 -0
- package/packages/frame/src/render/keys.ts +43 -0
- package/packages/frame/src/render/lead.ts +37 -0
- package/packages/frame/src/render/paint.ts +135 -0
- package/packages/frame/src/render/safe.ts +19 -0
- package/packages/frame/src/render/segments.ts +735 -0
- package/packages/frame/src/render/streaming.ts +223 -0
- package/packages/frame/src/render/timing.ts +106 -0
- package/packages/frame/src/render/tools.ts +167 -0
- package/packages/frame/src/settings.ts +105 -0
- package/packages/frame/src/ui/editor.ts +272 -0
- package/packages/frame/src/ui/effort.ts +36 -0
- package/packages/frame/src/ui/footer.ts +165 -0
- package/packages/frame/src/ui/header.ts +362 -0
- package/packages/frame/src/ui/history-search.ts +23 -0
- package/packages/frame/src/ui/interrupt.ts +21 -0
- package/packages/frame/src/ui/outputs.ts +170 -0
- package/packages/frame/src/ui/pi-entries.ts +298 -0
- package/packages/frame/src/ui/previews.ts +72 -0
- package/packages/frame/src/ui/queue.ts +75 -0
- package/packages/frame/src/ui/screen.ts +59 -0
- package/packages/frame/src/ui/timings.ts +48 -0
- package/packages/frame/src/ui/todos.ts +205 -0
- package/packages/frame/src/ui/tool-frame.ts +595 -0
- package/packages/frame/src/ui/working.ts +25 -0
- package/packages/frame/src/view/state.ts +97 -0
- package/packages/frame/src/view/transcript.ts +175 -0
- package/packages/frame/src/view/view.ts +162 -0
- package/packages/memory/CONTRIBUTING.md +7 -0
- package/packages/memory/LICENSE +21 -0
- package/packages/memory/README.md +44 -0
- package/packages/memory/SECURITY.md +20 -0
- package/packages/memory/package.json +55 -0
- package/packages/memory/skills/review-memory/SKILL.md +48 -0
- package/packages/memory/src/index.ts +163 -0
- package/packages/memory/src/prompt.ts +25 -0
- package/packages/memory/src/review.ts +150 -0
- package/packages/memory/src/secrets.ts +22 -0
- package/packages/memory/src/store.ts +173 -0
- package/packages/modes/CHANGELOG.md +104 -0
- package/packages/modes/CONTRIBUTING.md +7 -0
- package/packages/modes/LICENSE +21 -0
- package/packages/modes/README.md +174 -0
- package/packages/modes/SECURITY.md +17 -0
- package/packages/modes/package.json +55 -0
- package/packages/modes/skills/review-permissions/SKILL.md +54 -0
- package/packages/modes/src/bash.ts +218 -0
- package/packages/modes/src/deny-off.ts +62 -0
- package/packages/modes/src/git-safety.ts +72 -0
- package/packages/modes/src/index.ts +338 -0
- package/packages/modes/src/interview.ts +206 -0
- package/packages/modes/src/judge-gate.ts +113 -0
- package/packages/modes/src/modes.ts +121 -0
- package/packages/modes/src/review/apply.mjs +156 -0
- package/packages/modes/src/review/collect.mjs +208 -0
- package/packages/modes/src/review/tools.ts +71 -0
- package/packages/modes/src/sandbox.ts +37 -0
- package/packages/router/CHANGELOG.md +8 -0
- package/packages/router/LICENSE +21 -0
- package/packages/router/README.md +90 -0
- package/packages/router/package.json +51 -0
- package/packages/router/src/config.ts +65 -0
- package/packages/router/src/index.ts +172 -0
- package/packages/router/src/judge.ts +51 -0
- package/packages/router/src/route.ts +67 -0
- package/packages/web/CHANGELOG.md +36 -0
- package/packages/web/LICENSE +21 -0
- package/packages/web/README.md +87 -0
- package/packages/web/SECURITY.md +13 -0
- package/packages/web/package.json +59 -0
- package/packages/web/src/config.ts +109 -0
- package/packages/web/src/dnsfilter.ts +97 -0
- package/packages/web/src/extract.ts +65 -0
- package/packages/web/src/index.ts +217 -0
- package/packages/web/src/markdown.ts +122 -0
- package/packages/web/src/net.ts +109 -0
- package/packages/web/src/reader.ts +73 -0
- package/packages/web/src/sanitize.ts +50 -0
- package/packages/web/src/search.ts +76 -0
- package/packages/web/src/secrets.ts +22 -0
- package/packages/web/src/validate.ts +186 -0
- package/src/index.ts +156 -0
- package/src/settings.ts +79 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Who caused a state change: the operator at the terminal, the operator on a phone, or an agent.
|
|
2
|
+
import { isObject } from "../decode.ts";
|
|
3
|
+
|
|
4
|
+
export interface By {
|
|
5
|
+
kind: "terminal" | "client" | "agent";
|
|
6
|
+
/** The handle for a terminal or an agent; the device's name for a client. */
|
|
7
|
+
name: string;
|
|
8
|
+
/** The hostname. */
|
|
9
|
+
machine: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const KINDS: ReadonlySet<string> = new Set<By["kind"]>(["terminal", "client", "agent"]);
|
|
13
|
+
|
|
14
|
+
export function isBy(value: unknown): value is By {
|
|
15
|
+
return (
|
|
16
|
+
isObject(value) &&
|
|
17
|
+
typeof value.kind === "string" &&
|
|
18
|
+
KINDS.has(value.kind) &&
|
|
19
|
+
typeof value.name === "string" &&
|
|
20
|
+
typeof value.machine === "string"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// What decided a tool call, published by the deciding feature and read by whoever frames the call.
|
|
2
|
+
import { defineContract } from "../contract.ts";
|
|
3
|
+
import { boolean, guarded, isObject } from "../decode.ts";
|
|
4
|
+
|
|
5
|
+
export const RULING_TONES = ["pending", "success", "warning", "error"] as const;
|
|
6
|
+
|
|
7
|
+
export type RulingTone = (typeof RULING_TONES)[number];
|
|
8
|
+
|
|
9
|
+
export interface CallRuling {
|
|
10
|
+
tone: RulingTone;
|
|
11
|
+
head: string;
|
|
12
|
+
why?: string;
|
|
13
|
+
note?: string;
|
|
14
|
+
detail?: string;
|
|
15
|
+
blocked?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Keyed by tool call id. */
|
|
19
|
+
export const RULING = defineContract<CallRuling, string>(
|
|
20
|
+
"calls:ruling",
|
|
21
|
+
guarded(isRuling, "a ruling with a tone and a head"),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/** A feature that frames every call answers true, so the ruling is not drawn twice. */
|
|
25
|
+
export const CALLS_FRAMED = defineContract<boolean>("calls:framed", boolean);
|
|
26
|
+
|
|
27
|
+
function isRuling(value: unknown): value is CallRuling {
|
|
28
|
+
return (
|
|
29
|
+
isObject(value) &&
|
|
30
|
+
typeof value.head === "string" &&
|
|
31
|
+
RULING_TONES.some((tone) => tone === value.tone)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** In the order they are worth trying. */
|
|
36
|
+
export const TARGET_FIELDS = ["path", "file_path", "command", "query", "url", "pattern"] as const;
|
|
37
|
+
|
|
38
|
+
/** The path, command or query, cut to its first line; empty when the arguments name nothing. */
|
|
39
|
+
export function callTarget(args: unknown): string {
|
|
40
|
+
const fields = typeof args === "object" && args !== null ? (args as Record<string, unknown>) : {};
|
|
41
|
+
for (const field of TARGET_FIELDS) {
|
|
42
|
+
const value = fields[field];
|
|
43
|
+
if (typeof value === "string" && value.trim() !== "") return value.split("\n")[0] ?? "";
|
|
44
|
+
}
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { BoardView } from "../board/model.ts";
|
|
2
|
+
// One shape per event type. Shapes only grow: a field that changes meaning gets a new name.
|
|
3
|
+
import type { AskQuestion, AskResult } from "./ask.ts";
|
|
4
|
+
import type { By } from "./by.ts";
|
|
5
|
+
import type { CallRuling } from "./calls.ts";
|
|
6
|
+
import type { PermissionMode } from "./mode.ts";
|
|
7
|
+
|
|
8
|
+
export type RoomActivity = "running" | "idle";
|
|
9
|
+
|
|
10
|
+
export type Delivery = "woke" | "after-run" | "steered" | "next-turn";
|
|
11
|
+
|
|
12
|
+
/** Why a message handed for the next turn waits there. */
|
|
13
|
+
export type Held = "asleep" | "quiet" | "full" | "stopped";
|
|
14
|
+
|
|
15
|
+
export type Activity =
|
|
16
|
+
| { kind: "edited"; path: string }
|
|
17
|
+
| { kind: "ran"; command: string; ok: boolean }
|
|
18
|
+
| { kind: "committed"; sha: string; subject: string };
|
|
19
|
+
|
|
20
|
+
/** `folder` is absolute with symlinks resolved. */
|
|
21
|
+
export type Member = { folder: string } | { session: string; cwd: string };
|
|
22
|
+
|
|
23
|
+
/** `listening` hears only what names it. `@everyone` and `@anyone` reach only `in`. */
|
|
24
|
+
export type RoomMode = "in" | "listening" | "out";
|
|
25
|
+
|
|
26
|
+
export interface LogEventMap {
|
|
27
|
+
/** `start` tells this process from a later one with the same pid. */
|
|
28
|
+
"session.started": { pid: number; host: string; model?: string; cwd?: string; start?: string };
|
|
29
|
+
"session.ended": Record<string, never>;
|
|
30
|
+
"session.named": { handle?: string };
|
|
31
|
+
"board.changed": { view: BoardView };
|
|
32
|
+
"session.state": { state: RoomActivity };
|
|
33
|
+
/** The text stays in pi's own session file. */
|
|
34
|
+
"session.prompted": Record<string, never>;
|
|
35
|
+
/** A prompt from somewhere other than the terminal; its text is in pi's session file. */
|
|
36
|
+
"prompt.received": { by: By };
|
|
37
|
+
activity: Activity;
|
|
38
|
+
/** `to` is session ids, "all" (`@everyone`) or "any" (`@anyone`). */
|
|
39
|
+
"message.posted": {
|
|
40
|
+
id: string;
|
|
41
|
+
from: "you" | "agent";
|
|
42
|
+
to: string[] | "all" | "any";
|
|
43
|
+
text: string;
|
|
44
|
+
urgent?: boolean;
|
|
45
|
+
/** 0 when a person started the run, d + 1 when a message of depth d woke it. Depth 4 wakes no one. */
|
|
46
|
+
depth?: number;
|
|
47
|
+
};
|
|
48
|
+
/** Written by the receiving session when it hands the message to its agent. */
|
|
49
|
+
"message.received": { id: string; mode: Delivery; held?: Held };
|
|
50
|
+
/** The operator lets through what `session` holds for its next turn. */
|
|
51
|
+
"messages.released": { session: string };
|
|
52
|
+
"message.read": { id: string };
|
|
53
|
+
"request.bid": { request: string; fit: number; reason: string; idle: boolean };
|
|
54
|
+
/** Written only by the session every other one agrees won. */
|
|
55
|
+
"request.taken": { request: string };
|
|
56
|
+
/** Its name is for people; two rooms may share one. */
|
|
57
|
+
"room.created": { name: string };
|
|
58
|
+
"room.added":
|
|
59
|
+
| {
|
|
60
|
+
member: { folder: string };
|
|
61
|
+
by: "you" | "agent";
|
|
62
|
+
why?: string;
|
|
63
|
+
allowedBy?: string;
|
|
64
|
+
/** A folder brings its sessions in taking part. */
|
|
65
|
+
mode?: never;
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
member: { session: string; cwd: string };
|
|
69
|
+
by: "you" | "agent";
|
|
70
|
+
why?: string;
|
|
71
|
+
allowedBy?: string;
|
|
72
|
+
mode?: RoomMode;
|
|
73
|
+
};
|
|
74
|
+
"room.mode": { member: Member; mode: Exclude<RoomMode, "out"> };
|
|
75
|
+
/** A removed session stays out while its folder is in, until added by name again. */
|
|
76
|
+
"room.removed": { member: Member };
|
|
77
|
+
"room.closed": Record<string, never>;
|
|
78
|
+
/** `source` is the feature that asked. */
|
|
79
|
+
"ask.opened": { id: string; source: string; questions: AskQuestion[]; expiresAt?: number };
|
|
80
|
+
/** Without `by`, nobody known closed it. */
|
|
81
|
+
"ask.closed": { id: string; by?: By; result: AskResult };
|
|
82
|
+
/** `by` is the person who answered, when one did. */
|
|
83
|
+
"call.ruled": { toolCallId: string; ruling: CallRuling; by?: By };
|
|
84
|
+
"mode.changed": { mode: PermissionMode };
|
|
85
|
+
/** Logged before the command runs, so a resent id is a repeat across restarts. */
|
|
86
|
+
"command.received": { id: string; command: string; by?: By };
|
|
87
|
+
/** On the id of its `command.received` line. */
|
|
88
|
+
"command.outcome": { id: string; command: string; by?: By; outcome: "refused" | "failed" };
|
|
89
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineContract } from "../contract.ts";
|
|
2
|
+
import { type Problem, isObject, literal, pass } from "../decode.ts";
|
|
3
|
+
import type { By } from "./by.ts";
|
|
4
|
+
|
|
5
|
+
export const PERMISSION_MODES = ["manual", "edits", "judge", "full"] as const;
|
|
6
|
+
|
|
7
|
+
export type PermissionMode = (typeof PERMISSION_MODES)[number];
|
|
8
|
+
|
|
9
|
+
export const MODE = defineContract<PermissionMode>("mode", literal(...PERMISSION_MODES));
|
|
10
|
+
|
|
11
|
+
export interface ModeSet {
|
|
12
|
+
/** Applied as if typed at the terminal, and announced through `MODE`. */
|
|
13
|
+
set(mode: PermissionMode, by?: By): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isModeSet(value: unknown): value is ModeSet {
|
|
17
|
+
return isObject(value) && typeof value.set === "function";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const MODE_SET = defineContract<ModeSet>("mode:set", {
|
|
21
|
+
decode(value) {
|
|
22
|
+
return isModeSet(value)
|
|
23
|
+
? pass(value)
|
|
24
|
+
: { ok: false, problems: [{ path: "", message: "a way to set the mode" }] as Problem[] };
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineContract } from "../contract.ts";
|
|
2
|
+
import { guarded, isObject } from "../decode.ts";
|
|
3
|
+
|
|
4
|
+
export interface NamesValue {
|
|
5
|
+
handle: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const NAMES = defineContract<NamesValue>("names", guarded(isNames, "a name"));
|
|
9
|
+
|
|
10
|
+
function isNames(value: unknown): value is NamesValue {
|
|
11
|
+
return isObject(value) && typeof value.handle === "string" && value.handle.length > 0;
|
|
12
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { defineContract } from "../contract.ts";
|
|
2
|
+
import { guarded, isObject } from "../decode.ts";
|
|
3
|
+
import type { RoomMode } from "./events.ts";
|
|
4
|
+
|
|
5
|
+
export interface PeerMark {
|
|
6
|
+
handle: string;
|
|
7
|
+
/** A theme color name, so a peer looks the same in the footer, the room and the board. */
|
|
8
|
+
color: string;
|
|
9
|
+
state: "running" | "idle" | "waiting";
|
|
10
|
+
room?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A room this session is in, how it stands there, and the other agents in it. */
|
|
14
|
+
export interface RoomMark {
|
|
15
|
+
name: string;
|
|
16
|
+
mode: RoomMode;
|
|
17
|
+
peers: PeerMark[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WritingMark {
|
|
21
|
+
handle: string;
|
|
22
|
+
color: string;
|
|
23
|
+
/** `you`, `@handle` or `everyone`. */
|
|
24
|
+
to: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PeersState {
|
|
28
|
+
peers: PeerMark[];
|
|
29
|
+
/** Absent when the session is in no room yet, which differs from a session with no color. */
|
|
30
|
+
me?: PeerMark;
|
|
31
|
+
/** Messages to this session its agent has not read yet. */
|
|
32
|
+
unread: number;
|
|
33
|
+
/** A draft is never worth a frame over the editor, so it goes to the footer. */
|
|
34
|
+
writing?: WritingMark[];
|
|
35
|
+
/** The folder's room first. */
|
|
36
|
+
rooms?: RoomMark[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const PEERS = defineContract<PeersState>(
|
|
40
|
+
"peers",
|
|
41
|
+
guarded(isPeersState, "a list of peers and an unread count"),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const STATES = new Set(["running", "idle", "waiting"]);
|
|
45
|
+
|
|
46
|
+
function isPeersState(value: unknown): value is PeersState {
|
|
47
|
+
if (!isObject(value) || typeof value.unread !== "number") return false;
|
|
48
|
+
if (!Array.isArray(value.peers) || !value.peers.every(isPeerMark)) return false;
|
|
49
|
+
if (value.me !== undefined && !isPeerMark(value.me)) return false;
|
|
50
|
+
if (value.rooms !== undefined && !(Array.isArray(value.rooms) && value.rooms.every(isRoomMark)))
|
|
51
|
+
return false;
|
|
52
|
+
if (value.writing === undefined) return true;
|
|
53
|
+
return Array.isArray(value.writing) && value.writing.every(isWritingMark);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const MODES = new Set(["in", "listening", "out"]);
|
|
57
|
+
|
|
58
|
+
function isRoomMark(value: unknown): value is RoomMark {
|
|
59
|
+
if (!isObject(value) || typeof value.name !== "string") return false;
|
|
60
|
+
if (typeof value.mode !== "string" || !MODES.has(value.mode)) return false;
|
|
61
|
+
return Array.isArray(value.peers) && value.peers.every(isPeerMark);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isWritingMark(value: unknown): value is WritingMark {
|
|
65
|
+
if (!isObject(value)) return false;
|
|
66
|
+
return (
|
|
67
|
+
typeof value.handle === "string" &&
|
|
68
|
+
typeof value.color === "string" &&
|
|
69
|
+
typeof value.to === "string"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isPeerMark(value: unknown): value is PeerMark {
|
|
74
|
+
if (!isObject(value) || typeof value.handle !== "string" || typeof value.color !== "string")
|
|
75
|
+
return false;
|
|
76
|
+
return typeof value.state === "string" && STATES.has(value.state);
|
|
77
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Where a prompt came from when a person sent it from a client. The routing of `@` mentions treats it
|
|
2
|
+
// as typed at the terminal, and the transcript says where it came from.
|
|
3
|
+
import { defineContract } from "../contract.ts";
|
|
4
|
+
import { guarded } from "../decode.ts";
|
|
5
|
+
import { type By, isBy } from "./by.ts";
|
|
6
|
+
|
|
7
|
+
/** Keyed by the prompt's text, as pi hands it to `input` and draws it. */
|
|
8
|
+
export const PROMPT_FROM = defineContract<By, string>("prompts:from", guarded(isBy, "a sender"));
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
// Every harness piece is its own extension, so the screen asks them over pi.events.
|
|
4
|
+
import type { Control, Json } from "../control.ts";
|
|
5
|
+
import type { Layer } from "../settings/store.ts";
|
|
6
|
+
|
|
7
|
+
export const LIST = "harness:screen:list";
|
|
8
|
+
export const APPLY = "harness:screen:apply";
|
|
9
|
+
export const RUN = "harness:screen:run";
|
|
10
|
+
export const DONE = "harness:screen:done";
|
|
11
|
+
export const CHANGED = "harness:settings:changed";
|
|
12
|
+
|
|
13
|
+
export type RowKind = "setting" | "value" | "action" | "info";
|
|
14
|
+
|
|
15
|
+
export interface RowView {
|
|
16
|
+
feature: string;
|
|
17
|
+
id: string;
|
|
18
|
+
kind: RowKind;
|
|
19
|
+
section: string;
|
|
20
|
+
label: string;
|
|
21
|
+
description: string;
|
|
22
|
+
control?: Control;
|
|
23
|
+
value?: Json;
|
|
24
|
+
text?: string;
|
|
25
|
+
confirm?: string;
|
|
26
|
+
indent?: number;
|
|
27
|
+
/** When that is not this session. */
|
|
28
|
+
meta?: string;
|
|
29
|
+
layer?: Layer;
|
|
30
|
+
fallback?: Json;
|
|
31
|
+
/** The global value a project value hides. */
|
|
32
|
+
hidden?: Json;
|
|
33
|
+
restart?: boolean;
|
|
34
|
+
/** Drawn under the list. */
|
|
35
|
+
preview?: Preview;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type Preview = (value: Json, theme: Theme, width: number) => string[];
|
|
39
|
+
|
|
40
|
+
export interface TabView {
|
|
41
|
+
title: string;
|
|
42
|
+
sections: string[];
|
|
43
|
+
rows: RowView[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ListRequest {
|
|
47
|
+
tabs: TabView[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `unset` drops the value that wins, so the one below shows. */
|
|
51
|
+
export interface ApplyRequest {
|
|
52
|
+
feature: string;
|
|
53
|
+
id: string;
|
|
54
|
+
op: "set" | "unset";
|
|
55
|
+
value?: Json;
|
|
56
|
+
answer?: { error?: string };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RunRequest {
|
|
60
|
+
feature: string;
|
|
61
|
+
id: string;
|
|
62
|
+
request: string;
|
|
63
|
+
answer?: { error?: string };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface RunDone {
|
|
67
|
+
request: string;
|
|
68
|
+
text?: string;
|
|
69
|
+
error?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Changed {
|
|
73
|
+
/** The store that wrote, so it skips its own write. */
|
|
74
|
+
source: string;
|
|
75
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { defineContract, type EventScope } from "../contract.ts";
|
|
2
|
+
import { stringList } from "../decode.ts";
|
|
3
|
+
|
|
4
|
+
export const SKILLS = defineContract<string[]>("skills:names", stringList("skill names"));
|
|
5
|
+
|
|
6
|
+
export const SKILL_REF = "/skill:";
|
|
7
|
+
|
|
8
|
+
/** Empty when no skills feature runs. */
|
|
9
|
+
export function skillNames(scope: EventScope): string[] {
|
|
10
|
+
return SKILLS.current(scope) ?? [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Group 1 is what came before, group 2 the name. */
|
|
14
|
+
function skillRefPattern(names: readonly string[]): RegExp | undefined {
|
|
15
|
+
if (names.length === 0) return undefined;
|
|
16
|
+
const alternatives = names
|
|
17
|
+
.toSorted((left, right) => right.length - left.length)
|
|
18
|
+
.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
19
|
+
.join("|");
|
|
20
|
+
return new RegExp(`(^|[\\s([{"'])${SKILL_REF}(${alternatives})(?![\\w-])`, "g");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TYPING = /(?:^|[\s([{"'])(\/([\w.:-]*))$/;
|
|
24
|
+
|
|
25
|
+
export interface SkillQuery {
|
|
26
|
+
/** What the completion replaces, slash included. */
|
|
27
|
+
typed: string;
|
|
28
|
+
query: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Undefined at the start of the message, where pi's own palette opens. */
|
|
32
|
+
export function skillQueryAt(
|
|
33
|
+
lines: readonly string[],
|
|
34
|
+
cursorLine: number,
|
|
35
|
+
cursorCol: number,
|
|
36
|
+
): SkillQuery | undefined {
|
|
37
|
+
const before = (lines[cursorLine] ?? "").slice(0, cursorCol);
|
|
38
|
+
const match = TYPING.exec(before);
|
|
39
|
+
if (match === null) return undefined;
|
|
40
|
+
const [, typed = "", query = ""] = match;
|
|
41
|
+
if (cursorLine === 0 && before.trimStart() === typed) return undefined;
|
|
42
|
+
return { typed, query };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function referencedSkills(text: string, names: readonly string[]): string[] {
|
|
46
|
+
const pattern = skillRefPattern(names);
|
|
47
|
+
if (pattern === undefined) return [];
|
|
48
|
+
const found: string[] = [];
|
|
49
|
+
for (const prose of outsideCode(text)) {
|
|
50
|
+
for (const match of prose.matchAll(pattern)) {
|
|
51
|
+
const name = match[2];
|
|
52
|
+
if (name !== undefined && !found.includes(name)) found.push(name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function drawSkillRefs(
|
|
59
|
+
text: string,
|
|
60
|
+
names: readonly string[],
|
|
61
|
+
draw: (name: string) => string,
|
|
62
|
+
): string {
|
|
63
|
+
const pattern = skillRefPattern(names);
|
|
64
|
+
if (pattern === undefined) return text;
|
|
65
|
+
return splitCode(text)
|
|
66
|
+
.map(({ code, part }) =>
|
|
67
|
+
code
|
|
68
|
+
? part
|
|
69
|
+
: part.replace(pattern, (_whole, before: string, name: string) => `${before}${draw(name)}`),
|
|
70
|
+
)
|
|
71
|
+
.join("");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The text cut at inline code: `/skill:x` in backticks stays text. */
|
|
75
|
+
function splitCode(text: string): { code: boolean; part: string }[] {
|
|
76
|
+
return text.split(/(`[^`]*`)/).map((part, index) => ({ code: index % 2 === 1, part }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function outsideCode(text: string): string[] {
|
|
80
|
+
return splitCode(text).flatMap(({ code, part }) => (code ? [] : [part]));
|
|
81
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Screens draw a session from a source: this pi, a session file, or a remote one. A source holds
|
|
2
|
+
// functions, so it travels on raw channels rather than `defineContract`, whose values are JSON.
|
|
3
|
+
import type { ExtensionAPI, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
import { isObject } from "../decode.ts";
|
|
6
|
+
|
|
7
|
+
type Events = ExtensionAPI["events"];
|
|
8
|
+
|
|
9
|
+
export interface SessionSummary {
|
|
10
|
+
session: string;
|
|
11
|
+
/** The hostname. */
|
|
12
|
+
machine: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
/** Pi's `/name`, not the room's handle. */
|
|
15
|
+
name?: string;
|
|
16
|
+
/** Pi draws the branch from it back to the root. */
|
|
17
|
+
leafId?: string;
|
|
18
|
+
model?: { provider: string; id: string };
|
|
19
|
+
thinkingLevel?: string;
|
|
20
|
+
/** `window` is missing where the source cannot know it, such as a file. */
|
|
21
|
+
context?: { tokens: number; window?: number };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SourceCursor {
|
|
25
|
+
entry?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `reset`: the source lost its place, so drop what was held; everything follows from the start. */
|
|
29
|
+
export type SourceItem =
|
|
30
|
+
| { kind: "entry"; entry: SessionEntry }
|
|
31
|
+
| { kind: "summary"; summary: SessionSummary }
|
|
32
|
+
| { kind: "reset" }
|
|
33
|
+
| { kind: "ended" };
|
|
34
|
+
|
|
35
|
+
export interface SessionSource {
|
|
36
|
+
open(): Promise<SessionSummary>;
|
|
37
|
+
/** Follows from `after`, or from the start, until the returned function is called. */
|
|
38
|
+
follow(after: SourceCursor | undefined, on: (item: SourceItem) => void): () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `file` lets a session of this machine be read without anyone's help. */
|
|
42
|
+
export interface SourceAddress {
|
|
43
|
+
session: string;
|
|
44
|
+
machine: string;
|
|
45
|
+
file?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Remote answers with its sources; the kit's file source is the fallback. */
|
|
49
|
+
export const SOURCES = "harness:sources";
|
|
50
|
+
interface SourceRequest {
|
|
51
|
+
address: SourceAddress;
|
|
52
|
+
source?: SessionSource;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** An undefined answer leaves the address to another provider. */
|
|
56
|
+
export function provideSources(
|
|
57
|
+
events: Events,
|
|
58
|
+
answer: (address: SourceAddress) => SessionSource | undefined,
|
|
59
|
+
): () => void {
|
|
60
|
+
return events.on(SOURCES, (data) => {
|
|
61
|
+
const request = data as SourceRequest;
|
|
62
|
+
if (!isObject(data) || request.source !== undefined || !isSourceAddress(request.address))
|
|
63
|
+
return;
|
|
64
|
+
request.source = answer(request.address);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function askSource(events: Events, address: SourceAddress): SessionSource | undefined {
|
|
69
|
+
const request: SourceRequest = { address };
|
|
70
|
+
events.emit(SOURCES, request);
|
|
71
|
+
return request.source;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isSourceAddress(value: unknown): value is SourceAddress {
|
|
75
|
+
return (
|
|
76
|
+
isObject(value) &&
|
|
77
|
+
typeof value.session === "string" &&
|
|
78
|
+
typeof value.machine === "string" &&
|
|
79
|
+
(value.file === undefined || typeof value.file === "string")
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Showing a session over the screen: the look answers, the panel and remote ask. Raw channels, like the sources'.
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
import { isObject } from "../decode.ts";
|
|
5
|
+
import { isSourceAddress, type SourceAddress } from "./sources.ts";
|
|
6
|
+
|
|
7
|
+
type Events = ExtensionAPI["events"];
|
|
8
|
+
|
|
9
|
+
export const VIEW_OPEN = "harness:view.open";
|
|
10
|
+
export const VIEW_AVAILABLE = "harness:view.available";
|
|
11
|
+
|
|
12
|
+
export function canOpenView(events: Events): boolean {
|
|
13
|
+
const probe = { available: false };
|
|
14
|
+
events.emit(VIEW_AVAILABLE, probe);
|
|
15
|
+
return probe.available;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** False when nobody took the request. */
|
|
19
|
+
export function openView(events: Events, address: SourceAddress): boolean {
|
|
20
|
+
const request = { address, opened: false };
|
|
21
|
+
events.emit(VIEW_OPEN, request);
|
|
22
|
+
return request.opened;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function provideViews(
|
|
26
|
+
events: Events,
|
|
27
|
+
open: (address: SourceAddress) => boolean,
|
|
28
|
+
available: () => boolean = () => true,
|
|
29
|
+
): () => void {
|
|
30
|
+
const stops = [
|
|
31
|
+
events.on(VIEW_AVAILABLE, (data) => {
|
|
32
|
+
if (isObject(data) && available()) data.available = true;
|
|
33
|
+
}),
|
|
34
|
+
events.on(VIEW_OPEN, (data) => {
|
|
35
|
+
if (!isObject(data) || data.opened === true || !isSourceAddress(data.address)) return;
|
|
36
|
+
data.opened = open(data.address);
|
|
37
|
+
}),
|
|
38
|
+
];
|
|
39
|
+
return () => {
|
|
40
|
+
for (const stop of stops) stop();
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Plain JSON, because the screen may live in another package.
|
|
2
|
+
export type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
|
|
3
|
+
|
|
4
|
+
export interface ControlOption {
|
|
5
|
+
value: Json;
|
|
6
|
+
label?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Control =
|
|
11
|
+
| { type: "toggle" }
|
|
12
|
+
| { type: "choice"; options: ControlOption[]; custom?: boolean }
|
|
13
|
+
| { type: "number"; min?: number; max?: number; step?: number; unit?: string; nullable?: boolean }
|
|
14
|
+
| { type: "text"; multiline?: boolean; presets?: ControlOption[] }
|
|
15
|
+
| { type: "list"; options?: ControlOption[] };
|
|
16
|
+
|
|
17
|
+
export function formatValue(control: Control, value: Json | undefined): string {
|
|
18
|
+
if (value === undefined || value === null) return control.type === "number" ? "none" : "";
|
|
19
|
+
switch (control.type) {
|
|
20
|
+
case "toggle":
|
|
21
|
+
return value === true ? "on" : "off";
|
|
22
|
+
case "choice":
|
|
23
|
+
return optionLabel(control.options, value);
|
|
24
|
+
case "number":
|
|
25
|
+
return control.unit === undefined ? String(value) : `${value}${control.unit}`;
|
|
26
|
+
case "text":
|
|
27
|
+
if (typeof value !== "string") return JSON.stringify(value);
|
|
28
|
+
if (control.presets !== undefined) {
|
|
29
|
+
const preset = control.presets.find((option) => option.value === value);
|
|
30
|
+
if (preset !== undefined) return preset.label ?? String(preset.value);
|
|
31
|
+
if (value.trim() === "") return "(empty)";
|
|
32
|
+
return "custom";
|
|
33
|
+
}
|
|
34
|
+
return firstLine(value);
|
|
35
|
+
case "list":
|
|
36
|
+
if (!Array.isArray(value)) return JSON.stringify(value);
|
|
37
|
+
if (value.length === 0) return "(none)";
|
|
38
|
+
return value.map((entry) => optionLabel(control.options ?? [], entry)).join(", ");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function optionLabel(options: readonly ControlOption[], value: Json): string {
|
|
43
|
+
const option = options.find((entry) => sameJson(entry.value, value));
|
|
44
|
+
return optionText(option ?? { value });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function optionText(option: ControlOption): string {
|
|
48
|
+
if (option.label !== undefined) return option.label;
|
|
49
|
+
return typeof option.value === "string" ? option.value : JSON.stringify(option.value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function sameJson(left: unknown, right: unknown): boolean {
|
|
53
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function firstLine(text: string): string {
|
|
57
|
+
const line = text.split("\n")[0] ?? "";
|
|
58
|
+
return text.includes("\n") ? `${line} ...` : line;
|
|
59
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
const BACKGROUND_TOKENS = new Set([
|
|
8
|
+
"selectedBg",
|
|
9
|
+
"searchMatchBg",
|
|
10
|
+
"userMessageBg",
|
|
11
|
+
"customMessageBg",
|
|
12
|
+
"toolPendingBg",
|
|
13
|
+
"toolSuccessBg",
|
|
14
|
+
"toolErrorBg",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/** Read from pi's own theme file, so a pi release that moves it fails loudly. */
|
|
18
|
+
export function darkTheme(): Theme {
|
|
19
|
+
const root = dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent")));
|
|
20
|
+
const file = JSON.parse(
|
|
21
|
+
readFileSync(join(root, "modes/interactive/theme/dark.json"), "utf8"),
|
|
22
|
+
) as { vars: Record<string, string | number>; colors: Record<string, string | number> };
|
|
23
|
+
const resolve = (value: string | number, seen: Set<string>): string | number => {
|
|
24
|
+
if (typeof value !== "string") return value;
|
|
25
|
+
const target = file.vars[value];
|
|
26
|
+
if (target === undefined || seen.has(value)) return value;
|
|
27
|
+
return resolve(target, seen.add(value));
|
|
28
|
+
};
|
|
29
|
+
const foreground: Record<string, string | number> = {};
|
|
30
|
+
const background: Record<string, string | number> = {};
|
|
31
|
+
for (const [token, value] of Object.entries(file.colors)) {
|
|
32
|
+
const target = BACKGROUND_TOKENS.has(token) ? background : foreground;
|
|
33
|
+
target[token] = resolve(value, new Set());
|
|
34
|
+
}
|
|
35
|
+
return new Theme(
|
|
36
|
+
foreground as ConstructorParameters<typeof Theme>[0],
|
|
37
|
+
background as ConstructorParameters<typeof Theme>[1],
|
|
38
|
+
"truecolor",
|
|
39
|
+
{ name: "dark" },
|
|
40
|
+
);
|
|
41
|
+
}
|