pim-agent 0.0.1 → 0.9.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/LICENSE +21 -0
- package/README.md +293 -7
- package/bin/pim.ts +151 -0
- package/package.json +82 -4
- package/packages/core/src/attachments/AttachmentStore.ts +99 -0
- package/packages/core/src/attachments/Attachments.ts +69 -0
- package/packages/core/src/extensions/CoreExtensions.ts +51 -0
- package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
- package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
- package/packages/core/src/extensions/apply-patch/index.ts +69 -0
- package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
- package/packages/core/src/extensions/apply-patch/model.ts +39 -0
- package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
- package/packages/core/src/extensions/apply-patch/render.ts +216 -0
- package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
- package/packages/core/src/extensions/apply-patch/types.ts +30 -0
- package/packages/core/src/extensions/bash/capture.ts +86 -0
- package/packages/core/src/extensions/bash/format.ts +110 -0
- package/packages/core/src/extensions/bash/image.ts +31 -0
- package/packages/core/src/extensions/bash/index.ts +88 -0
- package/packages/core/src/extensions/bash/render.ts +31 -0
- package/packages/core/src/extensions/bash/run.ts +221 -0
- package/packages/core/src/extensions/bash/schema.ts +64 -0
- package/packages/core/src/extensions/edit/edit.ts +366 -0
- package/packages/core/src/extensions/edit/index.ts +36 -0
- package/packages/core/src/extensions/edit/render.ts +16 -0
- package/packages/core/src/extensions/edit/schema.ts +37 -0
- package/packages/core/src/extensions/glob/glob.ts +34 -0
- package/packages/core/src/extensions/glob/index.ts +83 -0
- package/packages/core/src/extensions/glob/render.ts +72 -0
- package/packages/core/src/extensions/glob/schema.ts +66 -0
- package/packages/core/src/extensions/grep/grep.ts +256 -0
- package/packages/core/src/extensions/grep/index.ts +114 -0
- package/packages/core/src/extensions/grep/render.ts +271 -0
- package/packages/core/src/extensions/grep/schema.ts +92 -0
- package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
- package/packages/core/src/extensions/read/image.ts +57 -0
- package/packages/core/src/extensions/read/index.ts +88 -0
- package/packages/core/src/extensions/read/read.ts +191 -0
- package/packages/core/src/extensions/read/render.ts +58 -0
- package/packages/core/src/extensions/read/schema.ts +49 -0
- package/packages/core/src/extensions/subagent/index.ts +33 -0
- package/packages/core/src/extensions/subagent/render.ts +127 -0
- package/packages/core/src/extensions/subagent/schema.ts +9 -0
- package/packages/core/src/extensions/subagent/subagent.ts +419 -0
- package/packages/core/src/extensions/system-prompt/index.ts +28 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +228 -0
- package/packages/core/src/extensions/todo/index.ts +123 -0
- package/packages/core/src/extensions/todo/render.ts +156 -0
- package/packages/core/src/extensions/todo/schema.ts +24 -0
- package/packages/core/src/extensions/todo/todo.ts +185 -0
- package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
- package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
- package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
- package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
- package/packages/core/src/extensions/web-fetch/image.ts +165 -0
- package/packages/core/src/extensions/web-fetch/index.ts +83 -0
- package/packages/core/src/extensions/web-fetch/render.ts +100 -0
- package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
- package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
- package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
- package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
- package/packages/core/src/extensions/web-search/index.ts +90 -0
- package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
- package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
- package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
- package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
- package/packages/core/src/extensions/web-search/render.ts +49 -0
- package/packages/core/src/extensions/web-search/schema.ts +29 -0
- package/packages/core/src/extensions/web-search/search.ts +23 -0
- package/packages/core/src/extensions/write/index.ts +58 -0
- package/packages/core/src/extensions/write/render.ts +16 -0
- package/packages/core/src/extensions/write/schema.ts +14 -0
- package/packages/core/src/extensions/write/write.ts +87 -0
- package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
- package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
- package/packages/core/src/picker/PickerItem.ts +6 -0
- package/packages/core/src/picker/PickerService.ts +110 -0
- package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
- package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
- package/packages/core/src/picker/catalog.ts +220 -0
- package/packages/core/src/picker/commandRanker.ts +17 -0
- package/packages/core/src/picker/filePickerWorker.ts +66 -0
- package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
- package/packages/core/src/picker/ranker.ts +245 -0
- package/packages/core/src/picker/token.ts +70 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +213 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionHost.ts +876 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionRegistry.ts +169 -0
- package/packages/core/src/session/WriteMark.ts +37 -0
- package/packages/core/src/shared/Cli.ts +25 -0
- package/packages/core/src/shared/DaemonUnit.ts +13 -0
- package/packages/core/src/shared/DiffLines.ts +304 -0
- package/packages/core/src/shared/DiffRenderer.ts +283 -0
- package/packages/core/src/shared/DiffView.ts +29 -0
- package/packages/core/src/shared/Directories.ts +61 -0
- package/packages/core/src/shared/EditMatcher.ts +767 -0
- package/packages/core/src/shared/Errors.ts +9 -0
- package/packages/core/src/shared/ExtensionToggles.ts +130 -0
- package/packages/core/src/shared/FileEnumerator.ts +361 -0
- package/packages/core/src/shared/FileScanner.ts +57 -0
- package/packages/core/src/shared/FileWatch.ts +107 -0
- package/packages/core/src/shared/Format.ts +74 -0
- package/packages/core/src/shared/Fs.ts +91 -0
- package/packages/core/src/shared/FsErrors.ts +67 -0
- package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
- package/packages/core/src/shared/Git.ts +89 -0
- package/packages/core/src/shared/GlobExclusions.ts +13 -0
- package/packages/core/src/shared/Http.ts +14 -0
- package/packages/core/src/shared/ImageMime.ts +36 -0
- package/packages/core/src/shared/Images.ts +357 -0
- package/packages/core/src/shared/Json.ts +19 -0
- package/packages/core/src/shared/Languages.ts +95 -0
- package/packages/core/src/shared/Levenshtein.ts +30 -0
- package/packages/core/src/shared/Lines.ts +79 -0
- package/packages/core/src/shared/McpClient.ts +444 -0
- package/packages/core/src/shared/OutputBudget.ts +84 -0
- package/packages/core/src/shared/PatchSummary.ts +40 -0
- package/packages/core/src/shared/Paths.ts +80 -0
- package/packages/core/src/shared/PimSettings.ts +128 -0
- package/packages/core/src/shared/PimVersion.ts +93 -0
- package/packages/core/src/shared/Pool.ts +26 -0
- package/packages/core/src/shared/Proc.ts +30 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -0
- package/packages/core/src/shared/SafePath.ts +17 -0
- package/packages/core/src/shared/SearchRender.ts +82 -0
- package/packages/core/src/shared/SendFile.ts +23 -0
- package/packages/core/src/shared/SpillCache.ts +78 -0
- package/packages/core/src/shared/SubagentLogs.ts +102 -0
- package/packages/core/src/shared/Supervisor.ts +460 -0
- package/packages/core/src/shared/Sweeper.ts +32 -0
- package/packages/core/src/shared/Tools.ts +801 -0
- package/packages/core/src/shared/Updater.ts +193 -0
- package/packages/core/src/view/AnsiPainter.ts +227 -0
- package/packages/core/src/view/BodyRenderer.ts +102 -0
- package/packages/core/src/view/DiffBlocks.ts +52 -0
- package/packages/core/src/view/DiffLayout.ts +69 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -0
- package/packages/core/src/view/Painting.ts +182 -0
- package/packages/core/src/view/ViewBlock.ts +124 -0
- package/packages/daemon/src/Daemon.ts +63 -0
- package/packages/daemon/src/DaemonInstall.ts +115 -0
- package/packages/daemon/src/Surfaces.ts +66 -0
- package/packages/daemon/src/TelegramSurface.ts +30 -0
- package/packages/daemon/src/WebSurface.ts +57 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +92 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/Protocol.ts +6 -0
- package/packages/protocol/src/ServerEvent.ts +251 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +265 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +312 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +202 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +601 -0
- package/packages/server/src/StaticClient.ts +95 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +563 -0
- package/packages/server/src/probe.ts +146 -0
- package/packages/telegram/src/Bot.ts +229 -0
- package/packages/telegram/src/Commands.ts +768 -0
- package/packages/telegram/src/Config.ts +148 -0
- package/packages/telegram/src/Markdown.ts +235 -0
- package/packages/telegram/src/Message.ts +152 -0
- package/packages/telegram/src/Renderer.ts +456 -0
- package/packages/telegram/src/SendFileSchema.ts +18 -0
- package/packages/telegram/src/SendFileTool.ts +65 -0
- package/packages/telegram/src/Session.ts +252 -0
- package/packages/telegram/src/SessionRegistry.ts +126 -0
- package/packages/telegram/src/TaskScheduler.ts +296 -0
- package/packages/telegram/src/TaskSchema.ts +88 -0
- package/packages/telegram/src/TaskStore.ts +67 -0
- package/packages/telegram/src/TaskTool.ts +208 -0
- package/packages/telegram/src/TelegramHtml.ts +288 -0
- package/packages/telegram/src/TypingIndicator.ts +43 -0
- package/packages/telegram/src/UpdateConfirm.ts +61 -0
- package/packages/tui/src/extensions/_init/index.ts +144 -0
- package/packages/tui/src/extensions/command-picker/index.ts +43 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +117 -0
- package/packages/tui/src/extensions/footer/powerline.ts +87 -0
- package/packages/tui/src/extensions/footer/segments.ts +202 -0
- package/packages/tui/src/extensions/pim/index.ts +89 -0
- package/packages/tui/src/extensions/session-lease/index.ts +248 -0
- package/packages/tui/src/extensions/tps/index.ts +118 -0
- package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
- package/packages/tui/src/extensions/wrapProvider.ts +18 -0
- package/packages/tui/src/themes/pim-dark.json +84 -0
- package/packages/tui/src/themes/pim-light.json +84 -0
- package/packages/tui/src/themes/themeCliArgs.ts +20 -0
- package/packages/web/dist/client/apple-touch-icon.png +0 -0
- package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
- package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
- package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
- package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
- package/packages/web/dist/client/assets/core-CGdQx9la.js +3 -0
- package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
- package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
- package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
- package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
- package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
- package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
- package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
- package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
- package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
- package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
- package/packages/web/dist/client/assets/index-BMNwleWO.js +40 -0
- package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -0
- package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
- package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
- package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
- package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
- package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
- package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
- package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
- package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
- package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
- package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
- package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
- package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
- package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
- package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
- package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
- package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
- package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
- package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
- package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
- package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
- package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
- package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
- package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
- package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
- package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
- package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
- package/packages/web/dist/client/favicon.svg +1 -0
- package/packages/web/dist/client/icon-192.png +0 -0
- package/packages/web/dist/client/icon-512.png +0 -0
- package/packages/web/dist/client/icon-maskable.png +0 -0
- package/packages/web/dist/client/index.html +24 -0
- package/packages/web/dist/client/manifest.webmanifest +21 -0
- package/packages/web/dist/client/wordmark.svg +1 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { Painting, type PainterMap } from "./Painting";
|
|
3
|
+
import type {
|
|
4
|
+
BlockOf,
|
|
5
|
+
DiffHunk,
|
|
6
|
+
NoticeSeverity,
|
|
7
|
+
Span,
|
|
8
|
+
Tone,
|
|
9
|
+
ToolIcon,
|
|
10
|
+
ToolView,
|
|
11
|
+
ViewBlock,
|
|
12
|
+
} from "./ViewBlock";
|
|
13
|
+
|
|
14
|
+
type Mode = "inline" | "block";
|
|
15
|
+
|
|
16
|
+
/** How a block sits in a body; `embed` payloads must never be re-wrapped. */
|
|
17
|
+
export type MarkdownFrame = "flow" | "embed" | "heading";
|
|
18
|
+
|
|
19
|
+
export type MarkdownGroup = {
|
|
20
|
+
readonly frame: MarkdownFrame;
|
|
21
|
+
readonly lines: readonly string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type PaintedTool = {
|
|
25
|
+
readonly icon: string;
|
|
26
|
+
readonly lines: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const INLINE_LIMIT = 180;
|
|
30
|
+
|
|
31
|
+
const ENTITIES: Readonly<Record<string, string>> = {
|
|
32
|
+
"&": "&",
|
|
33
|
+
"<": "<",
|
|
34
|
+
">": ">",
|
|
35
|
+
'"': """,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function escape(text: string): string {
|
|
39
|
+
return text.replace(/[&<>"]/g, (char) => ENTITIES[char] ?? char);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function paint(blocks: readonly ViewBlock[]): string[] {
|
|
43
|
+
return blocks.flatMap((block) => paintBlock(block, "block"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function paintInline(blocks: readonly ViewBlock[]): string {
|
|
47
|
+
return blocks
|
|
48
|
+
.flatMap((block) => paintBlock(block, "inline"))
|
|
49
|
+
.filter((line) => line !== "")
|
|
50
|
+
.join(" ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function paintBody(blocks: readonly ViewBlock[]): MarkdownGroup[] {
|
|
54
|
+
return Painting.groupByFrame(blocks, Painting.FRAMES).map((group) => ({
|
|
55
|
+
frame: group.frame,
|
|
56
|
+
lines: group.blocks.flatMap((block) => paintBlock(block, "block")),
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function icon(toolIcon: ToolIcon | undefined): string {
|
|
61
|
+
return toolIcon === undefined ? DEFAULT_ICON : ICONS[toolIcon];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function paintTool(view: ToolView): PaintedTool {
|
|
65
|
+
const head = [view.title, view.summary ?? []]
|
|
66
|
+
.map((blocks) => paintInline(blocks))
|
|
67
|
+
.filter((text) => text !== "")
|
|
68
|
+
.join(" ");
|
|
69
|
+
const outline = outlined(view.body ?? []).flatMap((block) =>
|
|
70
|
+
paintBlock(block, "block")
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
icon: icon(view.icon),
|
|
75
|
+
lines: [head, ...outline].filter((line) => line !== ""),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A chat row carries a body's headings and any payload that fits a line; an embedded one needs a screen. */
|
|
80
|
+
function outlined(blocks: readonly ViewBlock[]): readonly ViewBlock[] {
|
|
81
|
+
return blocks.filter((block) => {
|
|
82
|
+
const frame = Painting.FRAMES[block.kind];
|
|
83
|
+
return (
|
|
84
|
+
frame === "heading" ||
|
|
85
|
+
(frame === "flow" && Painting.WEIGHT[block.kind] === "payload")
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ICONS = {
|
|
91
|
+
file: "📄",
|
|
92
|
+
edit: "✏️",
|
|
93
|
+
trash: "🗑️",
|
|
94
|
+
terminal: "⚡️",
|
|
95
|
+
search: "🔎",
|
|
96
|
+
checklist: "📋",
|
|
97
|
+
globe: "🌐",
|
|
98
|
+
upload: "📤",
|
|
99
|
+
clock: "⏰",
|
|
100
|
+
robot: "🤖",
|
|
101
|
+
} as const satisfies Record<ToolIcon, string>;
|
|
102
|
+
|
|
103
|
+
const DEFAULT_ICON = "⚙️";
|
|
104
|
+
|
|
105
|
+
const TONE_WRAPPERS = {
|
|
106
|
+
default: identity,
|
|
107
|
+
muted: identity,
|
|
108
|
+
dim: identity,
|
|
109
|
+
error: bold,
|
|
110
|
+
warning: identity,
|
|
111
|
+
accent: identity,
|
|
112
|
+
added: identity,
|
|
113
|
+
removed: identity,
|
|
114
|
+
title: identity,
|
|
115
|
+
} as const satisfies Record<Tone, (text: string) => string>;
|
|
116
|
+
|
|
117
|
+
const NOTICE_PREFIXES = {
|
|
118
|
+
info: "",
|
|
119
|
+
warn: "⚠️ ",
|
|
120
|
+
error: "❌ ",
|
|
121
|
+
} as const satisfies Record<NoticeSeverity, string>;
|
|
122
|
+
|
|
123
|
+
function identity(text: string): string {
|
|
124
|
+
return text;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function bold(text: string): string {
|
|
128
|
+
return `<b>${text}</b>`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function paintBlock(block: ViewBlock, mode: Mode): readonly string[] {
|
|
132
|
+
return Painting.dispatch(PAINTERS, block, mode);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function oneLine(text: string): string {
|
|
136
|
+
const index = text.indexOf("\n");
|
|
137
|
+
const first = index < 0 ? text : `${text.slice(0, index).trimEnd()} …`;
|
|
138
|
+
return first.length <= INLINE_LIMIT
|
|
139
|
+
? first
|
|
140
|
+
: `${first.slice(0, INLINE_LIMIT - 1)}…`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function escapeIn(text: string, mode: Mode): string {
|
|
144
|
+
return escape(mode === "inline" ? oneLine(text) : text);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function lines(text: string, mode: Mode): readonly string[] {
|
|
148
|
+
return mode === "inline"
|
|
149
|
+
? [escapeIn(text, mode)]
|
|
150
|
+
: text.split("\n").map((line) => escape(line));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function paintText(block: BlockOf<"text">, mode: Mode): readonly string[] {
|
|
154
|
+
const wrap = TONE_WRAPPERS[block.tone ?? "default"];
|
|
155
|
+
return lines(block.text, mode).map(wrap);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function paintSpans(block: BlockOf<"spans">, mode: Mode): readonly string[] {
|
|
159
|
+
return [spansText(block.spans, mode)];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function spansText(spans: readonly Span[], mode: Mode): string {
|
|
163
|
+
return spans
|
|
164
|
+
.filter((span) => span.text !== "")
|
|
165
|
+
.map((span) => {
|
|
166
|
+
let text = escapeIn(span.text, mode);
|
|
167
|
+
if (span.code === true) {
|
|
168
|
+
text = `<code>${text}</code>`;
|
|
169
|
+
}
|
|
170
|
+
if (span.strike === true) {
|
|
171
|
+
text = `<s>${text}</s>`;
|
|
172
|
+
}
|
|
173
|
+
if (span.strong === true) {
|
|
174
|
+
text = bold(text);
|
|
175
|
+
}
|
|
176
|
+
return TONE_WRAPPERS[span.tone ?? "default"](text);
|
|
177
|
+
})
|
|
178
|
+
.join("");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function paintSection(block: BlockOf<"section">): readonly string[] {
|
|
182
|
+
const lead =
|
|
183
|
+
block.icon === undefined ? bold(escape(block.label)) : icon(block.icon);
|
|
184
|
+
const content = paintInline(block.content);
|
|
185
|
+
return [content === "" ? lead : `${lead} ${content}`];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function paintCode(block: BlockOf<"code">, mode: Mode): readonly string[] {
|
|
189
|
+
if (mode === "inline") {
|
|
190
|
+
return [`<code>${escapeIn(block.text, mode)}</code>`];
|
|
191
|
+
}
|
|
192
|
+
const start = block.startLine;
|
|
193
|
+
const body = numbered(block.text.split("\n"), start)
|
|
194
|
+
.map((line) => escape(line))
|
|
195
|
+
.join("\n");
|
|
196
|
+
return [preformatted(body, block.lang)];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function numbered(
|
|
200
|
+
source: readonly string[],
|
|
201
|
+
start: number | undefined
|
|
202
|
+
): readonly string[] {
|
|
203
|
+
if (start === undefined) {
|
|
204
|
+
return source;
|
|
205
|
+
}
|
|
206
|
+
const width = Painting.lineNumberWidth(start, source.length);
|
|
207
|
+
return source.map(
|
|
208
|
+
(line, index) => `${String(start + index).padStart(width)} ${line}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function preformatted(body: string, lang?: string): string {
|
|
213
|
+
return lang === undefined || lang === ""
|
|
214
|
+
? `<pre>${body}</pre>`
|
|
215
|
+
: `<pre><code class="language-${escape(lang)}">${body}</code></pre>`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function paintDiff(block: BlockOf<"diff">, mode: Mode): readonly string[] {
|
|
219
|
+
const body = block.hunks.flatMap((hunk) => diffLines(hunk));
|
|
220
|
+
if (body.length === 0) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
if (mode === "inline") {
|
|
224
|
+
return [`<code>${escapeIn(body[0] ?? "", mode)}</code>`];
|
|
225
|
+
}
|
|
226
|
+
return [preformatted(body.map((line) => escape(line)).join("\n"), "diff")];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function diffLines(hunk: DiffHunk): readonly string[] {
|
|
230
|
+
const header = `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
|
|
231
|
+
return [
|
|
232
|
+
header,
|
|
233
|
+
...hunk.lines.map((line) => `${DIFF_MARKERS[line.kind]}${line.text}`),
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const DIFF_MARKERS = {
|
|
238
|
+
context: " ",
|
|
239
|
+
added: "+",
|
|
240
|
+
removed: "-",
|
|
241
|
+
} as const;
|
|
242
|
+
|
|
243
|
+
function paintFile(block: BlockOf<"file">, mode: Mode): readonly string[] {
|
|
244
|
+
const path = mode === "inline" ? basename(block.path) : block.path;
|
|
245
|
+
const { range, truncated } = Painting.fileSuffix(block);
|
|
246
|
+
return [`<code>${escape(oneLine(path) + range)}</code>${truncated}`];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function paintList(block: BlockOf<"list">, mode: Mode): readonly string[] {
|
|
250
|
+
return Painting.hangingList(block.items, block.ordered === true, (item) =>
|
|
251
|
+
paintBlock(item, mode)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function paintKv(block: BlockOf<"kv">, mode: Mode): readonly string[] {
|
|
256
|
+
return block.pairs.map(
|
|
257
|
+
([key, value]) =>
|
|
258
|
+
`${bold(`${escapeIn(key, mode)}:`)} ${escapeIn(value, mode)}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function paintLink(block: BlockOf<"link">, mode: Mode): readonly string[] {
|
|
263
|
+
const label = Painting.linkLabel(block);
|
|
264
|
+
return [`<a href="${escape(block.href)}">${escapeIn(label, mode)}</a>`];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function paintAttachment(
|
|
268
|
+
block: BlockOf<"attachment">,
|
|
269
|
+
mode: Mode
|
|
270
|
+
): readonly string[] {
|
|
271
|
+
return [escapeIn(block.name, mode)];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function paintImage(block: BlockOf<"image">, mode: Mode): readonly string[] {
|
|
275
|
+
return [escapeIn(Painting.imageSummary(block), mode)];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function paintMarkdown(
|
|
279
|
+
block: BlockOf<"markdown">,
|
|
280
|
+
mode: Mode
|
|
281
|
+
): readonly string[] {
|
|
282
|
+
return lines(block.text, mode);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function paintNotice(block: BlockOf<"notice">, mode: Mode): readonly string[] {
|
|
286
|
+
const prefix = NOTICE_PREFIXES[block.severity];
|
|
287
|
+
return lines(block.text, mode).map((line) => `${prefix}${line}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const PAINTERS: PainterMap<readonly string[], [Mode]> = {
|
|
291
|
+
text: paintText,
|
|
292
|
+
markdown: paintMarkdown,
|
|
293
|
+
spans: paintSpans,
|
|
294
|
+
section: paintSection,
|
|
295
|
+
code: paintCode,
|
|
296
|
+
diff: paintDiff,
|
|
297
|
+
file: paintFile,
|
|
298
|
+
list: paintList,
|
|
299
|
+
kv: paintKv,
|
|
300
|
+
link: paintLink,
|
|
301
|
+
attachment: paintAttachment,
|
|
302
|
+
image: paintImage,
|
|
303
|
+
notice: paintNotice,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/** Paints a `ViewBlock` tree to Telegram-flavoured HTML; the caller joins lines. */
|
|
307
|
+
export const MarkdownPainter = {
|
|
308
|
+
escape,
|
|
309
|
+
paint,
|
|
310
|
+
paintInline,
|
|
311
|
+
paintBody,
|
|
312
|
+
icon,
|
|
313
|
+
paintTool,
|
|
314
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Format } from "../shared/Format";
|
|
2
|
+
import { ImageMime } from "../shared/ImageMime";
|
|
3
|
+
import type { ImageDetails } from "../shared/Images";
|
|
4
|
+
import type { BlockOf, KvPair, ViewBlock } from "./ViewBlock";
|
|
5
|
+
|
|
6
|
+
export type PainterMap<TOut, TArgs extends readonly unknown[] = []> = {
|
|
7
|
+
readonly [TKind in ViewBlock["kind"]]: (
|
|
8
|
+
block: BlockOf<TKind>,
|
|
9
|
+
...args: TArgs
|
|
10
|
+
) => TOut;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function dispatch<TOut, TArgs extends readonly unknown[]>(
|
|
14
|
+
painters: PainterMap<TOut, TArgs>,
|
|
15
|
+
block: ViewBlock,
|
|
16
|
+
...args: TArgs
|
|
17
|
+
): TOut {
|
|
18
|
+
const painter = painters[block.kind] as (
|
|
19
|
+
block: ViewBlock,
|
|
20
|
+
...args: TArgs
|
|
21
|
+
) => TOut;
|
|
22
|
+
return painter(block, ...args);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** How a block sits in its container: `embed` must not be restyled or re-wrapped, `tight` paints its own leading column. */
|
|
26
|
+
export type BlockFrame = "flow" | "embed" | "tight" | "heading";
|
|
27
|
+
|
|
28
|
+
const FRAMES = {
|
|
29
|
+
text: "flow",
|
|
30
|
+
markdown: "embed",
|
|
31
|
+
spans: "flow",
|
|
32
|
+
section: "heading",
|
|
33
|
+
code: "embed",
|
|
34
|
+
diff: "embed",
|
|
35
|
+
file: "flow",
|
|
36
|
+
list: "flow",
|
|
37
|
+
kv: "flow",
|
|
38
|
+
link: "flow",
|
|
39
|
+
attachment: "flow",
|
|
40
|
+
image: "flow",
|
|
41
|
+
notice: "flow",
|
|
42
|
+
} as const satisfies Record<ViewBlock["kind"], BlockFrame>;
|
|
43
|
+
|
|
44
|
+
/** What a block is to the tool that emitted it: its result, or the chrome around one. */
|
|
45
|
+
export type BlockWeight = "payload" | "chrome";
|
|
46
|
+
|
|
47
|
+
const WEIGHT = {
|
|
48
|
+
text: "chrome",
|
|
49
|
+
markdown: "payload",
|
|
50
|
+
spans: "chrome",
|
|
51
|
+
section: "chrome",
|
|
52
|
+
code: "chrome",
|
|
53
|
+
diff: "payload",
|
|
54
|
+
file: "chrome",
|
|
55
|
+
list: "chrome",
|
|
56
|
+
kv: "chrome",
|
|
57
|
+
link: "chrome",
|
|
58
|
+
attachment: "chrome",
|
|
59
|
+
image: "payload",
|
|
60
|
+
notice: "chrome",
|
|
61
|
+
} as const satisfies Record<ViewBlock["kind"], BlockWeight>;
|
|
62
|
+
|
|
63
|
+
export type FrameGroup<TFrame> = {
|
|
64
|
+
readonly frame: TFrame;
|
|
65
|
+
readonly blocks: readonly ViewBlock[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function groupByFrame<TFrame>(
|
|
69
|
+
blocks: readonly ViewBlock[],
|
|
70
|
+
frames: Readonly<Record<ViewBlock["kind"], TFrame>>,
|
|
71
|
+
mergeable: (frame: TFrame) => boolean = () => true
|
|
72
|
+
): FrameGroup<TFrame>[] {
|
|
73
|
+
const groups: Array<{ frame: TFrame; blocks: ViewBlock[] }> = [];
|
|
74
|
+
for (const block of blocks) {
|
|
75
|
+
const frame = frames[block.kind];
|
|
76
|
+
const open = groups.at(-1);
|
|
77
|
+
if (open?.frame === frame && mergeable(frame)) {
|
|
78
|
+
open.blocks.push(block);
|
|
79
|
+
} else {
|
|
80
|
+
groups.push({ frame, blocks: [block] });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return groups;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatRange(range: readonly [number, number | undefined]): string {
|
|
87
|
+
const [start, end] = range;
|
|
88
|
+
return end === undefined ? `:${start}` : `:${start}-${end}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fileSuffix(block: {
|
|
92
|
+
readonly range?: readonly [number, number | undefined];
|
|
93
|
+
readonly truncated?: boolean;
|
|
94
|
+
}): { readonly range: string; readonly truncated: string } {
|
|
95
|
+
return {
|
|
96
|
+
range: block.range ? formatRange(block.range) : "",
|
|
97
|
+
truncated: block.truncated === true ? " (truncated)" : "",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function linkLabel(block: {
|
|
102
|
+
readonly label: string;
|
|
103
|
+
readonly href: string;
|
|
104
|
+
}): string {
|
|
105
|
+
return block.label === "" || block.label === block.href
|
|
106
|
+
? block.href
|
|
107
|
+
: block.label;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** What a painter that cannot draw the picture says instead: `[image 1200×800 png · 240 KB]`. */
|
|
111
|
+
function imageSummary(block: BlockOf<"image">): string {
|
|
112
|
+
return `[image ${block.width}×${block.height} ${ImageMime.extensionOf(block.mimeType)} · ${Format.bytes(block.bytes)}]`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The picture plus what it cannot show about itself; `extra` is whatever only the calling tool knows. */
|
|
116
|
+
function imageBlocks(
|
|
117
|
+
details: ImageDetails,
|
|
118
|
+
alt: string,
|
|
119
|
+
extra: readonly KvPair[] = []
|
|
120
|
+
): readonly ViewBlock[] {
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
kind: "image",
|
|
124
|
+
sha256: details.sha256,
|
|
125
|
+
mimeType: details.mimeType,
|
|
126
|
+
width: details.width,
|
|
127
|
+
height: details.height,
|
|
128
|
+
bytes: details.bytes,
|
|
129
|
+
alt,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
kind: "kv",
|
|
133
|
+
pairs: [
|
|
134
|
+
[
|
|
135
|
+
"dimensions",
|
|
136
|
+
`${details.width}x${details.height}${details.resized ? " (downscaled)" : ""}`,
|
|
137
|
+
],
|
|
138
|
+
...(details.frames >= 2
|
|
139
|
+
? [["frames", `${details.frames} (frame 1 shown)`] as KvPair]
|
|
140
|
+
: []),
|
|
141
|
+
["size", Format.bytes(details.bytes)],
|
|
142
|
+
...extra,
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function lineNumberWidth(start: number, count: number): number {
|
|
149
|
+
return String(start + Math.max(0, count - 1)).length;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hangingList(
|
|
153
|
+
items: readonly ViewBlock[],
|
|
154
|
+
ordered: boolean,
|
|
155
|
+
paintItem: (item: ViewBlock) => readonly string[],
|
|
156
|
+
styleMarker: (marker: string) => string = (marker) => marker
|
|
157
|
+
): string[] {
|
|
158
|
+
const markers = items.map((_, index) => (ordered ? `${index + 1}.` : "•"));
|
|
159
|
+
const width = Math.max(0, ...markers.map((marker) => marker.length)) + 1;
|
|
160
|
+
const indent = " ".repeat(width);
|
|
161
|
+
|
|
162
|
+
return items.flatMap((item, index) => {
|
|
163
|
+
const marker = styleMarker((markers[index] ?? "•").padEnd(width));
|
|
164
|
+
return paintItem(item).map((line, lineIndex) =>
|
|
165
|
+
lineIndex === 0 ? marker + line : indent + line
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const Painting = {
|
|
171
|
+
FRAMES,
|
|
172
|
+
WEIGHT,
|
|
173
|
+
dispatch,
|
|
174
|
+
groupByFrame,
|
|
175
|
+
formatRange,
|
|
176
|
+
fileSuffix,
|
|
177
|
+
linkLabel,
|
|
178
|
+
imageSummary,
|
|
179
|
+
imageBlocks,
|
|
180
|
+
lineNumberWidth,
|
|
181
|
+
hangingList,
|
|
182
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ToolDiffHunk } from "../shared/DiffLines";
|
|
2
|
+
import type { ImageMimeType } from "../shared/Images";
|
|
3
|
+
|
|
4
|
+
export type DiffHunk = ToolDiffHunk;
|
|
5
|
+
|
|
6
|
+
/** Semantic inline styling roles; every painter must map the whole set. */
|
|
7
|
+
export type Tone =
|
|
8
|
+
| "default"
|
|
9
|
+
| "muted"
|
|
10
|
+
| "dim"
|
|
11
|
+
| "error"
|
|
12
|
+
| "warning"
|
|
13
|
+
| "accent"
|
|
14
|
+
| "added"
|
|
15
|
+
| "removed"
|
|
16
|
+
| "title";
|
|
17
|
+
|
|
18
|
+
export type ToolIcon =
|
|
19
|
+
| "file"
|
|
20
|
+
| "edit"
|
|
21
|
+
| "trash"
|
|
22
|
+
| "terminal"
|
|
23
|
+
| "search"
|
|
24
|
+
| "checklist"
|
|
25
|
+
| "globe"
|
|
26
|
+
| "upload"
|
|
27
|
+
| "clock"
|
|
28
|
+
| "robot";
|
|
29
|
+
|
|
30
|
+
export type Span = {
|
|
31
|
+
readonly text: string;
|
|
32
|
+
readonly tone?: Tone;
|
|
33
|
+
readonly strike?: boolean;
|
|
34
|
+
readonly strong?: boolean;
|
|
35
|
+
readonly code?: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type NoticeSeverity = "info" | "warn" | "error";
|
|
39
|
+
|
|
40
|
+
export type KvPair = readonly [string, string];
|
|
41
|
+
|
|
42
|
+
export type ViewBlock =
|
|
43
|
+
| { readonly kind: "text"; readonly text: string; readonly tone?: Tone }
|
|
44
|
+
/** Markdown source; framed as an embed and never re-coloured or re-wrapped. */
|
|
45
|
+
| { readonly kind: "markdown"; readonly text: string }
|
|
46
|
+
/** One line of mixed-tone text, e.g. a `+5`/`-2` diff stat or a rename. */
|
|
47
|
+
| { readonly kind: "spans"; readonly spans: readonly Span[] }
|
|
48
|
+
/** Introduces a sub-item inside a body; painters draw it as a separated heading. */
|
|
49
|
+
| {
|
|
50
|
+
readonly kind: "section";
|
|
51
|
+
readonly label: string;
|
|
52
|
+
readonly icon?: ToolIcon;
|
|
53
|
+
readonly content: readonly ViewBlock[];
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
readonly kind: "code";
|
|
57
|
+
readonly lang: string;
|
|
58
|
+
readonly text: string;
|
|
59
|
+
readonly startLine?: number;
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
readonly kind: "diff";
|
|
63
|
+
readonly path: string;
|
|
64
|
+
readonly hunks: readonly DiffHunk[];
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
readonly kind: "file";
|
|
68
|
+
readonly path: string;
|
|
69
|
+
/** An undefined end is an open-ended range (`:40`), not a missing one. */
|
|
70
|
+
readonly range?: readonly [number, number | undefined];
|
|
71
|
+
readonly truncated?: boolean;
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
readonly kind: "list";
|
|
75
|
+
readonly items: readonly ViewBlock[];
|
|
76
|
+
readonly ordered?: boolean;
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
readonly kind: "kv";
|
|
80
|
+
readonly pairs: readonly KvPair[];
|
|
81
|
+
}
|
|
82
|
+
| { readonly kind: "link"; readonly href: string; readonly label: string }
|
|
83
|
+
/** A file delivered to the reader, not a path on the agent's machine; `url` is server-relative. */
|
|
84
|
+
| {
|
|
85
|
+
readonly kind: "attachment";
|
|
86
|
+
/** What to call it on screen; never a path. */
|
|
87
|
+
readonly name: string;
|
|
88
|
+
readonly url: string;
|
|
89
|
+
readonly isImage: boolean;
|
|
90
|
+
}
|
|
91
|
+
/** A picture the model was shown, addressed by digest; the bytes never ride the wire, and the client that fetches them builds the URL. */
|
|
92
|
+
| {
|
|
93
|
+
readonly kind: "image";
|
|
94
|
+
readonly sha256: string;
|
|
95
|
+
readonly mimeType: ImageMimeType;
|
|
96
|
+
readonly width: number;
|
|
97
|
+
readonly height: number;
|
|
98
|
+
readonly bytes: number;
|
|
99
|
+
readonly alt: string;
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
readonly kind: "notice";
|
|
103
|
+
readonly text: string;
|
|
104
|
+
readonly severity: NoticeSeverity;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type BlockOf<TKind extends ViewBlock["kind"]> = Extract<
|
|
108
|
+
ViewBlock,
|
|
109
|
+
{ kind: TKind }
|
|
110
|
+
>;
|
|
111
|
+
|
|
112
|
+
export type ToolView = {
|
|
113
|
+
/** Display label for the title row, e.g. `"Read"`; defaults to the definition's label. */
|
|
114
|
+
readonly label?: string;
|
|
115
|
+
/** Tints the label; defaults to the title colour. */
|
|
116
|
+
readonly labelTone?: Tone;
|
|
117
|
+
readonly icon?: ToolIcon;
|
|
118
|
+
/** Painted as one line; a lone `markdown` block is handed over unpainted instead. */
|
|
119
|
+
readonly title: readonly ViewBlock[];
|
|
120
|
+
/** Status content rendered in every state: streaming, collapsed and expanded. */
|
|
121
|
+
readonly summary?: readonly ViewBlock[];
|
|
122
|
+
/** Rendered only once the row is expanded, and never while streaming. */
|
|
123
|
+
readonly body?: readonly ViewBlock[];
|
|
124
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { SurfaceName } from "./Surfaces";
|
|
2
|
+
|
|
3
|
+
/** A running surface, and the only thing the daemon asks of one. */
|
|
4
|
+
export type SurfaceHandle = {
|
|
5
|
+
readonly stop: () => Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type Surface = {
|
|
9
|
+
readonly name: SurfaceName;
|
|
10
|
+
/** Brings the surface up, resolving once it is serving rather than when it stops. */
|
|
11
|
+
readonly start: () => Promise<SurfaceHandle>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Every surface in one process, and none of them able to take another down: a
|
|
16
|
+
* bot that cannot reach Telegram leaves the browser served, and a port already
|
|
17
|
+
* bound leaves the bot polling.
|
|
18
|
+
*/
|
|
19
|
+
export class Daemon {
|
|
20
|
+
private readonly surfaces: ReadonlyArray<Surface>;
|
|
21
|
+
private readonly live = new Map<SurfaceName, SurfaceHandle>();
|
|
22
|
+
|
|
23
|
+
public constructor(surfaces: ReadonlyArray<Surface>) {
|
|
24
|
+
this.surfaces = surfaces;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public get running(): ReadonlyArray<SurfaceName> {
|
|
28
|
+
return [...this.live.keys()];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Starts every surface; throws only when none of them came up. */
|
|
32
|
+
public async start(): Promise<ReadonlyArray<SurfaceName>> {
|
|
33
|
+
await Promise.all(
|
|
34
|
+
this.surfaces.map(async (surface) => {
|
|
35
|
+
try {
|
|
36
|
+
this.live.set(surface.name, await surface.start());
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(`[daemon] the ${surface.name} surface failed:`, err);
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
if (this.live.size === 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`no surface started: ${this.surfaces.map((surface) => surface.name).join(", ")}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return this.running;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async stop(): Promise<void> {
|
|
51
|
+
const live = [...this.live];
|
|
52
|
+
this.live.clear();
|
|
53
|
+
await Promise.all(
|
|
54
|
+
live.map(async ([name, handle]) => {
|
|
55
|
+
try {
|
|
56
|
+
await handle.stop();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(`[daemon] the ${name} surface failed to stop:`, err);
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|