fleetlens 0.15.2 → 0.15.4
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/app/apps/web/.next/BUILD_ID +1 -1
- package/app/apps/web/.next/app-path-routes-manifest.json +3 -0
- package/app/apps/web/.next/build-manifest.json +3 -3
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/routes-manifest.json +18 -0
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/menubar/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/menubar/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/menubar/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/menubar/route.js +6 -0
- package/app/apps/web/.next/server/app/api/menubar/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/menubar/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/menubar/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/pdf/[key]/route.js +1 -1
- package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/projects/open-folder/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/projects/open-folder/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/projects/open-folder/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/projects/open-folder/route.js +7 -0
- package/app/apps/web/.next/server/app/api/projects/open-folder/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/projects/open-folder/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/projects/open-folder/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/settings/credentials/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/settings/credentials/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/settings/credentials/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/settings/credentials/route.js +10 -0
- package/app/apps/web/.next/server/app/api/settings/credentials/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/settings/credentials/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/settings/credentials/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/settings/route.js +4 -4
- package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page.js +1 -1
- package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page.js +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/page.js +1 -1
- package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/page.js +1 -1
- package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page.js +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/page.js +1 -1
- package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/page.js +1 -1
- package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/print/[key]/page.js +1 -1
- package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page.js +2 -2
- package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/runs/page.js +1 -1
- package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/settings/page.js +2 -2
- package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/team/onboarding/page.js +1 -1
- package/app/apps/web/.next/server/app/team/onboarding/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/team/onboarding/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/team/page.js +1 -1
- package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_projects_open-folder_route_actions_0m~419c.js +3 -0
- package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_settings_credentials_route_actions_04an_i2.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__05z0pcw._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0_nle98._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0b5r~-1._.js +3 -0
- package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0a0~7d6._.js → [root-of-the-server]__0t7i-hy._.js} +2 -2
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__12iid50._.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_menubar_route_actions_0xep8_i.js +3 -0
- package/app/apps/web/.next/server/chunks/{node_modules_next_036my~r._.js → node_modules_next_04~_e52._.js} +5 -5
- package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +14 -14
- package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
- package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0.zquoa._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__03svi~4._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04eq6z2._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04il6m2._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08l86e-._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__09rjz7_._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ar_8wr._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__10~b4bf._.js → [root-of-the-server]__0blm9gz._.js} +3 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0e.gxk3._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0g-tomj._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0i.q-1h._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0k1x-c9._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0lz4a~g._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0nr2~y1._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0o1_g.~._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0p1x6v2._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0r4tks8._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0rvzdnx._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0stu~~r._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0tejjy_._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0u39afl._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ufi3.w._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0xy1qim._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +3 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__10srpu0._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__10xgshr._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_02r-a1a._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/_0k62ejd._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/{_0xu2pvc._.js → _0k8yioi._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/{_0wj750k._.js → _0t~mx7w._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/{_0kajk31._.js → _0whg~r-._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/_13jdm1g._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_0~aj0~u._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_sessions-grid_tsx_0bri5x.._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0x014hn._.js +16 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_12vc-v-._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
- package/app/apps/web/.next/server/functions-config-manifest.json +3 -0
- package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/app/apps/web/.next/server/middleware-manifest.json +5 -5
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/chunks/0-z49mtd-zwgv.js +1 -0
- package/app/apps/web/.next/static/chunks/043slkg3juwck.js +1 -0
- package/app/apps/web/.next/static/chunks/{0~qdw1b2uz._5.js → 044mtrzh7d7va.js} +3 -3
- package/app/apps/web/.next/static/chunks/05b0dn.r00hq1.js +1 -0
- package/app/apps/web/.next/static/chunks/{14vx702c32kn6.js → 0_0~~ipv90vm3.js} +1 -1
- package/app/apps/web/.next/static/chunks/0awkd5df28gvj.js +6 -0
- package/app/apps/web/.next/static/chunks/{0m7dd-2zd9vl2.js → 0eoc4yy0s0gpe.js} +1 -1
- package/app/apps/web/.next/static/chunks/0g0hv.dt-de9g.js +14 -0
- package/app/apps/web/.next/static/chunks/0rsp61c8wb.sx.js +6 -0
- package/app/apps/web/.next/static/chunks/{03rd.3cc_t~yf.js → 0s~0-8l~_kqnf.js} +1 -1
- package/app/apps/web/.next/static/chunks/0t2on1dhydyb2.css +1 -0
- package/app/apps/web/.next/static/chunks/0y3448zrint0k.js +6 -0
- package/app/apps/web/app/api/menubar/route.ts +66 -0
- package/app/apps/web/app/api/projects/open-folder/route.ts +60 -0
- package/app/apps/web/app/api/settings/credentials/route.ts +45 -0
- package/app/apps/web/app/layout.tsx +2 -0
- package/app/apps/web/app/projects/[slug]/local-folders-button.tsx +406 -0
- package/app/apps/web/app/projects/[slug]/page.tsx +155 -20
- package/app/apps/web/app/projects/page.tsx +2 -2
- package/app/apps/web/app/sessions/[id]/page.tsx +4 -1
- package/app/apps/web/app/sessions/[id]/session-view/header-stats.tsx +81 -0
- package/app/apps/web/app/sessions/[id]/session-view.tsx +11 -0
- package/app/apps/web/app/sessions/sessions-grid.tsx +56 -25
- package/app/apps/web/app/settings/page.tsx +26 -0
- package/app/apps/web/app/usage/page.tsx +80 -0
- package/app/apps/web/components/menubar-install-banner.tsx +117 -0
- package/app/apps/web/components/menubar-install.tsx +107 -0
- package/app/apps/web/components/zai-credentials-form.tsx +116 -0
- package/app/apps/web/lib/changelog-data.generated.ts +1 -1
- package/app/apps/web/lib/entries-index.ts +4 -4
- package/app/apps/web/lib/menubar-data.ts +50 -0
- package/app/apps/web/lib/usage-data.ts +4 -0
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +640 -240
- package/dist/index.js +938 -283
- package/package.json +3 -2
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__065lib8._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0a7n44-._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0c00i76._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0lhwp-k._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/_0s3n5mz._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/_12mbgs5._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/apps_web_0q2x4a~._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0b-4-4j._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +0 -16
- package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0cepfi8._.js +0 -3
- package/app/apps/web/.next/static/chunks/00a9oyw~h5i1p.js +0 -1
- package/app/apps/web/.next/static/chunks/03gy1evbn94qu.js +0 -6
- package/app/apps/web/.next/static/chunks/040s42ypidk7s.css +0 -1
- package/app/apps/web/.next/static/chunks/0d2x-wfu.m0-z.js +0 -6
- package/app/apps/web/.next/static/chunks/0i77er85v.rj..js +0 -1
- package/app/apps/web/.next/static/chunks/0nqklmbs5s3ax.js +0 -6
- package/app/apps/web/.next/static/chunks/0tirv_vo6q8uc.js +0 -1
- package/app/apps/web/.next/static/chunks/0u6gxpqhl0gc7.js +0 -14
- /package/app/apps/web/.next/static/{mTj8nKiazosAKJec15dLe → nmAGY0LiBgXuibWTKZ1RD}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{mTj8nKiazosAKJec15dLe → nmAGY0LiBgXuibWTKZ1RD}/_clientMiddlewareManifest.js +0 -0
- /package/app/apps/web/.next/static/{mTj8nKiazosAKJec15dLe → nmAGY0LiBgXuibWTKZ1RD}/_ssgManifest.js +0 -0
|
@@ -15,10 +15,91 @@
|
|
|
15
15
|
* a component (no hook calls at module load).
|
|
16
16
|
*/
|
|
17
17
|
import React, { useRef, useState } from "react";
|
|
18
|
+
import { Github, GitBranch } from "lucide-react";
|
|
18
19
|
import type { SessionEvent } from "@claude-lens/parser";
|
|
20
|
+
import type { GitFolderInfo } from "@claude-lens/parser/fs";
|
|
19
21
|
import { formatTokens } from "@/lib/format";
|
|
20
22
|
import { TooltipRow } from "./tooltip";
|
|
21
23
|
|
|
24
|
+
const repoChip = {
|
|
25
|
+
fontFamily: "var(--font-mono)",
|
|
26
|
+
fontSize: 10.5,
|
|
27
|
+
padding: "1px 6px",
|
|
28
|
+
border: "1px solid var(--af-border-subtle)",
|
|
29
|
+
borderRadius: 999,
|
|
30
|
+
whiteSpace: "nowrap" as const,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The repo a session's cwd belongs to. `branch` is that folder's branch RIGHT
|
|
34
|
+
* NOW — git keeps no record of what it was when the session ran — so it is
|
|
35
|
+
* labelled as such rather than presented as the session's branch. */
|
|
36
|
+
export function RepoStat({
|
|
37
|
+
git,
|
|
38
|
+
worktreeName,
|
|
39
|
+
}: {
|
|
40
|
+
git: GitFolderInfo;
|
|
41
|
+
worktreeName?: string;
|
|
42
|
+
}) {
|
|
43
|
+
const { remote, branch, defaultBranch } = git;
|
|
44
|
+
return (
|
|
45
|
+
<span
|
|
46
|
+
style={{
|
|
47
|
+
display: "inline-flex",
|
|
48
|
+
alignItems: "center",
|
|
49
|
+
gap: 6,
|
|
50
|
+
fontSize: 12,
|
|
51
|
+
color: "var(--af-text-secondary)",
|
|
52
|
+
minWidth: 0,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{remote && (
|
|
56
|
+
<a
|
|
57
|
+
href={remote.webUrl}
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noreferrer"
|
|
60
|
+
title={remote.url}
|
|
61
|
+
style={{
|
|
62
|
+
display: "inline-flex",
|
|
63
|
+
alignItems: "center",
|
|
64
|
+
gap: 5,
|
|
65
|
+
color: "inherit",
|
|
66
|
+
textDecoration: "none",
|
|
67
|
+
overflow: "hidden",
|
|
68
|
+
textOverflow: "ellipsis",
|
|
69
|
+
whiteSpace: "nowrap",
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{remote.host === "github.com" ? (
|
|
73
|
+
<Github size={12} aria-label="GitHub" />
|
|
74
|
+
) : (
|
|
75
|
+
<GitBranch size={12} aria-label={remote.host} />
|
|
76
|
+
)}
|
|
77
|
+
{remote.owner}/{remote.name}
|
|
78
|
+
</a>
|
|
79
|
+
)}
|
|
80
|
+
{worktreeName && (
|
|
81
|
+
<span style={{ ...repoChip, color: "var(--af-text-secondary)" }}>
|
|
82
|
+
worktree: {worktreeName}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
{branch && (
|
|
86
|
+
<span
|
|
87
|
+
title={`This folder is currently on "${branch}"${
|
|
88
|
+
defaultBranch ? ` (default: ${defaultBranch})` : ""
|
|
89
|
+
}. Not necessarily the branch this session ran on.`}
|
|
90
|
+
style={{
|
|
91
|
+
...repoChip,
|
|
92
|
+
color:
|
|
93
|
+
branch === defaultBranch ? "var(--af-text-tertiary)" : "var(--af-text)",
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{branch}
|
|
97
|
+
</span>
|
|
98
|
+
)}
|
|
99
|
+
</span>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
22
103
|
export function InlineStat({
|
|
23
104
|
icon,
|
|
24
105
|
value,
|
|
@@ -55,10 +55,13 @@ import {
|
|
|
55
55
|
InlineStatDivider,
|
|
56
56
|
EntrypointBadge,
|
|
57
57
|
InlineTokenStat,
|
|
58
|
+
RepoStat,
|
|
58
59
|
useAnchoredTooltip,
|
|
59
60
|
AnchoredTooltip,
|
|
60
61
|
} from "./session-view/header-stats";
|
|
61
62
|
import { Drawer } from "./session-view/drawer";
|
|
63
|
+
// type-only: erased at build, so node:fs never reaches the client bundle
|
|
64
|
+
import type { GitFolderInfo } from "@claude-lens/parser/fs";
|
|
62
65
|
import {
|
|
63
66
|
WorkflowsPanel,
|
|
64
67
|
WorkflowAgentDrawer,
|
|
@@ -128,11 +131,13 @@ const VALID_TABS: TabId[] = ["transcript", "workflows", "team", "debug"];
|
|
|
128
131
|
|
|
129
132
|
export function SessionView({
|
|
130
133
|
session,
|
|
134
|
+
git,
|
|
131
135
|
team,
|
|
132
136
|
teamLead,
|
|
133
137
|
entries = [],
|
|
134
138
|
}: {
|
|
135
139
|
session: SessionDetail;
|
|
140
|
+
git?: GitFolderInfo;
|
|
136
141
|
team?: (TimelineData & { teamName: string }) | null;
|
|
137
142
|
teamLead?: { leadSessionId: string; teamName: string; agentName: string } | null;
|
|
138
143
|
entries?: Entry[];
|
|
@@ -1128,6 +1133,12 @@ export function SessionView({
|
|
|
1128
1133
|
)}
|
|
1129
1134
|
<InlineStatDivider />
|
|
1130
1135
|
<InlineStat icon={<Folder size={12} />} value={projectName} truncate />
|
|
1136
|
+
{git && (git.remote || git.branch || session.worktreeName) && (
|
|
1137
|
+
<>
|
|
1138
|
+
<InlineStatDivider />
|
|
1139
|
+
<RepoStat git={git} worktreeName={session.worktreeName} />
|
|
1140
|
+
</>
|
|
1141
|
+
)}
|
|
1131
1142
|
<InlineStatDivider />
|
|
1132
1143
|
{airTimeMs !== undefined && (
|
|
1133
1144
|
<InlineStat
|
|
@@ -4,7 +4,7 @@ import Link from "next/link";
|
|
|
4
4
|
import { useCallback, useMemo } from "react";
|
|
5
5
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
6
6
|
import type { AgentKind, SessionMeta } from "@claude-lens/parser";
|
|
7
|
-
import { getAgentMetadata, isAgentKind } from "@claude-lens/parser";
|
|
7
|
+
import { buildProjectKeyResolver, getAgentMetadata, isAgentKind, projectKey } from "@claude-lens/parser";
|
|
8
8
|
import type { DayOutcome, EntryEnrichmentStatus } from "@claude-lens/entries";
|
|
9
9
|
import {
|
|
10
10
|
formatDuration,
|
|
@@ -30,6 +30,10 @@ export type SessionRow = {
|
|
|
30
30
|
latestLocalDay: string | null;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
type DisplaySessionRow = SessionRow & {
|
|
34
|
+
projectKey: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
33
37
|
type SortBy = "newest" | "longest" | "most-tokens" | "outcome";
|
|
34
38
|
|
|
35
39
|
const SORT_VALUES: readonly SortBy[] = ["newest", "longest", "most-tokens", "outcome"] as const;
|
|
@@ -38,6 +42,10 @@ function isSortBy(s: string | null | undefined): s is SortBy {
|
|
|
38
42
|
return s !== null && s !== undefined && (SORT_VALUES as readonly string[]).includes(s);
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
function totalSessionTokens(s: SessionMeta): number {
|
|
46
|
+
return s.totalUsage.input + s.totalUsage.output + s.totalUsage.cacheRead + s.totalUsage.cacheWrite;
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
|
|
42
50
|
const router = useRouter();
|
|
43
51
|
const pathname = usePathname();
|
|
@@ -46,7 +54,22 @@ export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
|
|
|
46
54
|
// Filter state lives in the URL — back/forward replays the same view, and
|
|
47
55
|
// returning from a session detail page restores what the user had picked.
|
|
48
56
|
const query = searchParams.get("q") ?? "";
|
|
49
|
-
const
|
|
57
|
+
const keyOf = useMemo(
|
|
58
|
+
() => buildProjectKeyResolver(rows.map((r) => r.session)),
|
|
59
|
+
[rows],
|
|
60
|
+
);
|
|
61
|
+
const displayRows = useMemo<DisplaySessionRow[]>(
|
|
62
|
+
() => rows.map((row) => ({ ...row, projectKey: keyOf(row.session) })),
|
|
63
|
+
[rows, keyOf],
|
|
64
|
+
);
|
|
65
|
+
// Older links carried the raw projectName path. Resolve those onto the
|
|
66
|
+
// project key so /sessions?project=… selects the same set as /projects/<key>.
|
|
67
|
+
const project = useMemo(() => {
|
|
68
|
+
const raw = searchParams.get("project") ?? "all";
|
|
69
|
+
if (raw === "all" || !raw.startsWith("/")) return raw;
|
|
70
|
+
const match = rows.find((r) => r.session.projectName === raw);
|
|
71
|
+
return match ? keyOf(match.session) : projectKey(raw);
|
|
72
|
+
}, [searchParams, rows, keyOf]);
|
|
50
73
|
const agentParam = searchParams.get("agent");
|
|
51
74
|
const agent: AgentKind | "all" = isAgentKind(agentParam) ? agentParam : "all";
|
|
52
75
|
const sortBy: SortBy = isSortBy(searchParams.get("sort"))
|
|
@@ -72,9 +95,22 @@ export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
|
|
|
72
95
|
const setSortBy = (v: SortBy) => updateParam("sort", v, "newest");
|
|
73
96
|
|
|
74
97
|
const projects = useMemo(() => {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
98
|
+
const totals = new Map<string, { tokens: number; sessions: number }>();
|
|
99
|
+
for (const r of displayRows) {
|
|
100
|
+
const cur = totals.get(r.projectKey) ?? { tokens: 0, sessions: 0 };
|
|
101
|
+
cur.tokens += totalSessionTokens(r.session);
|
|
102
|
+
cur.sessions += 1;
|
|
103
|
+
totals.set(r.projectKey, cur);
|
|
104
|
+
}
|
|
105
|
+
const ordered = Array.from(totals.entries())
|
|
106
|
+
.sort(([aKey, a], [bKey, b]) => {
|
|
107
|
+
if (b.tokens !== a.tokens) return b.tokens - a.tokens;
|
|
108
|
+
if (b.sessions !== a.sessions) return b.sessions - a.sessions;
|
|
109
|
+
return aKey.localeCompare(bKey);
|
|
110
|
+
})
|
|
111
|
+
.map(([key]) => key);
|
|
112
|
+
return ["all", ...ordered];
|
|
113
|
+
}, [displayRows]);
|
|
78
114
|
|
|
79
115
|
const agentOptions = useMemo(() => {
|
|
80
116
|
const counts = new Map<AgentKind, number>();
|
|
@@ -86,19 +122,20 @@ export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
|
|
|
86
122
|
}, [rows]);
|
|
87
123
|
|
|
88
124
|
const filtered = useMemo(() => {
|
|
89
|
-
let items =
|
|
90
|
-
if (project !== "all") items = items.filter((r) => r.
|
|
125
|
+
let items = displayRows.slice();
|
|
126
|
+
if (project !== "all") items = items.filter((r) => r.projectKey === project);
|
|
91
127
|
if (agent !== "all")
|
|
92
128
|
items = items.filter((r) => (r.session.agent ?? "claude-code") === agent);
|
|
93
129
|
if (query) {
|
|
94
130
|
const q = query.toLowerCase();
|
|
95
131
|
items = items.filter(
|
|
96
|
-
({ session: s, briefSummary }) =>
|
|
132
|
+
({ session: s, briefSummary, projectKey }) =>
|
|
97
133
|
s.id.toLowerCase().includes(q) ||
|
|
98
134
|
(briefSummary ?? "").toLowerCase().includes(q) ||
|
|
99
135
|
(s.firstUserPreview ?? "").toLowerCase().includes(q) ||
|
|
100
136
|
(s.lastAgentPreview ?? "").toLowerCase().includes(q) ||
|
|
101
|
-
s.projectName.toLowerCase().includes(q)
|
|
137
|
+
s.projectName.toLowerCase().includes(q) ||
|
|
138
|
+
projectKey.toLowerCase().includes(q),
|
|
102
139
|
);
|
|
103
140
|
}
|
|
104
141
|
if (sortBy === "newest")
|
|
@@ -115,18 +152,12 @@ export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
|
|
|
115
152
|
);
|
|
116
153
|
else if (sortBy === "most-tokens")
|
|
117
154
|
items.sort(
|
|
118
|
-
(a, b) =>
|
|
119
|
-
b.session.totalUsage.input +
|
|
120
|
-
b.session.totalUsage.output +
|
|
121
|
-
b.session.totalUsage.cacheRead -
|
|
122
|
-
(a.session.totalUsage.input +
|
|
123
|
-
a.session.totalUsage.output +
|
|
124
|
-
a.session.totalUsage.cacheRead),
|
|
155
|
+
(a, b) => totalSessionTokens(b.session) - totalSessionTokens(a.session),
|
|
125
156
|
);
|
|
126
157
|
else if (sortBy === "outcome")
|
|
127
158
|
items.sort((a, b) => outcomePriority(b.outcome) - outcomePriority(a.outcome));
|
|
128
159
|
return items;
|
|
129
|
-
}, [
|
|
160
|
+
}, [displayRows, project, agent, query, sortBy]);
|
|
130
161
|
|
|
131
162
|
return (
|
|
132
163
|
<div>
|
|
@@ -199,7 +230,7 @@ export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
|
|
|
199
230
|
{filtered.length === 0 ? (
|
|
200
231
|
<div className="af-empty">No sessions found.</div>
|
|
201
232
|
) : viewMode === "table" ? (
|
|
202
|
-
<DataTable<
|
|
233
|
+
<DataTable<DisplaySessionRow>
|
|
203
234
|
rows={filtered}
|
|
204
235
|
getRowKey={(r) => `${r.session.projectDir}/${r.session.id}`}
|
|
205
236
|
onRowClick={(r) => router.push(`/sessions/${r.session.id}`)}
|
|
@@ -224,7 +255,7 @@ export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
|
|
|
224
255
|
);
|
|
225
256
|
}
|
|
226
257
|
|
|
227
|
-
const sessionTableColumns: Column<
|
|
258
|
+
const sessionTableColumns: Column<DisplaySessionRow>[] = [
|
|
228
259
|
{
|
|
229
260
|
key: "outcome",
|
|
230
261
|
header: "Outcome",
|
|
@@ -234,7 +265,7 @@ const sessionTableColumns: Column<SessionRow>[] = [
|
|
|
234
265
|
{
|
|
235
266
|
key: "project",
|
|
236
267
|
header: "Project",
|
|
237
|
-
sortValue: (r) => r.
|
|
268
|
+
sortValue: (r) => r.projectKey,
|
|
238
269
|
render: (r) => (
|
|
239
270
|
<div
|
|
240
271
|
style={{
|
|
@@ -243,7 +274,7 @@ const sessionTableColumns: Column<SessionRow>[] = [
|
|
|
243
274
|
textOverflow: "ellipsis",
|
|
244
275
|
whiteSpace: "nowrap",
|
|
245
276
|
}}
|
|
246
|
-
title={r.session.projectName}
|
|
277
|
+
title={`${r.projectKey} (${r.session.projectName})`}
|
|
247
278
|
>
|
|
248
279
|
<div style={{ fontWeight: 500, display: "flex", alignItems: "center", gap: 6 }}>
|
|
249
280
|
<span
|
|
@@ -254,7 +285,7 @@ const sessionTableColumns: Column<SessionRow>[] = [
|
|
|
254
285
|
minWidth: 0,
|
|
255
286
|
}}
|
|
256
287
|
>
|
|
257
|
-
{prettyProjectName(r.
|
|
288
|
+
{prettyProjectName(r.projectKey)}
|
|
258
289
|
</span>
|
|
259
290
|
<TeamBadge session={r.session} />
|
|
260
291
|
<AgentBadge agent={r.session.agent} />
|
|
@@ -399,7 +430,7 @@ function OutcomeCell({ row }: { row: SessionRow }) {
|
|
|
399
430
|
return <span style={{ color: "var(--af-text-tertiary)", fontSize: 10 }}>—</span>;
|
|
400
431
|
}
|
|
401
432
|
|
|
402
|
-
function SessionCard({ row }: { row:
|
|
433
|
+
function SessionCard({ row }: { row: DisplaySessionRow }) {
|
|
403
434
|
const router = useRouter();
|
|
404
435
|
const { session: s, briefSummary, outcome, latestLocalDay, enrichmentStatus } = row;
|
|
405
436
|
const totalTokens =
|
|
@@ -440,13 +471,13 @@ function SessionCard({ row }: { row: SessionRow }) {
|
|
|
440
471
|
gap: 6,
|
|
441
472
|
minWidth: 0,
|
|
442
473
|
}}
|
|
443
|
-
title={s.projectName}
|
|
474
|
+
title={`${row.projectKey} (${s.projectName})`}
|
|
444
475
|
>
|
|
445
476
|
<LiveBadge mtimeIso={s.lastTimestamp} activityMs={s.lastActivityMs} />
|
|
446
477
|
<span
|
|
447
478
|
style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
|
448
479
|
>
|
|
449
|
-
{prettyProjectName(
|
|
480
|
+
{prettyProjectName(row.projectKey)}
|
|
450
481
|
</span>
|
|
451
482
|
<TeamBadge session={s} linkable={false} />
|
|
452
483
|
<AgentBadge agent={s.agent} />
|
|
@@ -2,10 +2,15 @@ import { readSettings } from "@claude-lens/entries/node";
|
|
|
2
2
|
import { AiFeaturesForm } from "./ai-features-form";
|
|
3
3
|
import { readAutostartState } from "@/lib/autostart-data";
|
|
4
4
|
import { AutostartToggle } from "@/components/autostart-toggle";
|
|
5
|
+
import { readMenubarState } from "@/lib/menubar-data";
|
|
6
|
+
import { MenubarInstall } from "@/components/menubar-install";
|
|
7
|
+
import { readCredentialsMasked } from "@claude-lens/entries/node";
|
|
8
|
+
import { ZaiCredentialsForm } from "@/components/zai-credentials-form";
|
|
5
9
|
|
|
6
10
|
// Layout mirrors /team: max-w-3xl, header + subtitle, card-style sections.
|
|
7
11
|
export default function SettingsPage() {
|
|
8
12
|
const s = readSettings();
|
|
13
|
+
const creds = readCredentialsMasked();
|
|
9
14
|
return (
|
|
10
15
|
<main className="mx-auto max-w-3xl p-6 space-y-8">
|
|
11
16
|
<header>
|
|
@@ -24,6 +29,27 @@ export default function SettingsPage() {
|
|
|
24
29
|
<AutostartToggle initial={readAutostartState()} />
|
|
25
30
|
</section>
|
|
26
31
|
|
|
32
|
+
<section className="af-card space-y-3">
|
|
33
|
+
<h2 className="text-lg font-medium">Menu bar widget</h2>
|
|
34
|
+
<p className="text-sm text-gray-500">
|
|
35
|
+
Native macOS menu bar widget showing live Claude Code / Codex / Z.ai plan
|
|
36
|
+
utilization, reset countdowns, and burn-rate. Installs to ~/Applications
|
|
37
|
+
and updates as the daemon polls.
|
|
38
|
+
</p>
|
|
39
|
+
<MenubarInstall initial={readMenubarState()} />
|
|
40
|
+
</section>
|
|
41
|
+
|
|
42
|
+
<section className="af-card space-y-3">
|
|
43
|
+
<h2 className="text-lg font-medium">Z.ai API key</h2>
|
|
44
|
+
<p className="text-sm text-gray-500">
|
|
45
|
+
Optional. Add your Z.ai (GLM Coding Plan) API key so the daemon can track
|
|
46
|
+
your Z.ai usage alongside Claude Code and Codex. The key is stored in
|
|
47
|
+
~/.cclens/credentials.json — nothing leaves your machine except the same
|
|
48
|
+
API calls Z.ai's own subscription UI makes.
|
|
49
|
+
</p>
|
|
50
|
+
<ZaiCredentialsForm initial={creds} />
|
|
51
|
+
</section>
|
|
52
|
+
|
|
27
53
|
<section className="af-card space-y-3">
|
|
28
54
|
<h2 className="text-lg font-medium">AI Features</h2>
|
|
29
55
|
<p className="text-sm text-gray-500">
|
|
@@ -149,6 +149,26 @@ export default async function UsagePage({
|
|
|
149
149
|
</span>
|
|
150
150
|
</span>
|
|
151
151
|
)}
|
|
152
|
+
{selected === "zai" && latest?.plan_type && (
|
|
153
|
+
<span
|
|
154
|
+
style={{
|
|
155
|
+
marginLeft: "auto",
|
|
156
|
+
display: "inline-flex",
|
|
157
|
+
alignItems: "center",
|
|
158
|
+
gap: 8,
|
|
159
|
+
fontSize: 12,
|
|
160
|
+
padding: "4px 10px",
|
|
161
|
+
border: "1px solid var(--af-border-subtle)",
|
|
162
|
+
borderRadius: 999,
|
|
163
|
+
fontFamily: "var(--font-mono)",
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>plan</span>
|
|
167
|
+
<span style={{ color: "var(--af-text)", fontWeight: 600 }}>
|
|
168
|
+
{latest.plan_type}
|
|
169
|
+
</span>
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
152
172
|
</header>
|
|
153
173
|
|
|
154
174
|
{/* Agent tab strip — one Link per agent that has snapshots. */}
|
|
@@ -162,6 +182,9 @@ export default async function UsagePage({
|
|
|
162
182
|
<PreviousCyclesTrend windowLabel="7d" cycles={cycles7d} />
|
|
163
183
|
)}
|
|
164
184
|
<UsageChartsDashboard snapshots={snapshots} predicted={predicted ?? undefined} />
|
|
185
|
+
{selected === "zai" && latest?.web_search_quota && (
|
|
186
|
+
<WebSearchQuotaCard quota={latest.web_search_quota} />
|
|
187
|
+
)}
|
|
165
188
|
<div
|
|
166
189
|
style={{
|
|
167
190
|
fontSize: 11,
|
|
@@ -179,6 +202,63 @@ export default async function UsagePage({
|
|
|
179
202
|
);
|
|
180
203
|
}
|
|
181
204
|
|
|
205
|
+
function WebSearchQuotaCard({
|
|
206
|
+
quota,
|
|
207
|
+
}: {
|
|
208
|
+
quota: { used: number | null; limit: number | null };
|
|
209
|
+
}) {
|
|
210
|
+
// web_search_quota is a percentage meter (used = 0–100, limit = 100),
|
|
211
|
+
// matching Z.ai's portal display — not a raw usage/remaining split.
|
|
212
|
+
const pct = quota.used ?? 0;
|
|
213
|
+
return (
|
|
214
|
+
<div className="af-card" style={{ padding: "16px 18px" }}>
|
|
215
|
+
<div
|
|
216
|
+
style={{
|
|
217
|
+
fontSize: 11,
|
|
218
|
+
color: "var(--af-text-tertiary)",
|
|
219
|
+
textTransform: "uppercase",
|
|
220
|
+
letterSpacing: "0.05em",
|
|
221
|
+
fontWeight: 600,
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
Monthly web search
|
|
225
|
+
</div>
|
|
226
|
+
<div
|
|
227
|
+
style={{
|
|
228
|
+
fontSize: 28,
|
|
229
|
+
fontWeight: 700,
|
|
230
|
+
marginTop: 8,
|
|
231
|
+
letterSpacing: "-0.02em",
|
|
232
|
+
lineHeight: 1.1,
|
|
233
|
+
fontVariantNumeric: "tabular-nums",
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
{Math.round(pct)}%
|
|
237
|
+
</div>
|
|
238
|
+
<div
|
|
239
|
+
style={{
|
|
240
|
+
marginTop: 10,
|
|
241
|
+
height: 6,
|
|
242
|
+
width: "100%",
|
|
243
|
+
background: "var(--af-border-subtle)",
|
|
244
|
+
borderRadius: 999,
|
|
245
|
+
overflow: "hidden",
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
<div
|
|
249
|
+
style={{
|
|
250
|
+
height: "100%",
|
|
251
|
+
width: `${Math.min(100, pct)}%`,
|
|
252
|
+
background: "rgb(59, 130, 246)",
|
|
253
|
+
borderRadius: 999,
|
|
254
|
+
transition: "width 0.24s ease",
|
|
255
|
+
}}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
182
262
|
function AgentTabs({ agents, selected }: { agents: AgentKind[]; selected: AgentKind }) {
|
|
183
263
|
// Claude first, then everything else alphabetically.
|
|
184
264
|
const sorted = [...agents].sort((a, b) => {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
type MenubarState = {
|
|
6
|
+
supported: boolean;
|
|
7
|
+
installed: boolean;
|
|
8
|
+
running: boolean;
|
|
9
|
+
bundled: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DISMISS_KEY = "fleetlens:menubar-banner-dismissed";
|
|
13
|
+
|
|
14
|
+
// Global, dismissible "install the menu bar widget" banner. Fetches state on
|
|
15
|
+
// mount and self-hides once installed (or when the user dismisses it). Mirrors
|
|
16
|
+
// components/team-welcome-banner.tsx styling + localStorage-dismiss pattern.
|
|
17
|
+
export function MenubarInstallBanner() {
|
|
18
|
+
const [hidden, setHidden] = useState(true); // start hidden to avoid SSR flash
|
|
19
|
+
const [state, setState] = useState<MenubarState | null>(null);
|
|
20
|
+
const [busy, setBusy] = useState(false);
|
|
21
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
let cancelled = false;
|
|
25
|
+
fetch("/api/menubar", { cache: "no-store" })
|
|
26
|
+
.then((r) => r.json())
|
|
27
|
+
.then((s: MenubarState) => {
|
|
28
|
+
if (cancelled) return;
|
|
29
|
+
setState(s);
|
|
30
|
+
const dismissed = window.localStorage.getItem(DISMISS_KEY);
|
|
31
|
+
// Show only on macOS, when not installed, and not previously dismissed.
|
|
32
|
+
if (s.supported && !s.installed && !dismissed) setHidden(false);
|
|
33
|
+
})
|
|
34
|
+
.catch(() => {});
|
|
35
|
+
return () => {
|
|
36
|
+
cancelled = true;
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
// Re-hide if a background refresh (SSE router.refresh) flips installed=true.
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (state?.installed) setHidden(true);
|
|
43
|
+
}, [state?.installed]);
|
|
44
|
+
|
|
45
|
+
if (hidden) return null;
|
|
46
|
+
|
|
47
|
+
const dismiss = () => {
|
|
48
|
+
window.localStorage.setItem(DISMISS_KEY, "1");
|
|
49
|
+
setHidden(true);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
async function install() {
|
|
53
|
+
setBusy(true);
|
|
54
|
+
setError(null);
|
|
55
|
+
const res = await fetch("/api/menubar", {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "content-type": "application/json" },
|
|
58
|
+
body: JSON.stringify({ action: "install" }),
|
|
59
|
+
});
|
|
60
|
+
const body = await res.json().catch(() => null);
|
|
61
|
+
setBusy(false);
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
setError(body?.error ?? `Error ${res.status}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setState(body);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
role="status"
|
|
72
|
+
style={{
|
|
73
|
+
padding: "10px 24px",
|
|
74
|
+
background: "var(--af-accent-subtle)",
|
|
75
|
+
borderBottom: "1px solid var(--af-accent)",
|
|
76
|
+
color: "var(--af-text)",
|
|
77
|
+
fontSize: 13,
|
|
78
|
+
display: "flex",
|
|
79
|
+
gap: 12,
|
|
80
|
+
alignItems: "center",
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<span style={{ flex: 1 }}>
|
|
84
|
+
Get live Claude Code / Codex / Z.ai usage in your menu bar —{" "}
|
|
85
|
+
<strong>install the Fleetlens widget</strong>.
|
|
86
|
+
</span>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className="af-btn af-btn-primary"
|
|
90
|
+
disabled={busy}
|
|
91
|
+
onClick={() => void install()}
|
|
92
|
+
style={{ fontSize: 12, padding: "4px 12px" }}
|
|
93
|
+
>
|
|
94
|
+
{busy ? "Installing…" : "Install"}
|
|
95
|
+
</button>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={dismiss}
|
|
99
|
+
aria-label="Dismiss"
|
|
100
|
+
style={{
|
|
101
|
+
background: "transparent",
|
|
102
|
+
border: "none",
|
|
103
|
+
color: "var(--af-text-tertiary)",
|
|
104
|
+
cursor: "pointer",
|
|
105
|
+
fontSize: 16,
|
|
106
|
+
lineHeight: 1,
|
|
107
|
+
padding: "2px 6px",
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
×
|
|
111
|
+
</button>
|
|
112
|
+
{error && (
|
|
113
|
+
<span style={{ color: "var(--af-danger, #dc2626)" }}>{error}</span>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|