History

version and sidebar

Rhys Sullivan committed 7 months ago

Commit 3ce7bca1 parent caf89fa22 files · +237 −24Browse files

Unified · Split

22 files
  1. apps/server/package.json +1 −1
  2. apps/web/package.json +1 −1
  3. apps/web/src/shell.tsx +179 −4
  4. apps/web/vite.config.ts +19 −0
  5. package.json +20 −1
  6. packages/clients/react/package.json +1 −1
  7. packages/core/api/package.json +1 −1
  8. packages/core/execution/package.json +1 −1
  9. packages/core/sdk/package.json +1 −1
  10. packages/core/storage-file/package.json +1 −1
  11. packages/hosts/mcp/package.json +1 −1
  12. packages/kernel/core/package.json +1 −1
  13. packages/kernel/ir/package.json +1 −1
  14. packages/kernel/runtime-quickjs/package.json +1 −1
  15. packages/plugins/file-secrets/package.json +1 −1
  16. packages/plugins/google-discovery/package.json +1 −1
  17. packages/plugins/graphql/package.json +1 −1
  18. packages/plugins/keychain/package.json +1 −1
  19. packages/plugins/mcp/package.json +1 −1
  20. packages/plugins/onepassword/package.json +1 −1
  21. packages/plugins/openapi/package.json +1 −1
  22. packages/ui/package.json +1 −1
