nebulosity 1.0.0 → 1.1.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/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +11 -11
- package/.next/diagnostics/route-bundle-stats.json +10 -10
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +5 -5
- package/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/.next/server/app/_not-found/page/next-font-manifest.json +2 -3
- package/.next/server/app/_not-found/page.js +3 -2
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +27 -20
- package/.next/server/app/_not-found.segments/_full.segment.rsc +27 -20
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +34 -26
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +29 -23
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +33 -24
- package/.next/server/app/index.segments/_full.segment.rsc +29 -23
- package/.next/server/app/index.segments/_tree.segment.rsc +4 -5
- package/.next/server/app/page/build-manifest.json +3 -3
- package/.next/server/app/page/next-font-manifest.json +2 -3
- package/.next/server/app/page.js +4 -4
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__06blyow8irfw3._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0bmqko-d8fd5k._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0cmmlmvta4i0g._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0jgr1dt1j95ku._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0m0n163465-zb._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0p7epsgp2b5m3._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__1i5e5sqg45ens._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__1vjt0neva-819._.js +3 -3
- package/.next/server/chunks/_0uqus0ehnijt-._.js +1 -1
- package/.next/server/chunks/lib_background_start_ts_11u_1uu8b9yuy._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0_3l5tdzqy-td._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__10ockmlli-lun._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__0dyligeswbb47._.js → [root-of-the-server]__16-gclvix9mjd._.js} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__1auodc3c0rppy._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1cf6qt9agg_9-._.js +3 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__1i9r59yk4i69p._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1l3qp8u9yc_ig._.js +3 -0
- package/.next/server/chunks/ssr/_1p-0wsx9qfm68._.js +13 -0
- package/.next/server/chunks/ssr/app_map_18iu6j19jsqmu._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_1r3iqtbm45lqp._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0q-w892nagajq.js +3 -0
- package/.next/server/middleware-build-manifest.js +11 -11
- package/.next/server/next-font-manifest.js +1 -1
- package/.next/server/next-font-manifest.json +4 -6
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/preview-props.json +3 -3
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{1rljem7o1jtdo.js → 18kfexnrqek07.js} +1 -1
- package/.next/static/chunks/1hwbgd4nvvhg8.js +1 -0
- package/.next/static/chunks/25r4lkkf1shfo.css +3 -0
- package/.next/static/chunks/35ubvk2ivdjgs.js +1 -0
- package/.next/static/chunks/3ieqvmcg3hofj.js +31 -0
- package/.next/static/chunks/3w00e1w2125pp.js +11 -0
- package/.next/static/media/733da6bd8dac84c8-s.3hlisc-pwq13z.woff2 +0 -0
- package/.next/static/media/784318cea1dfcfa4-s.0-oo9_1x_xmvg.woff2 +0 -0
- package/.next/static/media/7e114457660b62ed-s.36fqqi66-3tjb.woff2 +0 -0
- package/.next/static/media/85e86a88f68a624a-s.357dd_6abeqge.woff2 +0 -0
- package/.next/static/media/9a321a2c78eff0b5-s.p.1bkzbscqrt8rl.woff2 +0 -0
- package/.next/static/media/9fecbd3e5ea9bccb-s.3_z45zcoc-xoz.woff2 +0 -0
- package/.next/static/media/c64053b510252f55-s.1kd-r_s9smv5m.woff2 +0 -0
- package/.next/static/media/c6a5c2fdc12da45c-s.p.17-aodiw50953.woff2 +0 -0
- package/.next/static/media/f263782c6a4bddee-s.1gyfqm5yhhzs7.woff2 +0 -0
- package/.next/trace +1 -1
- package/.next/trace-build +1 -1
- package/README.md +13 -4
- package/app/globals.css +91 -54
- package/app/layout.tsx +20 -11
- package/app/map/BackButton.tsx +5 -5
- package/app/map/BoxActions.tsx +20 -12
- package/app/map/Breadcrumb.tsx +132 -46
- package/app/map/DetailPanel.tsx +408 -297
- package/app/map/Explorer.tsx +268 -0
- package/app/map/Header.tsx +105 -0
- package/app/map/HelpDialog.tsx +37 -34
- package/app/map/Legend.tsx +19 -75
- package/app/map/LineCard.tsx +187 -0
- package/app/map/MapBox.tsx +73 -38
- package/app/map/MapCanvas.tsx +344 -115
- package/app/map/MapToolbar.tsx +377 -0
- package/app/map/MapView.tsx +217 -213
- package/app/map/RepoMenu.tsx +13 -12
- package/app/map/RoutedEdge.tsx +48 -16
- package/app/map/SearchPalette.tsx +6 -6
- package/app/map/SourceViewer.tsx +19 -17
- package/app/map/StatusBar.tsx +166 -0
- package/app/map/embed.ts +22 -2
- package/app/map/emphasis.ts +94 -8
- package/app/map/explorerRows.ts +85 -0
- package/app/map/flowNodes.ts +12 -7
- package/app/map/format.ts +2 -2
- package/app/map/history.ts +4 -3
- package/app/map/inspector.ts +113 -0
- package/app/map/labels.ts +66 -7
- package/app/map/narrow.ts +165 -85
- package/app/map/opening.ts +16 -0
- package/app/map/portals.ts +2 -2
- package/app/map/routes.ts +1060 -55
- package/app/map/runs.ts +108 -0
- package/app/map/shell.ts +91 -0
- package/app/map/shortcuts.ts +2 -1
- package/app/map/summaries.ts +41 -5
- package/app/map/text.ts +9 -1
- package/app/map/theme.ts +197 -0
- package/app/map/useDismiss.ts +20 -6
- package/app/map/useEditorBridge.ts +12 -1
- package/app/map/useThemeKind.ts +28 -0
- package/app/map/useWindowSize.ts +37 -0
- package/app/map/viewport.ts +134 -16
- package/app/map/weights.ts +80 -0
- package/app/page.tsx +3 -5
- package/lib/background/child.ts +2 -0
- package/lib/background/service.ts +26 -10
- package/lib/contracts.ts +25 -2
- package/lib/db/migrations/0011_plain_summaries_prompt_version.sql +1 -0
- package/lib/db/migrations/meta/0011_snapshot.json +1498 -0
- package/lib/db/migrations/meta/_journal.json +7 -0
- package/lib/db/schema.ts +2 -1
- package/lib/db/store/map.ts +6 -0
- package/lib/db/store/summaries.ts +14 -8
- package/lib/engine/describe.ts +2 -0
- package/lib/index/summarize/budget.ts +5 -2
- package/lib/index/summarize/claude.ts +19 -4
- package/lib/index/summarize/compare.ts +2 -2
- package/lib/index/summarize/plan.ts +73 -13
- package/lib/index/summarize/preview.ts +12 -9
- package/lib/index/summarize/prompts.ts +122 -25
- package/lib/index/summarize/run.ts +48 -18
- package/lib/index/summarize/snapshot.ts +7 -2
- package/lib/index/summarize/status.ts +3 -2
- package/lib/tools/text.ts +3 -2
- package/mcp/server.ts +1 -1
- package/package.json +4 -2
- package/scripts/commands/summarize-command.ts +27 -9
- package/.next/server/chunks/ssr/[root-of-the-server]__0ekw8p2ramybv._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0fwbsqt9e2tr-._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__17_rqmx62cz59._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__1zfp6sltse4hf._.js +0 -3
- package/.next/server/chunks/ssr/_1d2woi6toj_0_._.js +0 -3
- package/.next/server/chunks/ssr/_1wfc1zz26g5ji._.js +0 -9
- package/.next/server/chunks/ssr/app_0gw6epmxyea3q._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_@swc_helpers_cjs__interop_require_default_cjs_1ztp13a5szri_._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_next_dist_1c5t_vsn6u8fr._.js +0 -3
- package/.next/static/chunks/02x9pxyl2d8om.css +0 -3
- package/.next/static/chunks/0p-0bv2dzer18.js +0 -7
- package/.next/static/chunks/10u7p2gj02qay.js +0 -1
- package/.next/static/chunks/31qp8jfkw0x2h.js +0 -31
- package/.next/static/media/123d516c9baeb953-s.1qc6dx89jd2qv.woff2 +0 -0
- package/.next/static/media/41ba59f553543ecf-s.3cb5lpr2l6xud.woff2 +0 -0
- package/.next/static/media/44ed10da57ebfb0e-s.2y65deuc30pmy.woff2 +0 -0
- package/.next/static/media/48eea41d8317494f-s.p.0wiir8udbzjvx.woff2 +0 -0
- package/.next/static/media/49b1d53681a91a99-s.1yd4_i3e35931.woff2 +0 -0
- package/.next/static/media/67bba87670b402d1-s.1u30mmyq6lnsd.woff2 +0 -0
- package/.next/static/media/6fca7e74603dd412-s.02z5rrbg5nt92.woff2 +0 -0
- package/.next/static/media/73263b2957b73100-s.2qt3tdwt-y9x_.woff2 +0 -0
- package/.next/static/media/7ac0b1cd3e695dd0-s.1i-sca43-qwng.woff2 +0 -0
- package/.next/static/media/916cd8a3bde3edf8-s.p.0oace-s_gkfks.woff2 +0 -0
- package/.next/static/media/a45aa1001bf9176f-s.2vemzm0itqzfy.woff2 +0 -0
- package/.next/static/media/a74d2542cc4c775f-s.29yke99gs9qpn.woff2 +0 -0
- package/.next/static/media/ab664c0e419cc841-s.3cvcemwuzoywn.woff2 +0 -0
- package/.next/static/media/c1b11b726b36a5e5-s.2rixgazldzomk.woff2 +0 -0
- package/.next/static/media/d6c1ff7e86974212-s.p.40sczeszzbjw1.woff2 +0 -0
- package/.next/static/media/faab88e4be272d29-s.0rgnxg2b64rzs.woff2 +0 -0
- package/app/map/ActivityBar.tsx +0 -81
- package/app/map/StatusPill.tsx +0 -93
- package/app/map/TopBar.tsx +0 -84
- /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_buildManifest.js +0 -0
- /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_ssgManifest.js +0 -0
package/app/map/viewport.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
import type { MapEdge } from "@/lib/contracts";
|
|
2
|
+
import { edgeKey } from "./emphasis";
|
|
1
3
|
import { BOX_HEIGHT, BOX_WIDTH, FIT_PADDING, type Position } from "./levelLayout";
|
|
4
|
+
import type { Point } from "./routes";
|
|
5
|
+
import { arrowHead, boxHeadWidths, lineWidth, maxImportsOf } from "./weights";
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Where a level opens (owner, 2026-10-13: "when you first open it, it should be a readable size"). A level is fitted to
|
|
5
9
|
* the canvas as before, unless fitting it whole would shrink it past MIN_READABLE_ZOOM. Then it opens as wide as the
|
|
6
10
|
* canvas allows (less SIDE_ROOM each side), between that floor and 100%, from its top, centred across, and the rest is
|
|
7
11
|
* a scroll or a drag away: the floor alone left wide empty margins on busy levels (an outside critique, owner's pick).
|
|
8
|
-
* The
|
|
12
|
+
* The toolbar's Fit still shows the whole level, and zooming out by hand still works (MapCanvas's minZoom).
|
|
13
|
+
*
|
|
14
|
+
* Both frame the drawn lines too (the owner, 2026-10-28): a lane beside the boxes ran off under the inspector. The
|
|
15
|
+
* boxes win, so framing the lines never shrinks them past LINES_FRAME_LEAST of their own fit, nor a level that opens
|
|
16
|
+
* fitted past MIN_READABLE_ZOOM. Lines cut off after a pan or a zoom are the person's doing, and stay.
|
|
9
17
|
*/
|
|
10
18
|
|
|
11
19
|
/** Box names are 15 px, so at 0.75 they're 11.25 px: the smallest the Map opens a level at. */
|
|
@@ -22,9 +30,18 @@ export const TOP_INSET = 64;
|
|
|
22
30
|
* opens VS Code's contrib, page 2, at 83% on a 1907 px screen instead of 75%.
|
|
23
31
|
*/
|
|
24
32
|
export const SIDE_ROOM = 72;
|
|
33
|
+
/** The room framed past the drawn lines' extent, so the outermost lane doesn't run along the canvas's edge. */
|
|
34
|
+
export const LINES_MARGIN = 16;
|
|
35
|
+
/**
|
|
36
|
+
* The least of the boxes' own fit that framing the lines may zoom out to: past it the boxes are framed alone and the
|
|
37
|
+
* outermost lines run off. Over VS Code, Fit then frames the lines on every page at rest, and with every line drawn
|
|
38
|
+
* keeps the boxes' fit on the densest few only (the merged-lines plan, fit4-final.log).
|
|
39
|
+
*/
|
|
40
|
+
export const LINES_FRAME_LEAST = 0.85;
|
|
25
41
|
|
|
26
42
|
export type Bounds = { x: number; y: number; width: number; height: number };
|
|
27
43
|
export type Viewport = { x: number; y: number; zoom: number };
|
|
44
|
+
type Canvas = { width: number; height: number };
|
|
28
45
|
|
|
29
46
|
/** The rectangle every box of a level covers; positions are top-left corners (levelLayout.ts). */
|
|
30
47
|
export function boundsOf(positions: ReadonlyMap<string, Position>): Bounds {
|
|
@@ -39,30 +56,131 @@ export function boundsOf(positions: ReadonlyMap<string, Position>): Bounds {
|
|
|
39
56
|
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
40
57
|
}
|
|
41
58
|
|
|
59
|
+
/**
|
|
60
|
+
* How far drawn lines reach: each line's corners grown by half its width, and its head's half-width either side of its
|
|
61
|
+
* tip. Every route ends going down into its box's top (RoutedEdge.tsx), so the head reaches across there, not past it.
|
|
62
|
+
* Null when no line has a route.
|
|
63
|
+
*/
|
|
64
|
+
export function linesBounds(lines: readonly { points: readonly Point[]; width: number; headHalf: number }[]): Bounds | null {
|
|
65
|
+
let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
|
|
66
|
+
for (const { points, width, headHalf } of lines){
|
|
67
|
+
if (points.length === 0) continue;
|
|
68
|
+
for (const { x, y } of points){
|
|
69
|
+
left = Math.min(left, x - width / 2);
|
|
70
|
+
top = Math.min(top, y - width / 2);
|
|
71
|
+
right = Math.max(right, x + width / 2);
|
|
72
|
+
bottom = Math.max(bottom, y + width / 2);
|
|
73
|
+
}
|
|
74
|
+
const tip = points[points.length - 1];
|
|
75
|
+
left = Math.min(left, tip.x - headHalf);
|
|
76
|
+
right = Math.max(right, tip.x + headHalf);
|
|
77
|
+
}
|
|
78
|
+
return left === Infinity ? null : { x: left, y: top, width: right - left, height: bottom - top };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The routed lines' reach (linesBounds), each as wide as MapCanvas draws it and under its box's one head (weights.ts),
|
|
83
|
+
* so what's framed is what's on screen.
|
|
84
|
+
*/
|
|
85
|
+
export function drawnLinesBounds(routes: ReadonlyMap<string, readonly Point[]>, edges: readonly MapEdge[]): Bounds | null {
|
|
86
|
+
const max = maxImportsOf(edges);
|
|
87
|
+
const heads = boxHeadWidths(edges);
|
|
88
|
+
const byKey = new Map(edges.map((e) => [edgeKey(e), e]));
|
|
89
|
+
return linesBounds([...routes].flatMap(([key, points]) => {
|
|
90
|
+
const edge = byKey.get(key);
|
|
91
|
+
if (edge === undefined) return [];
|
|
92
|
+
const width = lineWidth(edge.count, max);
|
|
93
|
+
return [{ points, width, headHalf: arrowHead(heads.get(edge.target) ?? width).half }];
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** What a fit frames, by its middle and its size. */
|
|
98
|
+
type Frame = { middle: Point; width: number; height: number };
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* One side to side (or top to bottom) span of a frame, from the boxes' middle, as React Flow centres its fit: half the
|
|
102
|
+
* boxes and FIT_PADDING's share each way, or as far as the lines and LINES_MARGIN reach past that. Worked out from the
|
|
103
|
+
* middle, so lines inside the boxes' frame leave it exactly as it was.
|
|
104
|
+
*/
|
|
105
|
+
function span(start: number, size: number, lines?: { start: number; size: number }): { middle: number; size: number } {
|
|
106
|
+
const middle = start + size / 2;
|
|
107
|
+
const boxes = size / 2 + (size * FIT_PADDING) / 2;
|
|
108
|
+
if (lines === undefined) return { middle, size: 2 * boxes };
|
|
109
|
+
const before = Math.max(boxes, middle - (lines.start - LINES_MARGIN));
|
|
110
|
+
const after = Math.max(boxes, lines.start + lines.size + LINES_MARGIN - middle);
|
|
111
|
+
return { middle: middle + (after - before) / 2, size: before + after };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The boxes as React Flow's fit frames them, and with `lines`, as far as they reach past them too. */
|
|
115
|
+
function frameOf(boxes: Bounds, lines?: Bounds): Frame {
|
|
116
|
+
const across = span(boxes.x, boxes.width, lines && { start: lines.x, size: lines.width });
|
|
117
|
+
const down = span(boxes.y, boxes.height, lines && { start: lines.y, size: lines.height });
|
|
118
|
+
return { middle: { x: across.middle, y: down.middle }, width: across.size, height: down.size };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const zoomFor = (frame: Frame, room: Canvas): number => Math.min(MAX_FIT_ZOOM, room.width / frame.width, room.height / frame.height);
|
|
122
|
+
|
|
123
|
+
/** The boxes' frame, grown to the lines when that keeps LINES_FRAME_LEAST of the boxes' own fit (the boxes win). */
|
|
124
|
+
function framed(boxes: Bounds, lines: Bounds | null, room: Canvas): Frame {
|
|
125
|
+
const own = frameOf(boxes);
|
|
126
|
+
if (lines === null) return own;
|
|
127
|
+
const both = frameOf(boxes, lines);
|
|
128
|
+
return zoomFor(both, room) >= LINES_FRAME_LEAST * zoomFor(own, room) ? both : own;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The toolbar's Fit: the level's boxes and its drawn lines (linesBounds) centred in the canvas, never past 100%, with
|
|
133
|
+
* no readable floor, since the person asked to see it whole. React Flow's own fit frames the boxes alone, and floors
|
|
134
|
+
* its padding to whole pixels, so a level without lines fits within a pixel of it.
|
|
135
|
+
*/
|
|
136
|
+
export function fitViewport(boxes: Bounds, lines: Bounds | null, canvas: Canvas): Viewport {
|
|
137
|
+
if (boxes.width === 0 || boxes.height === 0 || canvas.width === 0 || canvas.height === 0){
|
|
138
|
+
return { x: canvas.width / 2 - (boxes.x + boxes.width / 2), y: canvas.height / 2 - (boxes.y + boxes.height / 2), zoom: 1 };
|
|
139
|
+
}
|
|
140
|
+
const frame = framed(boxes, lines, canvas);
|
|
141
|
+
const zoom = zoomFor(frame, canvas);
|
|
142
|
+
return { x: canvas.width / 2 - frame.middle.x * zoom, y: canvas.height / 2 - frame.middle.y * zoom, zoom };
|
|
143
|
+
}
|
|
144
|
+
|
|
42
145
|
/**
|
|
43
146
|
* The viewport a level opens at: React Flow's fit (the same zoom and centring, with FIT_PADDING) when that's readable,
|
|
44
147
|
* otherwise as wide as the canvas allows, from the level's top, never under MIN_READABLE_ZOOM or over MAX_FIT_ZOOM.
|
|
45
148
|
* `topBand` keeps a fitted level below a strip at the top: on a paged level the page pill sits there, and a level
|
|
46
|
-
* centred in the whole canvas could open with its top-right box under the pill (deferred minor).
|
|
47
|
-
*
|
|
149
|
+
* centred in the whole canvas could open with its top-right box under the pill (deferred minor). `lines` is the drawn
|
|
150
|
+
* lines' reach (drawnLinesBounds), framed as Fit frames it while the fit stays readable, and across a level that opens
|
|
151
|
+
* from its top. Pure, so the rule is tested apart from React Flow.
|
|
48
152
|
*/
|
|
49
|
-
export function initialViewport(bounds: Bounds, canvas:
|
|
153
|
+
export function initialViewport(bounds: Bounds, canvas: Canvas, topBand = 0, lines: Bounds | null = null): Viewport {
|
|
50
154
|
const below = canvas.height - topBand;
|
|
51
|
-
const centre = (zoom: number): Viewport => ({
|
|
52
|
-
x: canvas.width / 2 -
|
|
53
|
-
y: topBand + below / 2 -
|
|
155
|
+
const centre = (frame: Frame, zoom: number): Viewport => ({
|
|
156
|
+
x: canvas.width / 2 - frame.middle.x * zoom,
|
|
157
|
+
y: topBand + below / 2 - frame.middle.y * zoom,
|
|
54
158
|
zoom,
|
|
55
159
|
});
|
|
56
|
-
if (bounds.width === 0 || bounds.height === 0 || canvas.width === 0 || canvas.height === 0) return centre(1);
|
|
160
|
+
if (bounds.width === 0 || bounds.height === 0 || canvas.width === 0 || canvas.height === 0) return centre(frameOf(bounds), 1);
|
|
57
161
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
162
|
+
const room = { width: canvas.width, height: below };
|
|
163
|
+
const own = frameOf(bounds);
|
|
164
|
+
const fit = zoomFor(own, room);
|
|
165
|
+
if (fit >= MIN_READABLE_ZOOM){
|
|
166
|
+
// the lines may not take a page that fits readably under the floor: then it keeps the boxes' view
|
|
167
|
+
const frame = framed(bounds, lines, room);
|
|
168
|
+
const zoom = zoomFor(frame, room);
|
|
169
|
+
return zoom >= MIN_READABLE_ZOOM ? centre(frame, zoom) : centre(own, fit);
|
|
170
|
+
}
|
|
64
171
|
|
|
65
172
|
// a tall level reads top to bottom like a page, so its width decides the zoom
|
|
66
|
-
const
|
|
67
|
-
|
|
173
|
+
const wide = (width: number): number => Math.min(MAX_FIT_ZOOM, Math.max(MIN_READABLE_ZOOM, (canvas.width - 2 * SIDE_ROOM) / width));
|
|
174
|
+
let zoom = wide(bounds.width);
|
|
175
|
+
let middle = bounds.x + bounds.width / 2;
|
|
176
|
+
if (lines !== null){
|
|
177
|
+
// the boxes and the lines across, SIDE_ROOM their margin, unless that shrinks the boxes past LINES_FRAME_LEAST
|
|
178
|
+
const left = Math.min(bounds.x, lines.x);
|
|
179
|
+
const right = Math.max(bounds.x + bounds.width, lines.x + lines.width);
|
|
180
|
+
if (wide(right - left) >= LINES_FRAME_LEAST * zoom){
|
|
181
|
+
zoom = wide(right - left);
|
|
182
|
+
middle = (left + right) / 2;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { x: canvas.width / 2 - middle * zoom, y: TOP_INSET - bounds.y * zoom, zoom };
|
|
68
186
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { MapEdge, MapNode } from "@/lib/contracts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What the Map draws bigger when there's more of it (the Workbench redesign, ADR-070): a folder's dot by how many files
|
|
5
|
+
* it holds, a line's width by how many imports it carries, and a box's arrowhead by the heaviest line into it, since
|
|
6
|
+
* its lines share one (boxHeadWidths). Each is a log scale against the largest on the page, so VS Code's src (10,280
|
|
7
|
+
* files) and a folder of 3 both read. Pure, so the scales are tested alone.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A folder's dot, 6 px across for an empty one up to 18 px for the page's largest. */
|
|
11
|
+
export const DOT_MIN = 6;
|
|
12
|
+
export const DOT_MAX = 18;
|
|
13
|
+
|
|
14
|
+
export function dotSize(files: number, maxFiles: number): number {
|
|
15
|
+
if (maxFiles <= 0) return DOT_MIN;
|
|
16
|
+
return DOT_MIN + ((DOT_MAX - DOT_MIN) * Math.log10(Math.max(0, files) + 1)) / Math.log10(maxFiles + 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A line, 1 px for a single import up to 6 px for the page's heaviest. The spec's 10 px made every strong line of a dense
|
|
21
|
+
* level a 7 to 10 px tube, too alike to tell apart (the owner, 2026-10-27, on src/vs).
|
|
22
|
+
*/
|
|
23
|
+
export const LINE_MIN = 1;
|
|
24
|
+
export const LINE_MAX = 6;
|
|
25
|
+
|
|
26
|
+
export function lineWidth(imports: number, maxImports: number): number {
|
|
27
|
+
if (maxImports <= 1 || imports <= 1) return LINE_MIN;
|
|
28
|
+
return LINE_MIN + ((LINE_MAX - LINE_MIN) * Math.log(imports)) / Math.log(maxImports);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A filled arrowhead wider than its line by a margin on each side, so it still stands out on the thickest line (the
|
|
33
|
+
* owner, 2026-10-27: the spec's small open chevron disappeared on thick lines). `half` is half its width, `length` how
|
|
34
|
+
* far it reaches back from its tip, both in px.
|
|
35
|
+
*/
|
|
36
|
+
export function arrowHead(width: number): { half: number; length: number } {
|
|
37
|
+
return { half: Math.max(4.5, 0.9 * width + 3), length: Math.max(7, 1.2 * width + 5) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The largest folder on a page, by files: what its dots are scaled against. Doorways stand for other pages, so they don't count. */
|
|
41
|
+
export const maxFilesOf = (nodes: readonly MapNode[]): number => Math.max(0, ...nodes.filter((n) => n.kind === "folder").map((n) => n.fileCount));
|
|
42
|
+
|
|
43
|
+
/** The heaviest line on a page, by imports. */
|
|
44
|
+
export const maxImportsOf = (edges: readonly MapEdge[]): number => Math.max(0, ...edges.map((e) => e.count));
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The width each box's one arrowhead is sized for (arrowHead), by box. The lines into a box share its head, so it's
|
|
48
|
+
* sized for the heaviest of them on the page, drawn or not, and never grows when a hover draws a heavier one (the
|
|
49
|
+
* owner, 2026-10-28). A line from a box to itself isn't drawn, so it doesn't count.
|
|
50
|
+
*/
|
|
51
|
+
export function boxHeadWidths(edges: readonly MapEdge[]): Map<string, number> {
|
|
52
|
+
const max = maxImportsOf(edges);
|
|
53
|
+
const heads = new Map<string, number>();
|
|
54
|
+
for (const e of edges){
|
|
55
|
+
if (e.source !== e.target) heads.set(e.target, Math.max(heads.get(e.target) ?? 0, lineWidth(e.count, max)));
|
|
56
|
+
}
|
|
57
|
+
return heads;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** How many of the page's heaviest lines carry their count, whatever is selected. */
|
|
61
|
+
export const LABELLED_LINES = 3;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The lines whose count is written on them: the page's three heaviest (ties by source, then target, so the same page
|
|
65
|
+
* always labels the same ones) and every line of the selected box. A line that carries one import goes without, on any
|
|
66
|
+
* page: a busy box's lines were covered in 1s, the thinnest line already says it, and the inspector and a line's hover
|
|
67
|
+
* sentence give it in words (the owner, 2026-10-28). Keys are "source->target", as React Flow's ids. A count is drawn
|
|
68
|
+
* only where it fits clear of everything else (routes.ts's placeCounts).
|
|
69
|
+
*/
|
|
70
|
+
export function labelledLines(edges: readonly MapEdge[], selected: string | null): Set<string> {
|
|
71
|
+
const key = (e: MapEdge): string => `${e.source}->${e.target}`;
|
|
72
|
+
const counted = edges.filter((e) => e.count > 1);
|
|
73
|
+
const heaviest = counted
|
|
74
|
+
.filter((e) => e.source !== e.target)
|
|
75
|
+
.sort((a, b) => b.count - a.count || (a.source < b.source ? -1 : a.source > b.source ? 1 : 0) || (a.target < b.target ? -1 : a.target > b.target ? 1 : 0))
|
|
76
|
+
.slice(0, LABELLED_LINES)
|
|
77
|
+
.map(key);
|
|
78
|
+
const ofSelected = selected === null ? [] : counted.filter((e) => e.source === selected || e.target === selected).map(key);
|
|
79
|
+
return new Set([...heaviest, ...ofSelected]);
|
|
80
|
+
}
|
package/app/page.tsx
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import MapView from "./map/MapView";
|
|
2
|
-
import { MIN_HEIGHT, MIN_WIDTH } from "./map/narrow";
|
|
3
2
|
|
|
4
3
|
export default function Home(){
|
|
5
4
|
// A server component: a flat canvas (VISUAL_REFRESH.md §3). The Map loads its data in the browser, through app/map/api.ts.
|
|
6
|
-
// It fills the window
|
|
7
|
-
//
|
|
8
|
-
// down by the scrollbar's height too.
|
|
5
|
+
// It fills the window at any size (app/map/shell.ts); in a window shorter than its least height it keeps that height
|
|
6
|
+
// and the page scrolls down. Nothing in it is wider than the window, so the page never scrolls sideways.
|
|
9
7
|
return (
|
|
10
|
-
<div data-testid="map-root"
|
|
8
|
+
<div data-testid="map-root" className="relative flex w-full flex-1 flex-col bg-canvas">
|
|
11
9
|
<MapView />
|
|
12
10
|
</div>
|
|
13
11
|
);
|
package/lib/background/child.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { IndexEventMap, SummarizeResponse } from "../contracts";
|
|
|
2
2
|
import type { StoredSummary } from "../db/store/summaries";
|
|
3
3
|
import { errorResponseFor } from "../engine/errors";
|
|
4
4
|
import type { IndexResult } from "../index/pipeline";
|
|
5
|
+
import { writtenInOlderStyle } from "../index/summarize/prompts";
|
|
5
6
|
import type { ChildMessage, ParentMessage } from "./protocol";
|
|
6
7
|
import type { BackgroundService } from "./service";
|
|
7
8
|
|
|
@@ -13,6 +14,7 @@ export function toSummarizeResponse(row: StoredSummary): SummarizeResponse {
|
|
|
13
14
|
...(row.tagline ? { tagline: row.tagline, summary: row.summary ?? "" } : {}),
|
|
14
15
|
summaryStatus: row.status,
|
|
15
16
|
...(row.status === "failed" && row.error ? { summaryError: row.error } : {}),
|
|
17
|
+
...(writtenInOlderStyle(row) ? { summaryOlderStyle: true as const } : {}),
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -12,6 +12,7 @@ import { configuredSummaryModels, SummaryModelsError, type SummaryModelsResoluti
|
|
|
12
12
|
import { DEFAULT_BUDGET_USD, runSummaries, StructureNotReadyError, summarizeNow } from "../index/summarize/run";
|
|
13
13
|
import type { PlannedTarget } from "../index/summarize/plan";
|
|
14
14
|
import { snapshotOf } from "../index/summarize/pricing";
|
|
15
|
+
import { writtenInOlderStyle } from "../index/summarize/prompts";
|
|
15
16
|
import { refreshSummaryStatus } from "../index/summarize/status";
|
|
16
17
|
import type { Summarizer } from "../index/summarize/types";
|
|
17
18
|
import { applyFileChanges } from "../index/update";
|
|
@@ -200,20 +201,24 @@ export function createBackgroundService(deps: BackgroundDeps): BackgroundService
|
|
|
200
201
|
return (repoId === null ? null : approvedBudgetUsd(db, repoId)) ?? DEFAULT_BUDGET_USD;
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
/** The repo's stored summaries by targetKey, read once per job as it starts, so every check the job makes reads the same rows. */
|
|
205
|
+
function storedRows(repo: string): Map<string, StoredSummary> {
|
|
206
|
+
const db = getDb();
|
|
207
|
+
const repoId = findRepository(db, repoKey(repo));
|
|
208
|
+
return new Map(repoId === null ? [] : loadSummaries(db, repoId).map((s) => [targetKey(s.kind, s.path), s]));
|
|
209
|
+
}
|
|
210
|
+
|
|
203
211
|
/**
|
|
204
212
|
* What an automatic job (a refresh after a save, the sweep) may send without asking (ADR-066, R17): a target whose
|
|
205
213
|
* stored text the model now in use for its kind wrote (so it's out of date only because its inputs changed), or one
|
|
206
214
|
* no model has written yet. Text another model wrote waits for `nebulosity summarize`'s estimate and yes, "Summarize
|
|
207
215
|
* now", or a panel being opened: redoing it is a model change, and only a yes pays for that. A dated snapshot the API
|
|
208
216
|
* reported (`<model>-20261001`) counts as its model, and nothing else does: claude-sonnet-5-5 isn't claude-sonnet-5
|
|
209
|
-
* (snapshotOf).
|
|
217
|
+
* (snapshotOf). `rows` are the job's storedRows.
|
|
210
218
|
*/
|
|
211
|
-
function sentUnasked(
|
|
212
|
-
const db = getDb();
|
|
213
|
-
const repoId = findRepository(db, repoKey(repo));
|
|
214
|
-
const writtenBy = new Map(repoId === null ? [] : loadSummaries(db, repoId).map((s) => [targetKey(s.kind, s.path), s.model]));
|
|
219
|
+
function sentUnasked(rows: ReadonlyMap<string, StoredSummary>): (target: PlannedTarget) => boolean {
|
|
215
220
|
return (target) => {
|
|
216
|
-
const model =
|
|
221
|
+
const model = rows.get(targetKey(target.kind, target.path))?.model;
|
|
217
222
|
return !model || model === "template" || model === target.model || snapshotOf(model) === target.model;
|
|
218
223
|
};
|
|
219
224
|
}
|
|
@@ -276,7 +281,7 @@ export function createBackgroundService(deps: BackgroundDeps): BackgroundService
|
|
|
276
281
|
}
|
|
277
282
|
|
|
278
283
|
const wanted = new Set(paths.map((p) => targetKey("file", p)));
|
|
279
|
-
const unasked = sentUnasked(repo);
|
|
284
|
+
const unasked = sentUnasked(storedRows(repo));
|
|
280
285
|
// The content whose quiet minute is up: each file's input hash as this run first plans it. A file saved again
|
|
281
286
|
// mid-refresh (structure updates don't wait for refreshes) plans with a new hash and isn't sent again here: its
|
|
282
287
|
// update restarted its own minute. Otherwise every save during a wave would be paid for at once.
|
|
@@ -318,7 +323,8 @@ export function createBackgroundService(deps: BackgroundDeps): BackgroundService
|
|
|
318
323
|
/**
|
|
319
324
|
* Out-of-date folders whose children are settled, deepest first, at most SWEEP_MAX_FOLDERS (PIVOT_SPEC.md §5.6): only
|
|
320
325
|
* those the model now in use wrote, or that no model has written (sentUnasked). After a folder or repo model change
|
|
321
|
-
* the rest wait for a yes, instead of being redone 20 every 10 minutes with no estimate (R17).
|
|
326
|
+
* the rest wait for a yes, instead of being redone 20 every 10 minutes with no estimate (R17). A folder or the repo
|
|
327
|
+
* an older prompt version wrote waits too, to be opened or redone by `nebulosity summarize` (Q2 B).
|
|
322
328
|
*/
|
|
323
329
|
async function runSweep(): Promise<void> {
|
|
324
330
|
const repo = root;
|
|
@@ -332,18 +338,28 @@ export function createBackgroundService(deps: BackgroundDeps): BackgroundService
|
|
|
332
338
|
return;
|
|
333
339
|
}
|
|
334
340
|
|
|
335
|
-
const
|
|
341
|
+
const rows = storedRows(repo);
|
|
342
|
+
const unasked = sentUnasked(rows);
|
|
343
|
+
const olderStyle = (target: PlannedTarget): boolean => {
|
|
344
|
+
const row = rows.get(targetKey(target.kind, target.path));
|
|
345
|
+
return row !== undefined && writtenInOlderStyle(row);
|
|
346
|
+
};
|
|
336
347
|
try{
|
|
337
348
|
const result = await runSummaries(repo, {
|
|
338
349
|
summarizer: deps.summarizer,
|
|
339
350
|
models,
|
|
340
351
|
transport: "direct",
|
|
341
352
|
refreshStaleFolders: true,
|
|
353
|
+
// such a folder plans lazy, so it never keeps the folder above it waiting (Q2 B, the select below)
|
|
354
|
+
olderFoldersWaitToBeOpened: true,
|
|
342
355
|
wait: false,
|
|
343
356
|
budgetUsd: INCREMENTAL_BUDGET_USD,
|
|
344
357
|
budgetScope: "run",
|
|
345
358
|
repoBudgetUsd: approvedBudget(repo),
|
|
346
|
-
|
|
359
|
+
// Q2 B: a folder or the repo an older prompt wrote is left to be opened, even once its inputs change (an
|
|
360
|
+
// edit below it, or a child a panel restyled), so the sweep never restyles the folders above an edit
|
|
361
|
+
// unasked. The refresh after a save keeps redoing an edited file whatever prompt wrote it (P1).
|
|
362
|
+
select: (target) => target.kind !== "file" && unasked(target) && !olderStyle(target),
|
|
347
363
|
maxTargets: SWEEP_MAX_FOLDERS,
|
|
348
364
|
onProgress: (message) => log(`sweep: ${message}`),
|
|
349
365
|
});
|
package/lib/contracts.ts
CHANGED
|
@@ -132,6 +132,8 @@ export type DescribeResponse = {
|
|
|
132
132
|
summaryStatus: SummaryStatus;
|
|
133
133
|
/** Why the last attempt failed, when summaryStatus is "failed". */
|
|
134
134
|
summaryError?: string;
|
|
135
|
+
/** A model wrote this summary with an older version of the prompt (ADR-034, amended 2026-10-29). It reads in the older style, one block that may count importers, until it's rewritten. */
|
|
136
|
+
summaryOlderStyle?: true;
|
|
135
137
|
/** For a folder or file, the docs linked to it or the folders above it; for the repo, the docs to read first (ADR-054). Left out when none. */
|
|
136
138
|
docs?: DocPointer[];
|
|
137
139
|
stats: { fileCount: number; lineCount: number };
|
|
@@ -189,6 +191,8 @@ export type SummarizeResponse = {
|
|
|
189
191
|
summaryStatus: SummaryStatus;
|
|
190
192
|
/** Why it failed, when summaryStatus is "failed". */
|
|
191
193
|
summaryError?: string;
|
|
194
|
+
/** A model wrote this summary with an older version of the prompt (ADR-034, amended 2026-10-29). It reads in the older style, one block that may count importers, until it's rewritten. */
|
|
195
|
+
summaryOlderStyle?: true;
|
|
192
196
|
};
|
|
193
197
|
|
|
194
198
|
/** Where a repo was cloned from: its origin remote's owner and name ("microsoft" / "vscode") — docs/API_CONTRACTS.md §15 */
|
|
@@ -220,13 +224,32 @@ export type BridgeToMap =
|
|
|
220
224
|
/** A deliberate jump: focus on the path, pushing a history entry. */
|
|
221
225
|
| { type: "nebulosity.show"; path: string }
|
|
222
226
|
| { type: "nebulosity.highlight"; paths: string[] }
|
|
223
|
-
| { type: "nebulosity.clear" }
|
|
227
|
+
| { type: "nebulosity.clear" }
|
|
228
|
+
/**
|
|
229
|
+
* The editor's color theme, sent by the webview itself after the handshake and again whenever the theme changes, so
|
|
230
|
+
* the Map takes its grays from it (the Workbench redesign, ADR-070). Added without a version change: an older Map
|
|
231
|
+
* ignores it, and an older webview never sends it, so the Map keeps its own palette.
|
|
232
|
+
*/
|
|
233
|
+
| { type: "nebulosity.theme"; kind: ThemeKind; colors: BridgeThemeColors };
|
|
234
|
+
|
|
235
|
+
/** Which kind of theme VS Code has on: its webview's `vscode-light`, `vscode-dark` and two high contrast kinds. */
|
|
236
|
+
export type ThemeKind = "dark" | "light" | "hc" | "hc-light";
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A theme's colors as the webview reads them from VS Code's `--vscode-*` variables, raw CSS colors (`#1f1f1f`,
|
|
240
|
+
* `rgba(…)`): editor.background, sideBar.background, a border (the high contrast border, else a widget's, the
|
|
241
|
+
* panel's or the side bar's), foreground, descriptionForeground and focusBorder. A theme may leave out the optional ones.
|
|
242
|
+
*/
|
|
243
|
+
export type BridgeThemeColors = { editor: string; side: string; fg: string; border?: string; desc?: string; focus?: string };
|
|
224
244
|
|
|
225
245
|
/** Messages from the Map to the webview — docs/API_CONTRACTS.md §20. */
|
|
226
246
|
export type BridgeFromMap =
|
|
227
247
|
/** The answer to hello. */
|
|
228
248
|
| { type: "nebulosity.ready"; version: 1 }
|
|
229
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* Open the file in the editor: Open in VS Code (the source viewer's at its highlighted line), a double-clicked file
|
|
251
|
+
* box, or a narrow window's Open beside it. A click on a box only selects it (amended 2026-10-29).
|
|
252
|
+
*/
|
|
230
253
|
| { type: "nebulosity.open"; path: string; line?: number }
|
|
231
254
|
/** A new level was drawn, the selection changed on the level drawn, or a `ready` was answered while a level is on screen (the extension's test and benchmarks read it). */
|
|
232
255
|
| { type: "nebulosity.drawn"; path: string; page: number; selected: string | null };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `summaries` ADD `prompt_version` integer;
|