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,193 @@
|
|
|
1
|
+
import { rename, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { Git } from "./Git";
|
|
5
|
+
import { PimVersion } from "./PimVersion";
|
|
6
|
+
import { Proc } from "./Proc";
|
|
7
|
+
import { Supervisor, type Install } from "./Supervisor";
|
|
8
|
+
|
|
9
|
+
const STAGING = "staging";
|
|
10
|
+
|
|
11
|
+
// Relative to `packages/web`: the vite root `web:build` cds into.
|
|
12
|
+
const STAGING_OUT_DIR = `dist/${STAGING}`;
|
|
13
|
+
|
|
14
|
+
type ClientDirs = {
|
|
15
|
+
readonly staging: string;
|
|
16
|
+
readonly client: string;
|
|
17
|
+
readonly previous: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** A spawned command, or filesystem work — never both. */
|
|
21
|
+
export type UpdateStep =
|
|
22
|
+
| {
|
|
23
|
+
readonly label: string;
|
|
24
|
+
readonly command: ReadonlyArray<string>;
|
|
25
|
+
readonly cwd: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
| { readonly label: string; readonly act: () => Promise<void> };
|
|
28
|
+
|
|
29
|
+
/** Work the plan declined to do; `blocking` means the run did less than asked. */
|
|
30
|
+
export type UpdateSkip = {
|
|
31
|
+
readonly label: string;
|
|
32
|
+
readonly reason: string;
|
|
33
|
+
readonly blocking: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type UpdatePlan = {
|
|
37
|
+
readonly steps: ReadonlyArray<UpdateStep>;
|
|
38
|
+
readonly skipped: ReadonlyArray<UpdateSkip>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type UpdateFacts = {
|
|
42
|
+
readonly at: Install;
|
|
43
|
+
readonly packageName: string;
|
|
44
|
+
/** A dev checkout with no uncommitted changes; irrelevant to a prod install. */
|
|
45
|
+
readonly cleanTree: boolean;
|
|
46
|
+
/** The newest published release, or undefined when the registry was silent. */
|
|
47
|
+
readonly latest: string | undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type UpdateOutcome = {
|
|
51
|
+
readonly ok: boolean;
|
|
52
|
+
readonly from: string;
|
|
53
|
+
/** Re-read from disk after the steps ran, never the version that was asked for. */
|
|
54
|
+
readonly to: string;
|
|
55
|
+
readonly skipped: ReadonlyArray<UpdateSkip>;
|
|
56
|
+
readonly error: string | undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type UpdateOptions = {
|
|
60
|
+
readonly onStep?: (label: string) => void | Promise<void>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function clientDirs(packageRoot: string): ClientDirs {
|
|
64
|
+
const dist = join(packageRoot, "packages", "web", "dist");
|
|
65
|
+
return {
|
|
66
|
+
staging: join(dist, STAGING),
|
|
67
|
+
client: join(dist, "client"),
|
|
68
|
+
previous: join(dist, "previous"),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// `vite build` empties its outDir: build to staging and swap, or a failed build deletes the live bundle.
|
|
73
|
+
async function swapClient(packageRoot: string): Promise<void> {
|
|
74
|
+
const dirs = clientDirs(packageRoot);
|
|
75
|
+
await rm(dirs.previous, { recursive: true, force: true });
|
|
76
|
+
await rename(dirs.client, dirs.previous).catch((err: unknown) => {
|
|
77
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
await rename(dirs.staging, dirs.client);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function plan(facts: UpdateFacts): UpdatePlan {
|
|
85
|
+
const steps: UpdateStep[] = [];
|
|
86
|
+
const skipped: UpdateSkip[] = [];
|
|
87
|
+
const { at, packageName, cleanTree, latest } = facts;
|
|
88
|
+
|
|
89
|
+
if (at.kind === "dev") {
|
|
90
|
+
if (cleanTree) {
|
|
91
|
+
steps.push({
|
|
92
|
+
label: "git pull",
|
|
93
|
+
command: ["git", "pull", "--ff-only"],
|
|
94
|
+
cwd: at.packageRoot,
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
skipped.push({
|
|
98
|
+
label: "git pull",
|
|
99
|
+
reason: "the working tree has uncommitted changes",
|
|
100
|
+
blocking: false,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
steps.push({
|
|
104
|
+
label: "bun install",
|
|
105
|
+
command: ["bun", "install"],
|
|
106
|
+
cwd: at.packageRoot,
|
|
107
|
+
});
|
|
108
|
+
steps.push({
|
|
109
|
+
label: "build the web client",
|
|
110
|
+
command: ["bun", "run", "web:build", "--", "--outDir", STAGING_OUT_DIR],
|
|
111
|
+
cwd: at.packageRoot,
|
|
112
|
+
});
|
|
113
|
+
steps.push({
|
|
114
|
+
label: "swap in the new web client",
|
|
115
|
+
act: () => swapClient(at.packageRoot),
|
|
116
|
+
});
|
|
117
|
+
return { steps, skipped };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (latest === undefined) {
|
|
121
|
+
skipped.push({
|
|
122
|
+
label: "install",
|
|
123
|
+
reason: "the npm registry could not be reached",
|
|
124
|
+
blocking: true,
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
// Install the exact version, never `@latest`: a tag cannot be reported truthfully.
|
|
128
|
+
steps.push({
|
|
129
|
+
label: `install ${packageName}@${latest}`,
|
|
130
|
+
command: ["bun", "install", "-g", `${packageName}@${latest}`],
|
|
131
|
+
// No cwd: a global install must not adopt the tree it is replacing.
|
|
132
|
+
cwd: undefined,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return { steps, skipped };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function gather(): Promise<UpdateFacts> {
|
|
139
|
+
const at = await Supervisor.detectInstall();
|
|
140
|
+
const [packageName, cleanTree, latest] = await Promise.all([
|
|
141
|
+
PimVersion.name(),
|
|
142
|
+
at.kind === "dev"
|
|
143
|
+
? Git.fetchStatus(at.packageRoot).then((git) => git.dirtyCount === 0)
|
|
144
|
+
: Promise.resolve(true),
|
|
145
|
+
at.kind === "prod" ? PimVersion.latest() : Promise.resolve(undefined),
|
|
146
|
+
]);
|
|
147
|
+
return { at, packageName, cleanTree, latest };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Never restarts or exits: the caller owns that. */
|
|
151
|
+
async function run(options: UpdateOptions = {}): Promise<UpdateOutcome> {
|
|
152
|
+
const from = await PimVersion.current();
|
|
153
|
+
const { steps, skipped } = plan(await gather());
|
|
154
|
+
for (const step of steps) {
|
|
155
|
+
await options.onStep?.(step.label);
|
|
156
|
+
try {
|
|
157
|
+
if ("command" in step) {
|
|
158
|
+
await runOrThrow(step.command, step.cwd);
|
|
159
|
+
} else {
|
|
160
|
+
await step.act();
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
from,
|
|
166
|
+
to: await PimVersion.current(),
|
|
167
|
+
skipped,
|
|
168
|
+
error: `${step.label}: ${(err as Error).message}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
from,
|
|
175
|
+
to: await PimVersion.current(),
|
|
176
|
+
skipped,
|
|
177
|
+
error: undefined,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function runOrThrow(
|
|
182
|
+
cmd: ReadonlyArray<string>,
|
|
183
|
+
cwd: string | undefined
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
const { code, stderr } = await Proc.run(cmd, { cwd, stdout: "inherit" });
|
|
186
|
+
if (code !== 0) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`exit ${code}: ${stderr.trim().split("\n").slice(-5).join("\n") || "(no stderr)"}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const Updater = { plan, run };
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { DiffRenderer } from "../shared/DiffRenderer";
|
|
3
|
+
import { Renderer } from "../shared/Renderer";
|
|
4
|
+
import { type BlockFrame, Painting, type PainterMap } from "./Painting";
|
|
5
|
+
import type {
|
|
6
|
+
BlockOf,
|
|
7
|
+
NoticeSeverity,
|
|
8
|
+
Span,
|
|
9
|
+
Tone,
|
|
10
|
+
ViewBlock,
|
|
11
|
+
} from "./ViewBlock";
|
|
12
|
+
|
|
13
|
+
export type { BlockFrame } from "./Painting";
|
|
14
|
+
|
|
15
|
+
export type PaintedGroup =
|
|
16
|
+
| {
|
|
17
|
+
readonly frame: Exclude<BlockFrame, "embed">;
|
|
18
|
+
readonly lines: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
| { readonly frame: "embed"; readonly markdown: string };
|
|
21
|
+
|
|
22
|
+
function paint(blocks: readonly ViewBlock[], theme: Theme): string[] {
|
|
23
|
+
return blocks.flatMap((block) => paintBlock(block, theme));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function paintTitle(
|
|
27
|
+
blocks: readonly ViewBlock[],
|
|
28
|
+
theme: Theme
|
|
29
|
+
): { readonly text: string; readonly markdown: boolean } {
|
|
30
|
+
const only = blocks.length === 1 ? blocks[0] : undefined;
|
|
31
|
+
if (only?.kind === "markdown") {
|
|
32
|
+
return { text: only.text, markdown: true };
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
text: paint(blocks, theme).join(" "),
|
|
36
|
+
markdown: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function themeColorFor(tone: Tone | undefined): ThemeColor | undefined {
|
|
41
|
+
return tone === undefined || tone === "default"
|
|
42
|
+
? undefined
|
|
43
|
+
: TONE_COLORS[tone];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function paintBody(blocks: readonly ViewBlock[], theme: Theme): PaintedGroup[] {
|
|
47
|
+
return Painting.groupByFrame(
|
|
48
|
+
blocks,
|
|
49
|
+
FRAMES,
|
|
50
|
+
(frame) => frame !== "embed"
|
|
51
|
+
).map((group): PaintedGroup => {
|
|
52
|
+
const [first] = group.blocks;
|
|
53
|
+
if (first?.kind === "markdown") {
|
|
54
|
+
return { frame: "embed", markdown: first.text };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
frame: group.frame as Exclude<BlockFrame, "embed">,
|
|
58
|
+
lines: group.blocks.flatMap((block) => paintBlock(block, theme)),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const TONE_COLORS = {
|
|
64
|
+
muted: "muted",
|
|
65
|
+
dim: "dim",
|
|
66
|
+
error: "error",
|
|
67
|
+
warning: "warning",
|
|
68
|
+
accent: "accent",
|
|
69
|
+
added: "toolDiffAdded",
|
|
70
|
+
removed: "toolDiffRemoved",
|
|
71
|
+
title: "toolTitle",
|
|
72
|
+
} as const satisfies Record<Exclude<Tone, "default">, ThemeColor>;
|
|
73
|
+
|
|
74
|
+
const NOTICE_COLORS = {
|
|
75
|
+
info: "muted",
|
|
76
|
+
warn: "warning",
|
|
77
|
+
error: "error",
|
|
78
|
+
} as const satisfies Record<NoticeSeverity, ThemeColor>;
|
|
79
|
+
|
|
80
|
+
function paintBlock(block: ViewBlock, theme: Theme): readonly string[] {
|
|
81
|
+
return Painting.dispatch(PAINTERS, block, theme);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function paintText(block: BlockOf<"text">, theme: Theme): readonly string[] {
|
|
85
|
+
const lines = block.text.split("\n");
|
|
86
|
+
const tone = block.tone ?? "default";
|
|
87
|
+
|
|
88
|
+
if (tone === "default") {
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return lines.map((line) => theme.fg(TONE_COLORS[tone], line));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function spansText(spans: readonly Span[], theme: Theme): string {
|
|
96
|
+
return spans
|
|
97
|
+
.filter((span) => span.text !== "")
|
|
98
|
+
.map((span) => {
|
|
99
|
+
let text =
|
|
100
|
+
span.strike === true ? theme.strikethrough(span.text) : span.text;
|
|
101
|
+
if (span.strong === true) {
|
|
102
|
+
text = theme.bold(text);
|
|
103
|
+
}
|
|
104
|
+
const tone = span.tone ?? "default";
|
|
105
|
+
return tone === "default" ? text : theme.fg(TONE_COLORS[tone], text);
|
|
106
|
+
})
|
|
107
|
+
.join("");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function paintSpans(block: BlockOf<"spans">, theme: Theme): readonly string[] {
|
|
111
|
+
return [spansText(block.spans, theme)];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function paintSection(
|
|
115
|
+
block: BlockOf<"section">,
|
|
116
|
+
theme: Theme
|
|
117
|
+
): readonly string[] {
|
|
118
|
+
return [
|
|
119
|
+
"",
|
|
120
|
+
Renderer.toolTitleText({
|
|
121
|
+
label: block.label,
|
|
122
|
+
title: paint(block.content, theme).join(" "),
|
|
123
|
+
theme,
|
|
124
|
+
markerColor: "success",
|
|
125
|
+
}),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function paintCode(block: BlockOf<"code">, theme: Theme): readonly string[] {
|
|
130
|
+
const lines = block.text.split("\n");
|
|
131
|
+
const start = block.startLine;
|
|
132
|
+
|
|
133
|
+
if (start === undefined) {
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const width = Painting.lineNumberWidth(start, lines.length);
|
|
138
|
+
return lines.map(
|
|
139
|
+
(line, index) =>
|
|
140
|
+
theme.fg("muted", `${String(start + index).padStart(width)} `) + line
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function paintDiff(block: BlockOf<"diff">, theme: Theme): readonly string[] {
|
|
145
|
+
return DiffRenderer.renderLines({
|
|
146
|
+
toolDiff: { path: block.path, hunks: block.hunks },
|
|
147
|
+
theme,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function paintFile(block: BlockOf<"file">, theme: Theme): readonly string[] {
|
|
152
|
+
const { range, truncated } = Painting.fileSuffix(block);
|
|
153
|
+
const suffix = `${range}${truncated}`;
|
|
154
|
+
return [suffix === "" ? block.path : block.path + theme.fg("muted", suffix)];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function paintList(block: BlockOf<"list">, theme: Theme): readonly string[] {
|
|
158
|
+
return Painting.hangingList(
|
|
159
|
+
block.items,
|
|
160
|
+
block.ordered === true,
|
|
161
|
+
(item) => paintBlock(item, theme),
|
|
162
|
+
(marker) => theme.fg("muted", marker)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function paintKv(block: BlockOf<"kv">, theme: Theme): readonly string[] {
|
|
167
|
+
const width = Math.max(0, ...block.pairs.map(([key]) => key.length + 1));
|
|
168
|
+
return block.pairs.map(
|
|
169
|
+
([key, value]) => theme.fg("muted", `${key}:`.padEnd(width + 1)) + value
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function paintLink(block: BlockOf<"link">, theme: Theme): readonly string[] {
|
|
174
|
+
const label = Painting.linkLabel(block);
|
|
175
|
+
if (label === block.href) {
|
|
176
|
+
return [theme.fg("mdLinkUrl", block.href)];
|
|
177
|
+
}
|
|
178
|
+
return [`${theme.fg("mdLink", label)} ${theme.fg("mdLinkUrl", block.href)}`];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function paintAttachment(
|
|
182
|
+
block: BlockOf<"attachment">,
|
|
183
|
+
theme: Theme
|
|
184
|
+
): readonly string[] {
|
|
185
|
+
return [`${theme.fg("muted", "sent")} ${block.name}`];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function paintImage(block: BlockOf<"image">, theme: Theme): readonly string[] {
|
|
189
|
+
return [theme.fg("muted", Painting.imageSummary(block))];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function paintMarkdown(block: BlockOf<"markdown">): readonly string[] {
|
|
193
|
+
return block.text.split("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function paintNotice(
|
|
197
|
+
block: BlockOf<"notice">,
|
|
198
|
+
theme: Theme
|
|
199
|
+
): readonly string[] {
|
|
200
|
+
const color = NOTICE_COLORS[block.severity];
|
|
201
|
+
return block.text.split("\n").map((line) => theme.fg(color, line));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const PAINTERS: PainterMap<readonly string[], [Theme]> = {
|
|
205
|
+
text: paintText,
|
|
206
|
+
markdown: paintMarkdown,
|
|
207
|
+
spans: paintSpans,
|
|
208
|
+
section: paintSection,
|
|
209
|
+
code: paintCode,
|
|
210
|
+
diff: paintDiff,
|
|
211
|
+
file: paintFile,
|
|
212
|
+
list: paintList,
|
|
213
|
+
kv: paintKv,
|
|
214
|
+
link: paintLink,
|
|
215
|
+
attachment: paintAttachment,
|
|
216
|
+
image: paintImage,
|
|
217
|
+
notice: paintNotice,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const FRAMES = {
|
|
221
|
+
...Painting.FRAMES,
|
|
222
|
+
code: "flow",
|
|
223
|
+
diff: "tight",
|
|
224
|
+
} as const satisfies Record<ViewBlock["kind"], BlockFrame>;
|
|
225
|
+
|
|
226
|
+
/** Paints a `ViewBlock` tree to ANSI lines. */
|
|
227
|
+
export const AnsiPainter = { paint, paintTitle, themeColorFor, paintBody };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Theme,
|
|
3
|
+
ThemeColor,
|
|
4
|
+
ToolRenderResultOptions,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { type Component, Container } from "@earendil-works/pi-tui";
|
|
7
|
+
import {
|
|
8
|
+
type PrefixSpec,
|
|
9
|
+
type RenderContext,
|
|
10
|
+
Renderer,
|
|
11
|
+
} from "../shared/Renderer";
|
|
12
|
+
import { AnsiPainter, type BlockFrame, type PaintedGroup } from "./AnsiPainter";
|
|
13
|
+
import type { ViewBlock } from "./ViewBlock";
|
|
14
|
+
|
|
15
|
+
type FrameBuilder = (group: PaintedGroup, theme: Theme) => Component[];
|
|
16
|
+
|
|
17
|
+
type FrameBuilders = Record<BlockFrame, FrameBuilder>;
|
|
18
|
+
|
|
19
|
+
function render(args: {
|
|
20
|
+
readonly summary?: readonly ViewBlock[];
|
|
21
|
+
readonly body?: readonly ViewBlock[];
|
|
22
|
+
readonly options: ToolRenderResultOptions;
|
|
23
|
+
readonly theme: Theme;
|
|
24
|
+
readonly context: RenderContext;
|
|
25
|
+
}): Container {
|
|
26
|
+
const { summary, body, options, theme, context } = args;
|
|
27
|
+
const container =
|
|
28
|
+
(context.lastComponent as Container | undefined) ?? new Container();
|
|
29
|
+
container.clear();
|
|
30
|
+
|
|
31
|
+
let drew = draw(container, summary ?? [], SUMMARY_BUILDERS, theme);
|
|
32
|
+
if (!options.isPartial && options.expanded) {
|
|
33
|
+
drew = draw(container, body ?? [], FRAME_BUILDERS, theme) || drew;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (drew) {
|
|
37
|
+
container.invalidate();
|
|
38
|
+
}
|
|
39
|
+
return container;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function draw(
|
|
43
|
+
container: Container,
|
|
44
|
+
blocks: readonly ViewBlock[],
|
|
45
|
+
builders: FrameBuilders,
|
|
46
|
+
theme: Theme
|
|
47
|
+
): boolean {
|
|
48
|
+
let drew = false;
|
|
49
|
+
for (const group of AnsiPainter.paintBody(blocks, theme)) {
|
|
50
|
+
for (const child of builders[group.frame](group, theme)) {
|
|
51
|
+
container.addChild(child);
|
|
52
|
+
drew = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return drew;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function gutter(prefix: PrefixSpec, lineColor?: ThemeColor): FrameBuilder {
|
|
59
|
+
return (group, theme) => {
|
|
60
|
+
if (group.frame === "embed") {
|
|
61
|
+
return [
|
|
62
|
+
Renderer.makeMarkdownBlock({
|
|
63
|
+
text: group.markdown,
|
|
64
|
+
theme,
|
|
65
|
+
prefix,
|
|
66
|
+
lineColor,
|
|
67
|
+
}),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
const lines = group.lines;
|
|
71
|
+
return lines.length === 0 || (lines.length === 1 && lines[0] === "")
|
|
72
|
+
? []
|
|
73
|
+
: [Renderer.makePrefixedBlock({ lines, theme, prefix, lineColor })];
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function blankLine(): Component {
|
|
78
|
+
return { render: () => [""], invalidate() {} };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function headings(group: PaintedGroup, theme: Theme): Component[] {
|
|
82
|
+
const lines =
|
|
83
|
+
group.frame === "embed" ? group.markdown.split("\n") : group.lines;
|
|
84
|
+
return lines.map((line) =>
|
|
85
|
+
line === "" ? blankLine() : Renderer.makeTitleBlock({ text: line, theme })
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const FRAME_BUILDERS: FrameBuilders = {
|
|
90
|
+
flow: gutter(Renderer.GAPPED_PREFIX, "toolOutput"),
|
|
91
|
+
embed: gutter(Renderer.GAPPED_PREFIX),
|
|
92
|
+
tight: gutter(Renderer.TIGHT_PREFIX),
|
|
93
|
+
heading: headings,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const SUMMARY_BUILDERS: FrameBuilders = {
|
|
97
|
+
...FRAME_BUILDERS,
|
|
98
|
+
flow: gutter(Renderer.GAPPED_PREFIX),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Turns painted body lines into the pi-tui components that frame them. */
|
|
102
|
+
export const BodyRenderer = { render };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ToolDiff } from "../shared/DiffLines";
|
|
2
|
+
import { DiffView } from "../shared/DiffView";
|
|
3
|
+
import { Paths } from "../shared/Paths";
|
|
4
|
+
import type { Span, ToolView, ViewBlock } from "./ViewBlock";
|
|
5
|
+
|
|
6
|
+
function statSpans(diff: ToolDiff | undefined): readonly Span[] {
|
|
7
|
+
const { added, removed } = DiffView.countStats(diff);
|
|
8
|
+
const spans: Span[] = [];
|
|
9
|
+
|
|
10
|
+
if (added > 0) {
|
|
11
|
+
spans.push({ text: `+${added}`, tone: "added" });
|
|
12
|
+
}
|
|
13
|
+
if (removed > 0) {
|
|
14
|
+
if (spans.length > 0) {
|
|
15
|
+
spans.push({ text: "/" });
|
|
16
|
+
}
|
|
17
|
+
spans.push({ text: `-${removed}`, tone: "removed" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return spans;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stats(diff: ToolDiff | undefined): readonly ViewBlock[] {
|
|
24
|
+
const spans = statSpans(diff);
|
|
25
|
+
return spans.length === 0 ? [] : [{ kind: "spans", spans }];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function body(diff: ToolDiff | undefined): readonly ViewBlock[] {
|
|
29
|
+
return diff === undefined
|
|
30
|
+
? []
|
|
31
|
+
: [{ kind: "diff", path: diff.path, hunks: diff.hunks }];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fileView(args: {
|
|
35
|
+
readonly label: string;
|
|
36
|
+
readonly path: string | undefined;
|
|
37
|
+
readonly cwd: string;
|
|
38
|
+
readonly diff: ToolDiff | undefined;
|
|
39
|
+
}): ToolView {
|
|
40
|
+
return {
|
|
41
|
+
label: args.label,
|
|
42
|
+
icon: "edit",
|
|
43
|
+
title: [
|
|
44
|
+
{ kind: "file", path: Paths.titleOr(args.path, args.cwd) },
|
|
45
|
+
...stats(args.diff),
|
|
46
|
+
],
|
|
47
|
+
body: body(args.diff),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** View-model fragments shared by every tool that reports a file diff. */
|
|
52
|
+
export const DiffBlocks = { statSpans, stats, body, fileView };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ToolDiffHunk } from "../shared/DiffLines";
|
|
2
|
+
|
|
3
|
+
const TAB = " ";
|
|
4
|
+
|
|
5
|
+
function detab(text: string): string {
|
|
6
|
+
return text.replace(/\t/g, TAB);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Highlight each side as one block, never line by line, or multi-line strings and comments mis-tokenise.
|
|
10
|
+
function mapSides<T>(
|
|
11
|
+
hunk: ToolDiffHunk,
|
|
12
|
+
highlight: (block: string) => readonly T[]
|
|
13
|
+
): readonly (T | undefined)[] {
|
|
14
|
+
const oldIndices: (number | undefined)[] = [];
|
|
15
|
+
const newIndices: (number | undefined)[] = [];
|
|
16
|
+
const oldBlock: string[] = [];
|
|
17
|
+
const newBlock: string[] = [];
|
|
18
|
+
|
|
19
|
+
for (const line of hunk.lines) {
|
|
20
|
+
// A context line must hold a slot in both blocks to keep tokeniser state aligned.
|
|
21
|
+
if (line.kind !== "added") {
|
|
22
|
+
oldIndices.push(oldBlock.length);
|
|
23
|
+
oldBlock.push(line.text);
|
|
24
|
+
} else {
|
|
25
|
+
oldIndices.push(undefined);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (line.kind !== "removed") {
|
|
29
|
+
newIndices.push(newBlock.length);
|
|
30
|
+
newBlock.push(line.text);
|
|
31
|
+
} else {
|
|
32
|
+
newIndices.push(undefined);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const oldLines = oldBlock.length === 0 ? [] : highlight(oldBlock.join("\n"));
|
|
37
|
+
const newLines = newBlock.length === 0 ? [] : highlight(newBlock.join("\n"));
|
|
38
|
+
|
|
39
|
+
return hunk.lines.map((line, index) => {
|
|
40
|
+
const side = line.kind === "removed" ? oldLines : newLines;
|
|
41
|
+
const at = line.kind === "removed" ? oldIndices[index] : newIndices[index];
|
|
42
|
+
return at === undefined ? undefined : side[at];
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function gutterWidth(hunks: readonly ToolDiffHunk[]): number {
|
|
47
|
+
let max = 0;
|
|
48
|
+
|
|
49
|
+
for (const hunk of hunks) {
|
|
50
|
+
max = Math.max(max, hunk.oldStart + hunk.oldLines - 1);
|
|
51
|
+
max = Math.max(max, hunk.newStart + hunk.newLines - 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return Math.max(1, String(max).length);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function lineNumber(line: ToolDiffHunk["lines"][number]): number | undefined {
|
|
58
|
+
if (line.kind === "added") {
|
|
59
|
+
return line.newLine;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (line.kind === "removed") {
|
|
63
|
+
return line.oldLine;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return line.newLine ?? line.oldLine;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const DiffLayout = { detab, mapSides, gutterWidth, lineNumber };
|