apps/server/package.json+1−1
@@ -1,6 +1,6 @@
1{1{
2 "name": "@executor/server",2 "name": "@executor/server",
3 "version": "0.0.0",3 "version": "1.4.0",
4 "private": true,4 "private": true,
5 "type": "module",5 "type": "module",
6 "exports": {6 "exports": {
apps/web/package.json+1−1
@@ -1,6 +1,6 @@
1{1{
2 "name": "@executor/web",2 "name": "@executor/web",
3 "version": "0.0.0",3 "version": "1.4.0",
4 "private": true,4 "private": true,
5 "type": "module",5 "type": "module",
6 "dependencies": {6 "dependencies": {
apps/web/src/shell.tsx+179−4
@@ -1,5 +1,5 @@
1import { Link, Outlet, useLocation } from "@tanstack/react-router";1import { Link, Outlet, useLocation } from "@tanstack/react-router";
2import { useEffect, useRef, useState } from "react";2import { useCallback, useEffect, useRef, useState } from "react";
3import {3import {
4 useAtomRefresh,4 useAtomRefresh,
5 useAtomValue,5 useAtomValue,
@@ -8,6 +8,165 @@ import { · 2 unmodified lines
8 sourcesAtom,8 sourcesAtom,
9 toolsAtom,9 toolsAtom,
10} from "@executor/react";10} from "@executor/react";
11import { Button } from "@executor/ui/components/button";
12
13// ── Env ─────────────────────────────────────────────────────────────────
14
15type AppMetaEnv = {
16 readonly VITE_APP_VERSION: string;
17 readonly VITE_GITHUB_URL: string;
18};
19
20const { VITE_APP_VERSION, VITE_GITHUB_URL } = (import.meta as ImportMeta & {
21 readonly env: AppMetaEnv;
22}).env;
23
24// ── Version helpers ─────────────────────────────────────────────────────
25
26type UpdateChannel = "latest" | "beta";
27
28const EXECUTOR_DIST_TAGS_PATH = "/v1/app/npm/dist-tags";
29
30type ParsedVersion = {
31 readonly major: number;
32 readonly minor: number;
33 readonly patch: number;
34 readonly prerelease: ReadonlyArray<string | number> | null;
35};
36
37const semverPattern =
38 /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<prerelease>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
39
40const resolveUpdateChannel = (version: string): UpdateChannel =>
41 version.includes("-beta.") ? "beta" : "latest";
42
43const parseVersion = (version: string): ParsedVersion | null => {
44 const match = version.trim().match(semverPattern);
45 if (!match?.groups) return null;
46 return {
47 major: Number(match.groups.major),
48 minor: Number(match.groups.minor),
49 patch: Number(match.groups.patch),
50 prerelease: match.groups.prerelease
51 ? match.groups.prerelease.split(".").map((id) =>
52 /^\d+$/.test(id) ? Number(id) : id,
53 )
54 : null,
55 };
56};
57
58const comparePrereleaseIdentifiers = (
59 left: ReadonlyArray<string | number> | null,
60 right: ReadonlyArray<string | number> | null,
61): number => {
62 if (left === null && right === null) return 0;
63 if (left === null) return 1;
64 if (right === null) return -1;
65 const max = Math.max(left.length, right.length);
66 for (let i = 0; i < max; i++) {
67 const l = left[i];
68 const r = right[i];
69 if (l === undefined) return -1;
70 if (r === undefined) return 1;
71 if (l === r) continue;
72 if (typeof l === "number" && typeof r === "number") return l < r ? -1 : 1;
73 if (typeof l === "number") return -1;
74 if (typeof r === "number") return 1;
75 return l < r ? -1 : 1;
76 }
77 return 0;
78};
79
80const compareVersions = (left: string, right: string): number | null => {
81 const lv = parseVersion(left);
82 const rv = parseVersion(right);
83 if (!lv || !rv) return null;
84 if (lv.major !== rv.major) return lv.major < rv.major ? -1 : 1;
85 if (lv.minor !== rv.minor) return lv.minor < rv.minor ? -1 : 1;
86 if (lv.patch !== rv.patch) return lv.patch < rv.patch ? -1 : 1;
87 return comparePrereleaseIdentifiers(lv.prerelease, rv.prerelease);
88};
89
90// ── useLatestVersion ────────────────────────────────────────────────────
91
92function useLatestVersion(currentVersion: string) {
93 const channel = resolveUpdateChannel(currentVersion);
94 const [latestVersion, setLatestVersion] = useState<string | null>(null);
95
96 useEffect(() => {
97 let cancelled = false;
98 fetch(EXECUTOR_DIST_TAGS_PATH)
99 .then(async (res) => {
100 if (!res.ok) throw new Error(`Failed to load dist tags: ${res.status}`);
101 return res.json() as Promise<Partial<Record<UpdateChannel, string>>>;
102 })
103 .then((data) => {
104 if (!cancelled) setLatestVersion(data[channel] ?? null);
105 })
106 .catch(() => {});
107 return () => { cancelled = true; };
108 }, [channel]);
109
110 const updateAvailable =
111 latestVersion !== null && compareVersions(currentVersion, latestVersion) === -1;
112
113 return { latestVersion, updateAvailable, channel };
114}
115
116// ── UpdateCard ──────────────────────────────────────────────────────────
117
118function UpdateCard(props: { latestVersion: string; channel: UpdateChannel }) {
119 const command = `npm i -g executor@${props.channel}`;
120 const [copied, setCopied] = useState(false);
121
122 const handleCopy = useCallback(() => {
123 void navigator.clipboard.writeText(command).then(() => {
124 setCopied(true);
125 setTimeout(() => setCopied(false), 1500);
126 });
127 }, [command]);
128
129 return (
130 <div className="mx-2 mb-2 rounded-xl border border-primary/25 bg-primary/[0.06] p-3">
131 <div className="flex items-center gap-2">
132 <div className="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/15">
133 <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary">
134 <path d="M8 3v7M5 7l3 3 3-3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
135 <path d="M3 12h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
136 </svg>
137 </div>
138 <div className="min-w-0">
139 <p className="text-[11px] font-semibold text-foreground">Update available</p>
140 <p className="text-[10px] text-muted-foreground">
141 v{props.latestVersion}
142 </p>
143 </div>
144 </div>
145 <Button
146 type="button"
147 variant="outline"
148 onClick={handleCopy}
149 className="mt-2.5 flex w-full items-center justify-between gap-2 rounded-lg border-border/60 bg-background/50 px-2.5 py-1.5 text-left hover:bg-background/80"
150 >
151 <code className="truncate font-mono text-[10px] text-sidebar-foreground">
152 {command}
153 </code>
154 <span className="shrink-0 text-muted-foreground transition-colors group-hover:text-foreground">
155 {copied ? (
156 <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary">
157 <path d="M3 8.5l3.5 3.5L13 4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
158 </svg>
159 ) : (
160 <svg viewBox="0 0 16 16" fill="none" className="size-3">
161 <rect x="5" y="5" width="8" height="8" rx="1.5" stroke="currentColor" strokeWidth="1.2" />
162 <path d="M3 11V3.5A.5.5 0 013.5 3H11" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
163 </svg>
164 )}
165 </span>
166 </Button>
167 </div>
168 );
169}
11 170
12// ── NavItem ──────────────────────────────────────────────────────────────171// ── NavItem ──────────────────────────────────────────────────────────────
13 172
@@ -83,6 +242,9 @@ function SidebarContent(props: { · 69 unmodified lines
83 pathname: string;242 pathname: string;
84 onNavigate?: () => void;243 onNavigate?: () => void;
85 showBrand?: boolean;244 showBrand?: boolean;
245 updateAvailable: boolean;
246 latestVersion: string | null;
247 channel: UpdateChannel;
86}) {248}) {
87 const isHome = props.pathname === "/" || props.pathname === "/tools";249 const isHome = props.pathname === "/" || props.pathname === "/tools";
88 const isSecrets = props.pathname === "/secrets";250 const isSecrets = props.pathname === "/secrets";
@@ -141,12 +303,16 @@ function SidebarContent(props: { · 52 unmodified lines
141 <SourceList pathname={props.pathname} onNavigate={props.onNavigate} />303 <SourceList pathname={props.pathname} onNavigate={props.onNavigate} />
142 </nav>304 </nav>
143 305
306 {props.updateAvailable && props.latestVersion && (
307 <UpdateCard latestVersion={props.latestVersion} channel={props.channel} />
308 )}
309
144 {/* Footer */}310 {/* Footer */}
145 <div className="shrink-0 border-t border-sidebar-border px-4 py-2.5">311 <div className="shrink-0 border-t border-sidebar-border px-4 py-2.5">
146 <div className="flex items-center justify-between text-[10px] leading-none">312 <div className="flex items-center justify-between text-[10px] leading-none">
147 <span className="text-muted-foreground/70 tabular-nums">v4.0.0-dev</span>313 <span className="text-muted-foreground/70 tabular-nums">v{VITE_APP_VERSION}</span>
148 <a314 <a
149 href="https://github.com"315 href={VITE_GITHUB_URL}
150 target="_blank"316 target="_blank"
151 rel="noreferrer"317 rel="noreferrer"
152 className="text-muted-foreground/70 transition-colors hover:text-foreground"318 className="text-muted-foreground/70 transition-colors hover:text-foreground"
@@ -172,6 +338,7 @@ export function Shell() { · 19 unmodified lines
172 const refreshSourceTools = useAtomRefresh(338 const refreshSourceTools = useAtomRefresh(
173 sourceToolsAtom(currentSourceId ?? "__runtime__"),339 sourceToolsAtom(currentSourceId ?? "__runtime__"),
174 );340 );
341 const { latestVersion, updateAvailable, channel } = useLatestVersion(VITE_APP_VERSION);
175 const lastPathname = useRef(pathname);342 const lastPathname = useRef(pathname);
176 const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);343 const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
177 if (lastPathname.current !== pathname) {344 if (lastPathname.current !== pathname) {
@@ -213,7 +380,12 @@ export function Shell() { · 35 unmodified lines
213 <div className="flex h-screen overflow-hidden">380 <div className="flex h-screen overflow-hidden">
214 {/* Desktop sidebar */}381 {/* Desktop sidebar */}
215 <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56">382 <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56">
216 <SidebarContent pathname={pathname} />383 <SidebarContent
384 pathname={pathname}
385 updateAvailable={updateAvailable}
386 latestVersion={latestVersion}
387 channel={channel}
388 />
217 </aside>389 </aside>
218 390
219 {/* Mobile sidebar overlay */}391 {/* Mobile sidebar overlay */}
@@ -250,6 +422,9 @@ export function Shell() { · 30 unmodified lines
250 pathname={pathname}422 pathname={pathname}
251 onNavigate={() => setMobileSidebarOpen(false)}423 onNavigate={() => setMobileSidebarOpen(false)}
252 showBrand={false}424 showBrand={false}
425 updateAvailable={updateAvailable}
426 latestVersion={latestVersion}
427 channel={channel}
253 />428 />
254 </div>429 </div>
255 </div>430 </div>
apps/web/vite.config.ts+19−0
@@ -1,3 +1,4 @@
1import { readFileSync } from "node:fs";
1import { resolve } from "node:path";2import { resolve } from "node:path";
2import { defineConfig } from "vite";3import { defineConfig } from "vite";
3import react from "@vitejs/plugin-react";4import react from "@vitejs/plugin-react";
@@ -77,7 +78,25 @@ const sendWebResponse = async ( · 73 unmodified lines
77 }78 }
78};79};
79 80
81const rootPackage = JSON.parse(
82 readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
83) as { version: string; homepage?: string; repository?: string | { url?: string } };
84
85const repositoryUrl =
86 typeof rootPackage.repository === "string"
87 ? rootPackage.repository
88 : rootPackage.repository?.url;
89
90const EXECUTOR_VERSION = rootPackage.version;
91const EXECUTOR_GITHUB_URL = (rootPackage.homepage ?? repositoryUrl ?? "https://github.com/RhysSullivan/executor")
92 .replace(/^git\+/, "")
93 .replace(/\.git$/, "");
94
80export default defineConfig({95export default defineConfig({
96 define: {
97 "import.meta.env.VITE_APP_VERSION": JSON.stringify(EXECUTOR_VERSION),
98 "import.meta.env.VITE_GITHUB_URL": JSON.stringify(EXECUTOR_GITHUB_URL),
99 },
81 plugins: [100 plugins: [
82 react(),101 react(),
83 tailwindcss(),102 tailwindcss(),
package.json+20−1
@@ -1,5 +1,24 @@
1{1{
2 "name": "v4-executor",2 "name": "executor",
3 "version": "1.4.0",
4 "description": "Local AI executor with a CLI, local API server, and web UI.",
5 "keywords": [
6 "executor",
7 "ai",
8 "agent",
9 "cli",
10 "automation",
11 "local-first"
12 ],
13 "homepage": "https://github.com/RhysSullivan/executor",
14 "bugs": {
15 "url": "https://github.com/RhysSullivan/executor/issues"
16 },
17 "repository": {
18 "type": "git",
19 "url": "git+https://github.com/RhysSullivan/executor.git"
20 },
21 "license": "MIT",
3 "private": true,22 "private": true,
4 "type": "module",23 "type": "module",
5 "packageManager": "bun@1.2.22",24 "packageManager": "bun@1.2.22",
packages/clients/react/package.json+1−1
@@ -1,6 +1,6 @@
1{1{
2 "name": "@executor/react",2 "name": "@executor/react",
3 "version": "0.0.0",3 "version": "1.4.0",
4 "private": true,4 "private": true,
5 "type": "module",5 "type": "module",
6 "exports": {6 "exports": {
packages/core/api/package.json+1−1
@@ -1,6 +1,6 @@
1{1{
2 "name": "@executor/api",2 "name": "@executor/api",
3 "version": "0.0.0",3 "version": "1.4.0",
4 "private": true,4 "private": true,
5 "type": "module",5 "type": "module",
6 "exports": {6 "exports": {
packages/core/execution/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/execution",2 "name": "@executor/execution",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/index.ts"7 ".": "./src/index.ts"
8 },8 },
packages/core/sdk/package.json+1−1
@@ -1,6 +1,6 @@
1{1{
2 "name": "@executor/sdk",2 "name": "@executor/sdk",
3 "version": "0.0.1",3 "version": "1.4.0",
4 "private": true,4 "private": true,
5 "type": "module",5 "type": "module",
6 "exports": {6 "exports": {
packages/core/storage-file/package.json+1−1
@@ -1,6 +1,6 @@
1{1{
2 "name": "@executor/storage-file",2 "name": "@executor/storage-file",
3 "version": "0.0.0",3 "version": "1.4.0",
4 "private": true,4 "private": true,
5 "type": "module",5 "type": "module",
6 "exports": {6 "exports": {
packages/hosts/mcp/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/host-mcp",2 "name": "@executor/host-mcp",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/index.ts"7 ".": "./src/index.ts"
8 },8 },
packages/kernel/core/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/codemode-core",2 "name": "@executor/codemode-core",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/index.ts"7 ".": "./src/index.ts"
8 },8 },
packages/kernel/ir/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/ir",2 "name": "@executor/ir",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/index.ts"7 ".": "./src/index.ts"
8 },8 },
packages/kernel/runtime-quickjs/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/runtime-quickjs",2 "name": "@executor/runtime-quickjs",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/index.ts"7 ".": "./src/index.ts"
8 },8 },
packages/plugins/file-secrets/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/plugin-file-secrets",2 "name": "@executor/plugin-file-secrets",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/index.ts"7 ".": "./src/index.ts"
8 },8 },
packages/plugins/google-discovery/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/plugin-google-discovery",2 "name": "@executor/plugin-google-discovery",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/sdk/index.ts",7 ".": "./src/sdk/index.ts",
8 "./api": "./src/api/index.ts",8 "./api": "./src/api/index.ts",
packages/plugins/graphql/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/plugin-graphql",2 "name": "@executor/plugin-graphql",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/sdk/index.ts",7 ".": "./src/sdk/index.ts",
8 "./api": "./src/api/index.ts",8 "./api": "./src/api/index.ts",
packages/plugins/keychain/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/plugin-keychain",2 "name": "@executor/plugin-keychain",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/index.ts"7 ".": "./src/index.ts"
8 },8 },
packages/plugins/mcp/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/plugin-mcp",2 "name": "@executor/plugin-mcp",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/sdk/index.ts",7 ".": "./src/sdk/index.ts",
8 "./api": "./src/api/index.ts",8 "./api": "./src/api/index.ts",
packages/plugins/onepassword/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/plugin-onepassword",2 "name": "@executor/plugin-onepassword",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/sdk/index.ts",7 ".": "./src/sdk/index.ts",
8 "./api": "./src/api/index.ts",8 "./api": "./src/api/index.ts",
packages/plugins/openapi/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
2 "name": "@executor/plugin-openapi",2 "name": "@executor/plugin-openapi",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "version": "0.0.0",5 "version": "1.4.0",
6 "exports": {6 "exports": {
7 ".": "./src/sdk/index.ts",7 ".": "./src/sdk/index.ts",
8 "./api": "./src/api/index.ts",8 "./api": "./src/api/index.ts",
packages/ui/package.json+1−1
@@ -1,6 +1,6 @@
1{1{
2 "name": "@executor/ui",2 "name": "@executor/ui",
3 "version": "0.0.0",3 "version": "1.4.0",
4 "private": true,4 "private": true,
5 "type": "module",5 "type": "module",
6 "exports": {6 "exports": {