pim-agent 0.0.1 → 0.10.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 +54 -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 +177 -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 +36 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +232 -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 +132 -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 +77 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +156 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SearchIndex.ts +752 -0
- package/packages/core/src/session/SearchTokens.ts +97 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionDigest.ts +161 -0
- package/packages/core/src/session/SessionHost.ts +892 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionMeta.ts +267 -0
- package/packages/core/src/session/SessionName.ts +18 -0
- package/packages/core/src/session/SessionRegistry.ts +244 -0
- package/packages/core/src/session/WriteMark.ts +51 -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/DiffPatch.ts +70 -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 +121 -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 +530 -0
- package/packages/core/src/shared/GitMonitor.ts +221 -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 +83 -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 +50 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -0
- package/packages/core/src/shared/RepoDiff.ts +741 -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/Surface.ts +2 -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 +209 -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/DiffExpand.ts +214 -0
- package/packages/core/src/view/DiffLayout.ts +69 -0
- package/packages/core/src/view/DiffPairs.ts +47 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -0
- package/packages/core/src/view/MovePath.ts +70 -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 +56 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +219 -0
- package/packages/protocol/src/Diff.ts +15 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/ServerEvent.ts +406 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +277 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +374 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +456 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +627 -0
- package/packages/server/src/StaticClient.ts +178 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +798 -0
- package/packages/server/src/probe.ts +200 -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 +253 -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 +75 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +93 -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 +253 -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-300-normal-B-iV2FbL.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +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-CA0aSzPu.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-B9be-i9d.css +1 -0
- package/packages/web/dist/client/assets/index-CpinM5NK.js +45 -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,61 @@
|
|
|
1
|
+
import { unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { Fs } from "#core/shared/Fs";
|
|
5
|
+
|
|
6
|
+
const CONFIRM_FILE = "update-confirm.json";
|
|
7
|
+
|
|
8
|
+
/** The "🔄 Updating..." message to edit once the restarted daemon is back. */
|
|
9
|
+
export type UpdateConfirmEntry = {
|
|
10
|
+
readonly chatId: number;
|
|
11
|
+
readonly threadId: number | undefined;
|
|
12
|
+
readonly messageId: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function append(
|
|
16
|
+
configDir: string,
|
|
17
|
+
entry: UpdateConfirmEntry
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const merged = [...(await read(configDir)), entry];
|
|
20
|
+
await Fs.writeAtomic(
|
|
21
|
+
updateConfirmPath(configDir),
|
|
22
|
+
JSON.stringify(merged, null, 2)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function read(
|
|
27
|
+
configDir: string
|
|
28
|
+
): Promise<ReadonlyArray<UpdateConfirmEntry>> {
|
|
29
|
+
const data = await Fs.readJsonOrEmpty<unknown[]>(
|
|
30
|
+
updateConfirmPath(configDir),
|
|
31
|
+
[]
|
|
32
|
+
);
|
|
33
|
+
if (!Array.isArray(data)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return data.filter(
|
|
37
|
+
(e): e is UpdateConfirmEntry =>
|
|
38
|
+
!!e &&
|
|
39
|
+
typeof e === "object" &&
|
|
40
|
+
typeof (e as UpdateConfirmEntry).chatId === "number" &&
|
|
41
|
+
((e as UpdateConfirmEntry).threadId === undefined ||
|
|
42
|
+
typeof (e as UpdateConfirmEntry).threadId === "number") &&
|
|
43
|
+
typeof (e as UpdateConfirmEntry).messageId === "number"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function clear(configDir: string): Promise<void> {
|
|
48
|
+
try {
|
|
49
|
+
await unlink(updateConfirmPath(configDir));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
52
|
+
console.warn(`[update-confirm] unlink failed:`, err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function updateConfirmPath(configDir: string): string {
|
|
58
|
+
return join(configDir, CONFIRM_FILE);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const UpdateConfirm = { append, read, clear };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import { PimVersion } from "#core/shared/PimVersion";
|
|
7
|
+
|
|
8
|
+
const SPLASH_ID = "pim-splash";
|
|
9
|
+
const UPDATE_CHECK_TIMEOUT_MS = 3_000;
|
|
10
|
+
|
|
11
|
+
const shortcuts = [
|
|
12
|
+
["Ctrl+C", "Clear editor (first) / exit (second)"],
|
|
13
|
+
["Escape", "Cancel autocomplete / abort streaming"],
|
|
14
|
+
["/<command>", "Slash commands", "<command>"],
|
|
15
|
+
["/hotkeys", "Show all keyboard shortcuts"],
|
|
16
|
+
["/settings", "Open settings menu"],
|
|
17
|
+
["/pim", "Enable or disable pim extensions"],
|
|
18
|
+
["@<path>", "Attach files", "<path>"],
|
|
19
|
+
["!<command>", "Run bash command", "<command>"],
|
|
20
|
+
["!!<command>", "Run bash command (excluded from context)", "<command>"],
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
24
|
+
if (typeof Bun === "undefined") {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Pim requires the Bun runtime.\n" +
|
|
27
|
+
"To run Pim: bun install -g pim-agent, then run `pim`.\n" +
|
|
28
|
+
"To run vanilla Pi without Pim: `pi -ne`."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const version = await PimVersion.current();
|
|
33
|
+
|
|
34
|
+
const keyCol = Math.max(...shortcuts.map(([k]) => k.length)) + 2;
|
|
35
|
+
|
|
36
|
+
let splashShown = false;
|
|
37
|
+
let update: string | undefined;
|
|
38
|
+
let updateCheck: Promise<string | undefined> | undefined;
|
|
39
|
+
|
|
40
|
+
const checkForUpdate = (): Promise<string | undefined> => {
|
|
41
|
+
updateCheck ??= process.env["PI_OFFLINE"]
|
|
42
|
+
? Promise.resolve(undefined)
|
|
43
|
+
: PimVersion.latest({ timeoutMs: UPDATE_CHECK_TIMEOUT_MS }).then(
|
|
44
|
+
(available) =>
|
|
45
|
+
available !== undefined && PimVersion.isNewer(available, version)
|
|
46
|
+
? available
|
|
47
|
+
: undefined
|
|
48
|
+
);
|
|
49
|
+
return updateCheck;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const showSplash = (ctx: ExtensionContext): void => {
|
|
53
|
+
const theme = ctx.ui.theme;
|
|
54
|
+
const renderKey = (key: string, muted: string | undefined): string => {
|
|
55
|
+
const padding = " ".repeat(Math.max(0, keyCol - key.length));
|
|
56
|
+
if (!muted) {
|
|
57
|
+
return theme.fg("mdCode", key + padding);
|
|
58
|
+
}
|
|
59
|
+
const idx = key.indexOf(muted);
|
|
60
|
+
if (idx === -1) {
|
|
61
|
+
return theme.fg("mdCode", key + padding);
|
|
62
|
+
}
|
|
63
|
+
return (
|
|
64
|
+
theme.fg("mdCode", key.slice(0, idx)) +
|
|
65
|
+
theme.fg("muted", muted) +
|
|
66
|
+
key.slice(idx + muted.length) +
|
|
67
|
+
padding
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const title =
|
|
72
|
+
theme.bold(theme.fg("accent", "PIM - Pi IMproved")) +
|
|
73
|
+
" " +
|
|
74
|
+
theme.italic(theme.fg("muted", `v${version}`));
|
|
75
|
+
ctx.ui.setWidget(SPLASH_ID, [
|
|
76
|
+
title,
|
|
77
|
+
...shortcuts.map(
|
|
78
|
+
([k, d, muted]) => renderKey(k, muted) + theme.fg("dim", d)
|
|
79
|
+
),
|
|
80
|
+
...(update === undefined
|
|
81
|
+
? []
|
|
82
|
+
: [
|
|
83
|
+
"",
|
|
84
|
+
theme.fg("warning", `Update available: v${update}`) +
|
|
85
|
+
theme.fg("dim", " - run ") +
|
|
86
|
+
theme.fg("mdCode", "pim update"),
|
|
87
|
+
]),
|
|
88
|
+
]);
|
|
89
|
+
splashShown = true;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
pi.on("session_start", (event, ctx) => {
|
|
93
|
+
if (event.reason !== "startup" && event.reason !== "new") {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
showSplash(ctx);
|
|
98
|
+
void checkForUpdate().then((available) => {
|
|
99
|
+
if (available === undefined || !splashShown) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
update = available;
|
|
103
|
+
showSplash(ctx);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const clearSplash = (ctx: ExtensionContext) => {
|
|
108
|
+
if (splashShown) {
|
|
109
|
+
ctx.ui.setWidget(SPLASH_ID, undefined);
|
|
110
|
+
splashShown = false;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
pi.on("input", (_event, ctx) => {
|
|
115
|
+
clearSplash(ctx);
|
|
116
|
+
return { action: "continue" };
|
|
117
|
+
});
|
|
118
|
+
pi.on("user_bash", (_event, ctx) => {
|
|
119
|
+
clearSplash(ctx);
|
|
120
|
+
});
|
|
121
|
+
pi.on("model_select", (_event, ctx) => {
|
|
122
|
+
clearSplash(ctx);
|
|
123
|
+
});
|
|
124
|
+
pi.on("thinking_level_select", (_event, ctx) => {
|
|
125
|
+
clearSplash(ctx);
|
|
126
|
+
});
|
|
127
|
+
pi.on("session_before_fork", (_event, ctx) => {
|
|
128
|
+
clearSplash(ctx);
|
|
129
|
+
});
|
|
130
|
+
pi.on("session_before_tree", (_event, ctx) => {
|
|
131
|
+
clearSplash(ctx);
|
|
132
|
+
});
|
|
133
|
+
pi.on("session_before_compact", (_event, ctx) => {
|
|
134
|
+
clearSplash(ctx);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
pi.registerCommand("clear", {
|
|
138
|
+
description: "Start a new session (alias: /new)",
|
|
139
|
+
handler: async (_args, ctx) => {
|
|
140
|
+
await ctx.waitForIdle();
|
|
141
|
+
await ctx.newSession();
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AutocompleteProviderFactory,
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
6
|
+
import { rankCommands } from "#core/picker/commandRanker";
|
|
7
|
+
import { SLASH_PREFIX } from "#core/picker/token";
|
|
8
|
+
import { wrapProvider } from "../wrapProvider";
|
|
9
|
+
|
|
10
|
+
const MAX_VISIBLE_ROWS = 10;
|
|
11
|
+
const SLASH_LINES = ["/"];
|
|
12
|
+
|
|
13
|
+
export function createCommandPickerProviderFactory(): AutocompleteProviderFactory {
|
|
14
|
+
return (current: AutocompleteProvider): AutocompleteProvider => {
|
|
15
|
+
let answered: string | undefined;
|
|
16
|
+
|
|
17
|
+
return wrapProvider(current, {
|
|
18
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
19
|
+
answered = undefined;
|
|
20
|
+
const line = lines[cursorLine] ?? "";
|
|
21
|
+
const beforeCursor = line.slice(0, cursorCol);
|
|
22
|
+
|
|
23
|
+
const slashMatch = beforeCursor.match(SLASH_PREFIX);
|
|
24
|
+
if (!slashMatch) {
|
|
25
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const all = await current.getSuggestions(SLASH_LINES, 0, 1, options);
|
|
29
|
+
if (all === null) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const query = slashMatch[1] ?? "";
|
|
34
|
+
const items = rankCommands(query, all.items, {
|
|
35
|
+
limit: MAX_VISIBLE_ROWS,
|
|
36
|
+
});
|
|
37
|
+
if (items.length === 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
answered = `/${query}`;
|
|
41
|
+
return { items, prefix: answered };
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// Pi completes a slash command only where it opens the line; ours sits anywhere in it.
|
|
45
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
46
|
+
if (prefix !== answered) {
|
|
47
|
+
return current.applyCompletion(
|
|
48
|
+
lines,
|
|
49
|
+
cursorLine,
|
|
50
|
+
cursorCol,
|
|
51
|
+
item,
|
|
52
|
+
prefix
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const line = lines[cursorLine] ?? "";
|
|
56
|
+
const head = line.slice(0, cursorCol - prefix.length);
|
|
57
|
+
const tail = line.slice(cursorCol);
|
|
58
|
+
const inserted = `/${item.value}${/^\s/.test(tail) ? "" : " "}`;
|
|
59
|
+
const next = [...lines];
|
|
60
|
+
next[cursorLine] = `${head}${inserted}${tail}`;
|
|
61
|
+
return {
|
|
62
|
+
lines: next,
|
|
63
|
+
cursorLine,
|
|
64
|
+
cursorCol: head.length + inserted.length,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default function (pi: ExtensionAPI): void {
|
|
72
|
+
pi.on("session_start", (_event, ctx) => {
|
|
73
|
+
ctx.ui.addAutocompleteProvider(createCommandPickerProviderFactory());
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AutocompleteProviderFactory,
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
6
|
+
import type { FilePickerSuggestionEngine } from "#core/picker/FilePickerSuggestionEngine";
|
|
7
|
+
import { AT_PREFIX, isDirectoryItem, sigilOffset } from "#core/picker/token";
|
|
8
|
+
import { WorkerFilePickerSuggestionEngine } from "#core/picker/WorkerFilePickerSuggestionEngine";
|
|
9
|
+
import { wrapProvider } from "../wrapProvider";
|
|
10
|
+
|
|
11
|
+
const MAX_VISIBLE_ROWS = 50;
|
|
12
|
+
|
|
13
|
+
// Pi cancels autocomplete after Tab; re-enter Tab next tick to keep drilling.
|
|
14
|
+
function keepDrilling(): void {
|
|
15
|
+
setTimeout(() => {
|
|
16
|
+
try {
|
|
17
|
+
process.stdin.emit("data", "\t");
|
|
18
|
+
} catch {}
|
|
19
|
+
}, 0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sameActiveAtToken(
|
|
23
|
+
a: ActiveAtToken | undefined,
|
|
24
|
+
b: ActiveAtToken
|
|
25
|
+
): boolean {
|
|
26
|
+
return a?.cursorLine === b.cursorLine && a.atCol === b.atCol;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type FilePickerProviderFactoryOptions = {
|
|
30
|
+
readonly engine: FilePickerSuggestionEngine;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ActiveAtToken = {
|
|
34
|
+
readonly cursorLine: number;
|
|
35
|
+
readonly atCol: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function createFilePickerProviderFactory(
|
|
39
|
+
options: FilePickerProviderFactoryOptions
|
|
40
|
+
): AutocompleteProviderFactory {
|
|
41
|
+
const refreshRelative = (): void => {
|
|
42
|
+
void options.engine.refreshRelative();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (current: AutocompleteProvider): AutocompleteProvider => {
|
|
46
|
+
let activeAtToken: ActiveAtToken | undefined;
|
|
47
|
+
|
|
48
|
+
return wrapProvider(current, {
|
|
49
|
+
async getSuggestions(lines, cursorLine, cursorCol, autocompleteOptions) {
|
|
50
|
+
const line = lines[cursorLine] ?? "";
|
|
51
|
+
const beforeCursor = line.slice(0, cursorCol);
|
|
52
|
+
|
|
53
|
+
const atMatch = beforeCursor.match(AT_PREFIX);
|
|
54
|
+
if (!atMatch) {
|
|
55
|
+
activeAtToken = undefined;
|
|
56
|
+
return current.getSuggestions(
|
|
57
|
+
lines,
|
|
58
|
+
cursorLine,
|
|
59
|
+
cursorCol,
|
|
60
|
+
autocompleteOptions
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const query = atMatch[1] ?? "";
|
|
65
|
+
const atToken = { cursorLine, atCol: sigilOffset(atMatch) };
|
|
66
|
+
if (!sameActiveAtToken(activeAtToken, atToken)) {
|
|
67
|
+
activeAtToken = atToken;
|
|
68
|
+
refreshRelative();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const items = await options.engine
|
|
72
|
+
.rank(query, {
|
|
73
|
+
limit: MAX_VISIBLE_ROWS,
|
|
74
|
+
signal: autocompleteOptions.signal,
|
|
75
|
+
})
|
|
76
|
+
.catch(() => undefined);
|
|
77
|
+
if (items === undefined) {
|
|
78
|
+
return current.getSuggestions(
|
|
79
|
+
lines,
|
|
80
|
+
cursorLine,
|
|
81
|
+
cursorCol,
|
|
82
|
+
autocompleteOptions
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (items.length === 0) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
items: items.map((item) => ({
|
|
90
|
+
...item,
|
|
91
|
+
value: `@${item.value}`,
|
|
92
|
+
})),
|
|
93
|
+
prefix: `@${query}`,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
98
|
+
// Pi appends a trailing space after file completions; apply @ items here instead.
|
|
99
|
+
if (prefix.startsWith("@")) {
|
|
100
|
+
const line = lines[cursorLine] ?? "";
|
|
101
|
+
const beforePrefix = line.slice(0, cursorCol - prefix.length);
|
|
102
|
+
const afterCursor = line.slice(cursorCol);
|
|
103
|
+
const hasTrailingQuote = item.value.endsWith('"');
|
|
104
|
+
const adjustedAfterCursor =
|
|
105
|
+
prefix.startsWith('@"') &&
|
|
106
|
+
hasTrailingQuote &&
|
|
107
|
+
afterCursor.startsWith('"')
|
|
108
|
+
? afterCursor.slice(1)
|
|
109
|
+
: afterCursor;
|
|
110
|
+
|
|
111
|
+
const newLines = [...lines];
|
|
112
|
+
newLines[cursorLine] =
|
|
113
|
+
`${beforePrefix}${item.value}${adjustedAfterCursor}`;
|
|
114
|
+
|
|
115
|
+
const isDirectory = isDirectoryItem(item);
|
|
116
|
+
const cursorOffset =
|
|
117
|
+
isDirectory && hasTrailingQuote
|
|
118
|
+
? item.value.length - 1
|
|
119
|
+
: item.value.length;
|
|
120
|
+
|
|
121
|
+
if (isDirectory) {
|
|
122
|
+
keepDrilling();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
lines: newLines,
|
|
127
|
+
cursorLine,
|
|
128
|
+
cursorCol: beforePrefix.length + cursorOffset,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = current.applyCompletion(
|
|
133
|
+
lines,
|
|
134
|
+
cursorLine,
|
|
135
|
+
cursorCol,
|
|
136
|
+
item,
|
|
137
|
+
prefix
|
|
138
|
+
);
|
|
139
|
+
if (item.value.endsWith("/")) {
|
|
140
|
+
keepDrilling();
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default function (pi: ExtensionAPI): void {
|
|
149
|
+
pi.on("session_start", (_event, ctx) => {
|
|
150
|
+
ctx.ui.addAutocompleteProvider(
|
|
151
|
+
createFilePickerProviderFactory({
|
|
152
|
+
engine: new WorkerFilePickerSuggestionEngine(ctx.cwd),
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type {
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
ReadonlyFooterDataProvider,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
8
|
+
import type { GitState } from "#core/shared/Git";
|
|
9
|
+
import { GitMonitor } from "#core/shared/GitMonitor";
|
|
10
|
+
import { renderFooterLine } from "./segments";
|
|
11
|
+
|
|
12
|
+
let activeGitRefresh: (() => void) | null = null;
|
|
13
|
+
|
|
14
|
+
type FooterTui = Pick<TUI, "requestRender">;
|
|
15
|
+
type FooterData = Pick<ReadonlyFooterDataProvider, "onBranchChange">;
|
|
16
|
+
type FooterWidget = Component & { readonly dispose: () => void };
|
|
17
|
+
|
|
18
|
+
type FooterWidgetDeps = {
|
|
19
|
+
readonly renderFooterLine: (
|
|
20
|
+
width: number,
|
|
21
|
+
ctx: ExtensionContext,
|
|
22
|
+
gitState: GitState,
|
|
23
|
+
cost: number
|
|
24
|
+
) => string;
|
|
25
|
+
readonly getTotalCost: (ctx: ExtensionContext) => number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DEFAULT_FOOTER_WIDGET_DEPS: FooterWidgetDeps = {
|
|
29
|
+
renderFooterLine,
|
|
30
|
+
getTotalCost,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function getTotalCost(ctx: ExtensionContext): number {
|
|
34
|
+
let cost = 0;
|
|
35
|
+
for (const e of ctx.sessionManager.getEntries()) {
|
|
36
|
+
if (e.type === "message" && e.message.role === "assistant") {
|
|
37
|
+
cost += (e.message as AssistantMessage).usage.cost.total;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return cost;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createFooterWidget(
|
|
44
|
+
ctx: ExtensionContext,
|
|
45
|
+
tui: FooterTui,
|
|
46
|
+
footerData: FooterData,
|
|
47
|
+
monitor: GitMonitor,
|
|
48
|
+
deps: FooterWidgetDeps = DEFAULT_FOOTER_WIDGET_DEPS
|
|
49
|
+
): FooterWidget {
|
|
50
|
+
const refresh = (): void => {
|
|
51
|
+
void monitor.refresh(ctx.cwd);
|
|
52
|
+
};
|
|
53
|
+
// The monitor holds the state; a copy here would be a second one to keep.
|
|
54
|
+
const unwatch = monitor.watch(ctx.cwd, () => {
|
|
55
|
+
tui.requestRender();
|
|
56
|
+
});
|
|
57
|
+
const unsubBranch = footerData.onBranchChange(refresh);
|
|
58
|
+
activeGitRefresh = refresh;
|
|
59
|
+
return {
|
|
60
|
+
invalidate(): void {},
|
|
61
|
+
render(width: number): string[] {
|
|
62
|
+
return [
|
|
63
|
+
deps.renderFooterLine(
|
|
64
|
+
width,
|
|
65
|
+
ctx,
|
|
66
|
+
monitor.stateOf(ctx.cwd),
|
|
67
|
+
deps.getTotalCost(ctx)
|
|
68
|
+
),
|
|
69
|
+
];
|
|
70
|
+
},
|
|
71
|
+
dispose(): void {
|
|
72
|
+
unsubBranch();
|
|
73
|
+
unwatch();
|
|
74
|
+
activeGitRefresh = null;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function (pi: ExtensionAPI): void {
|
|
80
|
+
const monitor = new GitMonitor();
|
|
81
|
+
pi.on("session_start", (_event, ctx) => {
|
|
82
|
+
if (!ctx.hasUI) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
ctx.ui.setFooter((tui, _theme, footerData) =>
|
|
86
|
+
createFooterWidget(ctx, tui, footerData, monitor)
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
pi.on("tool_execution_end", () => {
|
|
91
|
+
activeGitRefresh?.();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export const SEP_RIGHT = "";
|
|
4
|
+
export const SEP_LEFT = "";
|
|
5
|
+
export const SEP_THIN_LEFT = "";
|
|
6
|
+
|
|
7
|
+
export const GIT_ICON = "";
|
|
8
|
+
export const GIT_DIRTY_ICON = "";
|
|
9
|
+
export const GIT_AHEAD_ICON = "";
|
|
10
|
+
export const GIT_BEHIND_ICON = "";
|
|
11
|
+
|
|
12
|
+
export const RESET = "\x1b[0m";
|
|
13
|
+
export const BG_DEFAULT = "\x1b[49m";
|
|
14
|
+
export const REVERSE = "\x1b[7m";
|
|
15
|
+
export const RESET_REVERSE = "\x1b[27m";
|
|
16
|
+
|
|
17
|
+
export const FG_BLACK = "\x1b[30m";
|
|
18
|
+
export const FG_WHITE = "\x1b[97m";
|
|
19
|
+
export const BG_GRAY = "\x1b[100m";
|
|
20
|
+
export const BG_BRIGHT_RED = "\x1b[101m";
|
|
21
|
+
export const BG_BRIGHT_GREEN = "\x1b[102m";
|
|
22
|
+
export const BG_BRIGHT_YELLOW = "\x1b[103m";
|
|
23
|
+
export const BG_BRIGHT_MAGENTA = "\x1b[105m";
|
|
24
|
+
|
|
25
|
+
export const BG_TO_FG: Record<string, string> = {
|
|
26
|
+
[BG_GRAY]: "\x1b[90m",
|
|
27
|
+
[BG_BRIGHT_RED]: "\x1b[91m",
|
|
28
|
+
[BG_BRIGHT_GREEN]: "\x1b[92m",
|
|
29
|
+
[BG_BRIGHT_YELLOW]: "\x1b[93m",
|
|
30
|
+
[BG_BRIGHT_MAGENTA]: "\x1b[95m",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type Segment = {
|
|
34
|
+
readonly text: string;
|
|
35
|
+
readonly fg: string;
|
|
36
|
+
readonly bg: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function paint(seg: Segment): string {
|
|
40
|
+
return `${seg.bg}${seg.fg} ${seg.text} ${RESET}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function chevronRight(prev: Segment, next: Segment | null): string {
|
|
44
|
+
const fg = BG_TO_FG[prev.bg]!;
|
|
45
|
+
if (next === null) {
|
|
46
|
+
return `${fg}${SEP_RIGHT}${RESET}`;
|
|
47
|
+
}
|
|
48
|
+
return `${fg}${next.bg}${SEP_RIGHT}${RESET}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function chevronLeft(prev: Segment | null, next: Segment): string {
|
|
52
|
+
const fg = BG_TO_FG[next.bg]!;
|
|
53
|
+
if (prev === null) {
|
|
54
|
+
return `${fg}${SEP_LEFT}${RESET}`;
|
|
55
|
+
}
|
|
56
|
+
return `${fg}${prev.bg}${SEP_LEFT}${RESET}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function thinChevronLeft(bg: string, fg: string): string {
|
|
60
|
+
return `${BG_DEFAULT}${BG_TO_FG[bg]}${REVERSE}${SEP_THIN_LEFT}${RESET_REVERSE}${bg}${fg}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function renderLeftGroup(segs: readonly Segment[]): string {
|
|
64
|
+
let out = "";
|
|
65
|
+
for (let i = 0; i < segs.length; i++) {
|
|
66
|
+
out += paint(segs[i]!);
|
|
67
|
+
out += chevronRight(segs[i]!, segs[i + 1] ?? null);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function renderRightGroup(segs: readonly Segment[]): string {
|
|
73
|
+
let out = "";
|
|
74
|
+
for (let i = 0; i < segs.length; i++) {
|
|
75
|
+
out += chevronLeft(segs[i - 1] ?? null, segs[i]!);
|
|
76
|
+
out += paint(segs[i]!);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function groupWidth(segs: readonly Segment[]): number {
|
|
82
|
+
let w = 0;
|
|
83
|
+
for (const s of segs) {
|
|
84
|
+
w += visibleWidth(s.text) + 3;
|
|
85
|
+
}
|
|
86
|
+
return w;
|
|
87
|
+
}
|