santree 0.5.6 → 0.6.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/dist/commands/dashboard.js +210 -33
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/helpers/squirrel.d.ts +2 -0
- package/dist/commands/helpers/squirrel.js +12 -0
- package/dist/commands/worktree/commit.d.ts +9 -1
- package/dist/commands/worktree/commit.js +58 -14
- package/dist/lib/ai.d.ts +26 -0
- package/dist/lib/ai.js +53 -0
- package/dist/lib/claude-todos.d.ts +37 -0
- package/dist/lib/claude-todos.js +98 -0
- package/dist/lib/dashboard/DetailPanel.js +99 -9
- package/dist/lib/dashboard/IssueList.js +2 -0
- package/dist/lib/dashboard/MultilineTextArea.js +14 -1
- package/dist/lib/dashboard/Overlays.d.ts +5 -0
- package/dist/lib/dashboard/Overlays.js +75 -2
- package/dist/lib/dashboard/ReviewDetailPanel.d.ts +7 -0
- package/dist/lib/dashboard/ReviewDetailPanel.js +269 -77
- package/dist/lib/dashboard/ReviewList.js +12 -15
- package/dist/lib/dashboard/data.js +158 -7
- package/dist/lib/dashboard/types.d.ts +45 -5
- package/dist/lib/dashboard/types.js +40 -0
- package/dist/lib/diff-parse.d.ts +25 -0
- package/dist/lib/diff-parse.js +60 -0
- package/dist/lib/git.d.ts +22 -0
- package/dist/lib/git.js +41 -0
- package/dist/lib/github.d.ts +6 -0
- package/dist/lib/github.js +29 -0
- package/dist/lib/open-url.d.ts +10 -0
- package/dist/lib/open-url.js +20 -0
- package/dist/lib/squirrel-loader.d.ts +9 -0
- package/dist/lib/squirrel-loader.js +322 -0
- package/dist/lib/trackers/index.d.ts +13 -0
- package/dist/lib/trackers/index.js +19 -0
- package/package.json +1 -1
- package/prompts/fill-commit.njk +79 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Open a URL in the platform's default browser.
|
|
4
|
+
* macOS → `open <url>`
|
|
5
|
+
* else → `xdg-open <url>`
|
|
6
|
+
*
|
|
7
|
+
* Returns true on apparent success, false on failure. Callers decide how to
|
|
8
|
+
* surface failures (e.g. a dashboard action message). Uses execSync with
|
|
9
|
+
* `stdio: "ignore"` — fast, no output leak into the dashboard's alt screen.
|
|
10
|
+
*/
|
|
11
|
+
export function openUrl(url) {
|
|
12
|
+
try {
|
|
13
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
14
|
+
execSync(`${cmd} "${url}"`, { stdio: "ignore" });
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
text?: string;
|
|
3
|
+
}
|
|
4
|
+
/** Animated 3D squirrel for loading states. SDF ray-marched at module
|
|
5
|
+
* load with surface-normal lighting, soft shadows, AO, and a rainbow
|
|
6
|
+
* Y-gradient. Body spin + tail wag + breathing/sniff/ear-flick layers
|
|
7
|
+
* combine over independent phases. */
|
|
8
|
+
export default function SquirrelLoader({ text }: Props): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// 3D rendered squirrel via SDF + sphere tracing.
|
|
6
|
+
//
|
|
7
|
+
// Anatomy is composed of ellipsoid/sphere/capsule primitives blended with
|
|
8
|
+
// smooth-min so the silhouette looks organic. Per-pixel ray-marching
|
|
9
|
+
// finds the surface; surface normals are finite-differenced. Lighting
|
|
10
|
+
// combines Lambertian diffuse, soft shadows (Inigo Quilez technique),
|
|
11
|
+
// and ambient occlusion. Brightness picks a glyph from a luminance ramp;
|
|
12
|
+
// world-Y position picks a rainbow color band (red top → blue bottom)
|
|
13
|
+
// with shadow/lit two-tone variants.
|
|
14
|
+
//
|
|
15
|
+
// Animation runs on three independent phases per loop (so nothing reads
|
|
16
|
+
// as a single synchronised pendulum):
|
|
17
|
+
// - body angle: 1 full Y-rotation (every face visible)
|
|
18
|
+
// - tail phase: 3 wag cycles
|
|
19
|
+
// - anim phase: 1 cycle, with BodyAnim curves using internal multiples
|
|
20
|
+
// (×5 for ear flicks, ×2 for acorn jiggle) for per-part timing
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
22
|
+
const W = 100;
|
|
23
|
+
const H = 40;
|
|
24
|
+
const FRAME_COUNT = 36;
|
|
25
|
+
const FRAME_MS = 80;
|
|
26
|
+
const RAMP = ".,-~:;=!*#$@";
|
|
27
|
+
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
28
|
+
const addV = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
29
|
+
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
30
|
+
const vlen = (a) => Math.hypot(a[0], a[1], a[2]);
|
|
31
|
+
const vscale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
|
|
32
|
+
function vnorm(a) {
|
|
33
|
+
const l = vlen(a) || 1;
|
|
34
|
+
return [a[0] / l, a[1] / l, a[2] / l];
|
|
35
|
+
}
|
|
36
|
+
// Rotate around Y axis. `c`/`s` are precomputed cos/sin so callers can
|
|
37
|
+
// share them across many evals at the same angle.
|
|
38
|
+
const rotY = (p, c, s) => [c * p[0] + s * p[2], p[1], -s * p[0] + c * p[2]];
|
|
39
|
+
// ── SDF primitives ──────────────────────────────────────────────────
|
|
40
|
+
function sdEllipsoid(p, r) {
|
|
41
|
+
const k0 = Math.hypot(p[0] / r[0], p[1] / r[1], p[2] / r[2]);
|
|
42
|
+
const k1 = Math.hypot(p[0] / (r[0] * r[0]), p[1] / (r[1] * r[1]), p[2] / (r[2] * r[2]));
|
|
43
|
+
return (k0 * (k0 - 1)) / Math.max(0.0001, k1);
|
|
44
|
+
}
|
|
45
|
+
const sdSphere = (p, r) => vlen(p) - r;
|
|
46
|
+
function sdCapsule(p, a, b, r) {
|
|
47
|
+
const pa = sub(p, a);
|
|
48
|
+
const ba = sub(b, a);
|
|
49
|
+
const h = Math.max(0, Math.min(1, dot(pa, ba) / dot(ba, ba)));
|
|
50
|
+
return vlen(sub(pa, vscale(ba, h))) - r;
|
|
51
|
+
}
|
|
52
|
+
function smin(a, b, k) {
|
|
53
|
+
const h = Math.max(0, k - Math.abs(a - b)) / k;
|
|
54
|
+
return Math.min(a, b) - h * h * k * 0.25;
|
|
55
|
+
}
|
|
56
|
+
// ── Animation curves ────────────────────────────────────────────────
|
|
57
|
+
// Tail joint base positions. Peak (j5) is at y=2.7, level with the ear
|
|
58
|
+
// tips — tail no longer towers over the silhouette. Tip is well to the
|
|
59
|
+
// left (x=-1.1) and behind (z=0.0) so it stays clear of the head/ears.
|
|
60
|
+
const TAIL_BASES = [
|
|
61
|
+
[-1.4, -0.4, -0.4], // anchor at rump
|
|
62
|
+
[-1.9, 0.3, -0.6],
|
|
63
|
+
[-2.2, 1.1, -0.6],
|
|
64
|
+
[-2.3, 1.9, -0.4], // peak of outward arc
|
|
65
|
+
[-1.9, 2.5, -0.2],
|
|
66
|
+
[-1.1, 2.7, 0.0], // tip — at ear height, behind & left of head
|
|
67
|
+
];
|
|
68
|
+
// Per-frame tail joints: amplitude scales with joint index (rump stays
|
|
69
|
+
// anchored, tip flicks). Phase shift per joint creates a travelling
|
|
70
|
+
// wave instead of a synchronised wag.
|
|
71
|
+
function computeTailJoints(phase) {
|
|
72
|
+
return TAIL_BASES.map((b, i) => {
|
|
73
|
+
const amp = i / 5;
|
|
74
|
+
const wagX = Math.sin(phase + i * 0.6) * 0.7 * amp;
|
|
75
|
+
const liftY = Math.cos(phase * 0.9 + i * 0.4) * 0.5 * amp;
|
|
76
|
+
return [b[0] + wagX, b[1] + liftY, b[2]];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function computeBodyAnim(phase) {
|
|
80
|
+
const bobY = Math.sin(phase) * 0.35;
|
|
81
|
+
return {
|
|
82
|
+
bodyBobY: bobY,
|
|
83
|
+
stretchY: bobY * 0.4,
|
|
84
|
+
headExtraY: Math.sin(phase * 1.6 + 0.5) * 0.18,
|
|
85
|
+
headTurnAngle: Math.sin(phase * 0.7) * 0.45,
|
|
86
|
+
headLeanZ: Math.sin(phase * 1.3 + 1.0) * 0.18,
|
|
87
|
+
earLExtraY: Math.max(0, Math.sin(phase * 5)) * 0.35,
|
|
88
|
+
earRExtraY: Math.max(0, Math.sin(phase * 5 + 1.7)) * 0.35,
|
|
89
|
+
snoutWiggleZ: Math.sin(phase * 4) * 0.1,
|
|
90
|
+
pawForwardZ: Math.max(0, Math.sin(phase * 3 - 0.5)) * 0.15,
|
|
91
|
+
acornBobY: Math.sin(phase * 2 + 1) * 0.15,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Tail capsule radii from base (rump) to tip. Defined alongside the
|
|
95
|
+
// joint positions so anyone tweaking shape sees both at once.
|
|
96
|
+
const TAIL_RADII = [0.5, 0.6, 0.65, 0.6, 0.45];
|
|
97
|
+
function tailSDF(p, tj) {
|
|
98
|
+
const t1 = sdCapsule(p, tj[0], tj[1], TAIL_RADII[0]);
|
|
99
|
+
const t2 = sdCapsule(p, tj[1], tj[2], TAIL_RADII[1]);
|
|
100
|
+
const t3 = sdCapsule(p, tj[2], tj[3], TAIL_RADII[2]);
|
|
101
|
+
const t4 = sdCapsule(p, tj[3], tj[4], TAIL_RADII[3]);
|
|
102
|
+
const t5 = sdCapsule(p, tj[4], tj[5], TAIL_RADII[4]);
|
|
103
|
+
// Internal smooth-min keeps the tail one continuous bushy curve.
|
|
104
|
+
return smin(smin(smin(t1, t2, 0.3), smin(t3, t4, 0.3), 0.3), t5, 0.3);
|
|
105
|
+
}
|
|
106
|
+
// SDF evaluated in body-local space (point already rotated by camera
|
|
107
|
+
// spin). Returns signed distance to the squirrel surface.
|
|
108
|
+
function squirrelLocal(p, state) {
|
|
109
|
+
const { tj, anim } = state;
|
|
110
|
+
// Whole-body bob: shift the eval point down by bodyBobY, equivalent
|
|
111
|
+
// to moving the body up by bodyBobY. The tail uses ps too so it
|
|
112
|
+
// bobs with the body.
|
|
113
|
+
const ps = [p[0], p[1] - anim.bodyBobY, p[2]];
|
|
114
|
+
// Volume-preserving squash & stretch: Y grows when stretchY > 0,
|
|
115
|
+
// X/Z shrink slightly to compensate.
|
|
116
|
+
const sy = 1 + anim.stretchY;
|
|
117
|
+
const sxz = 1 - anim.stretchY * 0.5;
|
|
118
|
+
// Pear-shape body: wider haunches, narrower chest.
|
|
119
|
+
const haunches = sdEllipsoid(sub(ps, [0, -0.3, 0]), [1.2 * sxz, 1.0 * sy, 1.0 * sxz]);
|
|
120
|
+
const chest = sdEllipsoid(sub(ps, [0, 0.7, 0.1]), [0.9 * sxz, 0.9 * sy, 0.85 * sxz]);
|
|
121
|
+
const body = smin(haunches, chest, 0.3);
|
|
122
|
+
// Head primitives evaluated in a head-local frame: rotate the eval
|
|
123
|
+
// point around a neck pivot by -headTurnAngle so the head appears
|
|
124
|
+
// to look L/R while the body stays straight.
|
|
125
|
+
const headPivot = [0, 1.4, 0.2];
|
|
126
|
+
const cosTurn = Math.cos(anim.headTurnAngle);
|
|
127
|
+
const sinTurn = Math.sin(anim.headTurnAngle);
|
|
128
|
+
const psHead = addV(rotY(sub(ps, headPivot), cosTurn, sinTurn), headPivot);
|
|
129
|
+
const lean = anim.headLeanZ;
|
|
130
|
+
const head = sdEllipsoid(sub(psHead, [0, 1.85 + anim.headExtraY, 0.4 + lean]), [0.7, 0.65, 0.7]);
|
|
131
|
+
const snout = sdEllipsoid(sub(psHead, [0, 1.65 + anim.headExtraY, 1.0 + lean + anim.snoutWiggleZ]), [0.32, 0.28, 0.4]);
|
|
132
|
+
const earL = sdEllipsoid(sub(psHead, [-0.45, 2.7 + anim.earLExtraY, 0.25 + lean]), [0.16, 0.45, 0.18]);
|
|
133
|
+
const earR = sdEllipsoid(sub(psHead, [0.45, 2.7 + anim.earRExtraY, 0.25 + lean]), [0.16, 0.45, 0.18]);
|
|
134
|
+
// Paws + acorn shift forward together (nibble lunge).
|
|
135
|
+
const pe = anim.pawForwardZ;
|
|
136
|
+
const pawL = sdSphere(sub(ps, [-0.45, 0.7, 0.95 + pe]), 0.3);
|
|
137
|
+
const pawR = sdSphere(sub(ps, [0.45, 0.7, 0.95 + pe]), 0.3);
|
|
138
|
+
const acorn = sdEllipsoid(sub(ps, [0, 0.55 + anim.acornBobY, 1.15 + pe]), [0.22, 0.28, 0.22]);
|
|
139
|
+
const tail = tailSDF(ps, tj);
|
|
140
|
+
let d = body;
|
|
141
|
+
d = smin(d, head, 0.25);
|
|
142
|
+
d = smin(d, snout, 0.2);
|
|
143
|
+
d = Math.min(d, earL); // hard union for ears (keeps them pointy)
|
|
144
|
+
d = Math.min(d, earR);
|
|
145
|
+
d = smin(d, pawL, 0.2);
|
|
146
|
+
d = smin(d, pawR, 0.2);
|
|
147
|
+
d = Math.min(d, acorn);
|
|
148
|
+
d = Math.min(d, tail); // hard union: tail is its own visible silhouette
|
|
149
|
+
return d;
|
|
150
|
+
}
|
|
151
|
+
// SDF at a world-space point: rotates into body-local frame, evaluates.
|
|
152
|
+
function evalAt(p, state) {
|
|
153
|
+
return squirrelLocal(rotY(p, state.cosA, state.sinA), state);
|
|
154
|
+
}
|
|
155
|
+
function calcNormal(p, state) {
|
|
156
|
+
const e = 0.01;
|
|
157
|
+
return vnorm([
|
|
158
|
+
evalAt(addV(p, [e, 0, 0]), state) - evalAt(addV(p, [-e, 0, 0]), state),
|
|
159
|
+
evalAt(addV(p, [0, e, 0]), state) - evalAt(addV(p, [0, -e, 0]), state),
|
|
160
|
+
evalAt(addV(p, [0, 0, e]), state) - evalAt(addV(p, [0, 0, -e]), state),
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
// Soft shadow via light-direction march (Inigo Quilez technique). The
|
|
164
|
+
// closest miss along the ray gives a partial shadow; the k=10 multiplier
|
|
165
|
+
// controls penumbra softness.
|
|
166
|
+
function softShadow(origin, dir, state) {
|
|
167
|
+
let res = 1.0;
|
|
168
|
+
let t = 0.05; // step off the surface to avoid self-occlusion
|
|
169
|
+
for (let step = 0; step < 20; step++) {
|
|
170
|
+
const sp = [origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t];
|
|
171
|
+
const d = evalAt(sp, state);
|
|
172
|
+
if (d < 0.001)
|
|
173
|
+
return 0;
|
|
174
|
+
res = Math.min(res, (10 * d) / t);
|
|
175
|
+
t += d;
|
|
176
|
+
if (t > 8)
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
return Math.max(0, Math.min(1, res));
|
|
180
|
+
}
|
|
181
|
+
// Cheap ambient occlusion: sample SDF along the surface normal at
|
|
182
|
+
// increasing distances. Concave creases (ear bases, where tail meets
|
|
183
|
+
// body, under the chin) get darker because nearby geometry blocks the
|
|
184
|
+
// ambient light.
|
|
185
|
+
function ao(p, n, state) {
|
|
186
|
+
let occ = 0;
|
|
187
|
+
let sca = 1.0;
|
|
188
|
+
for (let i = 0; i < 5; i++) {
|
|
189
|
+
const h = 0.04 + 0.12 * i;
|
|
190
|
+
const sp = [p[0] + n[0] * h, p[1] + n[1] * h, p[2] + n[2] * h];
|
|
191
|
+
const d = evalAt(sp, state);
|
|
192
|
+
occ += (h - d) * sca;
|
|
193
|
+
sca *= 0.85;
|
|
194
|
+
}
|
|
195
|
+
return Math.max(0, Math.min(1, 1.0 - 0.8 * occ));
|
|
196
|
+
}
|
|
197
|
+
// ── Frame construction ──────────────────────────────────────────────
|
|
198
|
+
const LIGHT = vnorm([0.5, 0.6, -0.6]);
|
|
199
|
+
// Viewport in world space (orthographic). The squirrel spans roughly
|
|
200
|
+
// y=-1 (paws) to y=4.5 (ear tips), x=-2.5 (tail) to x=1.5 (snout).
|
|
201
|
+
const VIEW_X_MIN = -3.5;
|
|
202
|
+
const VIEW_X_MAX = 3.5;
|
|
203
|
+
const VIEW_Y_MID = 1.75;
|
|
204
|
+
const VIEW_Y_HALF = 2.75;
|
|
205
|
+
const RAY_MAX_T = 12;
|
|
206
|
+
const RAY_MAX_STEPS = 60;
|
|
207
|
+
const HIT_EPS = 0.01;
|
|
208
|
+
// Ambient floor keeps shadow regions clearly visible. A floor of 0.12
|
|
209
|
+
// produced internal "holes" in the silhouette that broke the shape.
|
|
210
|
+
const AMBIENT = 0.35;
|
|
211
|
+
const EMPTY_CELL = { ch: " ", y: 0, lit: 0 };
|
|
212
|
+
function buildFrame(angle, tailPhase, animPhase) {
|
|
213
|
+
const state = {
|
|
214
|
+
cosA: Math.cos(angle),
|
|
215
|
+
sinA: Math.sin(angle),
|
|
216
|
+
tj: computeTailJoints(tailPhase),
|
|
217
|
+
anim: computeBodyAnim(animPhase),
|
|
218
|
+
};
|
|
219
|
+
const grid = Array.from({ length: H }, () => Array.from({ length: W }, () => ({ ...EMPTY_CELL })));
|
|
220
|
+
for (let py = 0; py < H; py++) {
|
|
221
|
+
for (let px = 0; px < W; px++) {
|
|
222
|
+
const xw = VIEW_X_MIN + (px / (W - 1)) * (VIEW_X_MAX - VIEW_X_MIN);
|
|
223
|
+
const yw = VIEW_Y_MID + (1 - (py / (H - 1)) * 2) * VIEW_Y_HALF;
|
|
224
|
+
// Sphere-trace from in front of the model toward +Z (orthographic).
|
|
225
|
+
let t = 0;
|
|
226
|
+
let p = [xw, yw, -5];
|
|
227
|
+
let hit = false;
|
|
228
|
+
for (let step = 0; step < RAY_MAX_STEPS; step++) {
|
|
229
|
+
p = [xw, yw, -5 + t];
|
|
230
|
+
const d = evalAt(p, state);
|
|
231
|
+
if (d < HIT_EPS) {
|
|
232
|
+
hit = true;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (t > RAY_MAX_T)
|
|
236
|
+
break;
|
|
237
|
+
t += d;
|
|
238
|
+
}
|
|
239
|
+
if (!hit)
|
|
240
|
+
continue;
|
|
241
|
+
const n = calcNormal(p, state);
|
|
242
|
+
const diffuse = Math.max(0, dot(n, LIGHT));
|
|
243
|
+
// Offset shadow ray origin along the normal so we don't
|
|
244
|
+
// immediately re-hit the surface we just landed on.
|
|
245
|
+
const shadowOrigin = [p[0] + n[0] * 0.03, p[1] + n[1] * 0.03, p[2] + n[2] * 0.03];
|
|
246
|
+
const sh = softShadow(shadowOrigin, LIGHT, state);
|
|
247
|
+
const occ = ao(p, n, state);
|
|
248
|
+
const lit = Math.min(1, AMBIENT + (1 - AMBIENT) * diffuse * sh * occ);
|
|
249
|
+
const idx = Math.min(RAMP.length - 1, Math.floor(lit * 1.3 * RAMP.length));
|
|
250
|
+
// World Y = p[1] (rotY only rotates around Y, so Y is preserved).
|
|
251
|
+
grid[py][px] = { ch: RAMP[idx], y: p[1], lit };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return grid;
|
|
255
|
+
}
|
|
256
|
+
// Pre-compute every frame at module load; render path stays at zero
|
|
257
|
+
// math per tick.
|
|
258
|
+
const FRAMES = (() => {
|
|
259
|
+
const out = [];
|
|
260
|
+
for (let i = 0; i < FRAME_COUNT; i++) {
|
|
261
|
+
const t = i / FRAME_COUNT;
|
|
262
|
+
const angle = t * Math.PI * 2;
|
|
263
|
+
const tailPhase = t * Math.PI * 6; // 3 wag cycles per loop
|
|
264
|
+
const animPhase = t * Math.PI * 2;
|
|
265
|
+
out.push(buildFrame(angle, tailPhase, animPhase));
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
})();
|
|
269
|
+
// ── Color rendering ─────────────────────────────────────────────────
|
|
270
|
+
// Each band has lit/shadow variants of the same hue. Bundling them
|
|
271
|
+
// into one array means adding a band is a single-line change.
|
|
272
|
+
const COLOR_BANDS = [
|
|
273
|
+
{ lit: "redBright", shadow: "red" }, // top of head/ears
|
|
274
|
+
{ lit: "yellowBright", shadow: "yellow" },
|
|
275
|
+
{ lit: "yellowBright", shadow: "yellow" },
|
|
276
|
+
{ lit: "greenBright", shadow: "green" },
|
|
277
|
+
{ lit: "cyanBright", shadow: "cyan" },
|
|
278
|
+
{ lit: "cyanBright", shadow: "cyan" },
|
|
279
|
+
{ lit: "blueBright", shadow: "blue" }, // bottom (paws/feet)
|
|
280
|
+
];
|
|
281
|
+
// World Y range mapped onto bands. Squirrel spans roughly y=-1..3.5.
|
|
282
|
+
const COLOR_Y_MIN = -1;
|
|
283
|
+
const COLOR_Y_RANGE = 4.5;
|
|
284
|
+
const SHADOW_THRESHOLD = 0.5;
|
|
285
|
+
function colorForCell(y, lit) {
|
|
286
|
+
const t = Math.max(0, Math.min(0.999, (y - COLOR_Y_MIN) / COLOR_Y_RANGE));
|
|
287
|
+
const idx = Math.min(COLOR_BANDS.length - 1, Math.max(0, Math.floor((1 - t) * COLOR_BANDS.length)));
|
|
288
|
+
const band = COLOR_BANDS[idx];
|
|
289
|
+
return lit < SHADOW_THRESHOLD ? band.shadow : band.lit;
|
|
290
|
+
}
|
|
291
|
+
/** Animated 3D squirrel for loading states. SDF ray-marched at module
|
|
292
|
+
* load with surface-normal lighting, soft shadows, AO, and a rainbow
|
|
293
|
+
* Y-gradient. Body spin + tail wag + breathing/sniff/ear-flick layers
|
|
294
|
+
* combine over independent phases. */
|
|
295
|
+
export default function SquirrelLoader({ text }) {
|
|
296
|
+
const [frame, setFrame] = useState(0);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
const id = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), FRAME_MS);
|
|
299
|
+
return () => clearInterval(id);
|
|
300
|
+
}, []);
|
|
301
|
+
// Coalesce contiguous same-color cells into single Text spans —
|
|
302
|
+
// without this we'd hand Ink ~4000 nodes per frame and stutter.
|
|
303
|
+
const rows = useMemo(() => {
|
|
304
|
+
const grid = FRAMES[frame];
|
|
305
|
+
return grid.map((row) => {
|
|
306
|
+
const spans = [];
|
|
307
|
+
let cur = null;
|
|
308
|
+
for (const cell of row) {
|
|
309
|
+
const color = cell.ch === " " ? undefined : colorForCell(cell.y, cell.lit);
|
|
310
|
+
if (!cur || cur.color !== color) {
|
|
311
|
+
cur = { text: cell.ch, color };
|
|
312
|
+
spans.push(cur);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
cur.text += cell.ch;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return spans;
|
|
319
|
+
});
|
|
320
|
+
}, [frame]);
|
|
321
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [rows.map((spans, i) => (_jsx(Box, { children: _jsx(Text, { children: spans.map((s, j) => (_jsx(Text, { color: s.color, children: s.text }, j))) }) }, i))), text && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: text }) }))] }));
|
|
322
|
+
}
|
|
@@ -10,3 +10,16 @@ export { setRepoTracker, removeRepoTracker, readTrackerConfig } from "./config.j
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function getIssueTracker(repoRoot: string | null): IssueTracker;
|
|
12
12
|
export declare function getActiveTrackerKind(repoRoot: string | null): IssueTrackerKind;
|
|
13
|
+
/**
|
|
14
|
+
* Trackers worth trying when resolving a ticket from a foreign PR branch.
|
|
15
|
+
*
|
|
16
|
+
* The active tracker is always first. When the active tracker is GitHub but
|
|
17
|
+
* the user has stored Linear credentials, Linear is appended as a fallback —
|
|
18
|
+
* the typical reviewer scenario is a santree-managed repo (active tracker:
|
|
19
|
+
* GitHub) reviewing PRs from a Linear-driven team where branches encode
|
|
20
|
+
* `TEAM-1234`-style IDs that GitHub's parser ignores.
|
|
21
|
+
*
|
|
22
|
+
* Only used by features that look at OTHER people's branches (like the
|
|
23
|
+
* reviews tab). Per-repo flows still use the active tracker exclusively.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getCandidateTrackers(repoRoot: string | null): IssueTracker[];
|
|
@@ -32,3 +32,22 @@ export function getIssueTracker(repoRoot) {
|
|
|
32
32
|
export function getActiveTrackerKind(repoRoot) {
|
|
33
33
|
return getIssueTracker(repoRoot).kind;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Trackers worth trying when resolving a ticket from a foreign PR branch.
|
|
37
|
+
*
|
|
38
|
+
* The active tracker is always first. When the active tracker is GitHub but
|
|
39
|
+
* the user has stored Linear credentials, Linear is appended as a fallback —
|
|
40
|
+
* the typical reviewer scenario is a santree-managed repo (active tracker:
|
|
41
|
+
* GitHub) reviewing PRs from a Linear-driven team where branches encode
|
|
42
|
+
* `TEAM-1234`-style IDs that GitHub's parser ignores.
|
|
43
|
+
*
|
|
44
|
+
* Only used by features that look at OTHER people's branches (like the
|
|
45
|
+
* reviews tab). Per-repo flows still use the active tracker exclusively.
|
|
46
|
+
*/
|
|
47
|
+
export function getCandidateTrackers(repoRoot) {
|
|
48
|
+
const active = getIssueTracker(repoRoot);
|
|
49
|
+
if (active.kind === "github" && Object.keys(readLinearAuthStore()).length > 0) {
|
|
50
|
+
return [active, linearTracker];
|
|
51
|
+
}
|
|
52
|
+
return [active];
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
You are writing a Git commit message. Output ONLY the message string itself — no preamble, no quotes, no markdown, no explanation.
|
|
2
|
+
|
|
3
|
+
# Format rules
|
|
4
|
+
|
|
5
|
+
The message is **a single line** — no newlines, no body paragraphs.
|
|
6
|
+
|
|
7
|
+
If the diff covers multiple changes, list each one as a brief phrase joined by a `+`. Each phrase is one short imperative clause — never a sentence.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
[ABC-1] add login throttling + fix token refresh race + rename UserService
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If the diff is one change, write one phrase, no separator.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
[ABC-1] add login throttling
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
# Per-phrase rules (apply to every clause in the message)
|
|
20
|
+
|
|
21
|
+
- Imperative mood: "add", "fix", "update", "rename", "remove" — NEVER "added", "fixing", "updates".
|
|
22
|
+
- Lowercase first letter (after any [TICKET] prefix).
|
|
23
|
+
- Concise — describe the change in 3–8 words. No "and that does X also" hedging.
|
|
24
|
+
- No trailing period. No wrapping quotes.
|
|
25
|
+
{% if ticket_id %}- The whole message is prefixed once: `[{{ ticket_id }}] ` (square brackets, single space). The prefix is not repeated per clause.{% endif %}
|
|
26
|
+
|
|
27
|
+
# What to include vs drop
|
|
28
|
+
|
|
29
|
+
Include user-facing changes — new commands, new keybindings, new UI surfaces, user-visible bugfixes, behaviour changes a user would notice.
|
|
30
|
+
|
|
31
|
+
Drop or merge into a parent phrase:
|
|
32
|
+
- Pure type/interface refactors that exist only to support an included feature
|
|
33
|
+
- Internal helper extractions (e.g. "extract X into util") unless they ARE the entire commit
|
|
34
|
+
- Prompt tweaks that go with a feature you already mentioned
|
|
35
|
+
- Dependency bumps from `npm install` triggered by an included feature
|
|
36
|
+
|
|
37
|
+
If the diff is genuinely 100% internal (refactor commit, no user-facing change), write one phrase about the structural change.
|
|
38
|
+
|
|
39
|
+
# Good examples
|
|
40
|
+
|
|
41
|
+
{% if ticket_id %}[{{ ticket_id }}] add dashboard help overlay + show main repo row + draft commit messages with claude
|
|
42
|
+
[{{ ticket_id }}] fix tail rendering in squirrel loader + speed up frame generation
|
|
43
|
+
[{{ ticket_id }}] rename UserService to AccountService{% else %}add dashboard help overlay + show main repo row + draft commit messages with claude
|
|
44
|
+
fix tail rendering in squirrel loader + speed up frame generation
|
|
45
|
+
rename UserService to AccountService{% endif %}
|
|
46
|
+
|
|
47
|
+
# Bad examples (do NOT do this)
|
|
48
|
+
|
|
49
|
+
- `Added the help overlay.` ← past tense + period
|
|
50
|
+
- `add the help overlay and also fix the squirrel tail` ← prose-y "and" + "also"; use `+`
|
|
51
|
+
- `add help overlay\nfix tail rendering` ← multi-line; must be a single line
|
|
52
|
+
- `"add help overlay"` ← wrapped in quotes
|
|
53
|
+
- `add help overlay, fix tail, refactor types, bump deps` ← drop "refactor types" and "bump deps" if they support a listed feature
|
|
54
|
+
|
|
55
|
+
# Context
|
|
56
|
+
|
|
57
|
+
Branch: {{ branch_name }}
|
|
58
|
+
{% if ticket_id %}Ticket: {{ ticket_id }}{% endif %}
|
|
59
|
+
{% if ticket_content %}
|
|
60
|
+
{{ ticket_content }}
|
|
61
|
+
{% endif %}
|
|
62
|
+
|
|
63
|
+
# Staged diff
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
{{ diff_content }}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
# Final self-check
|
|
70
|
+
|
|
71
|
+
1. Is the message a single line (no newlines)?
|
|
72
|
+
2. Does each clause start with an imperative verb (add/fix/update/…)?
|
|
73
|
+
3. Are clauses separated by ` + ` (with spaces around the plus)?
|
|
74
|
+
4. Have I dropped clauses that just support another included clause?
|
|
75
|
+
5. Is each clause concise (under ~8 words)?
|
|
76
|
+
|
|
77
|
+
# Output
|
|
78
|
+
|
|
79
|
+
Output ONLY the final commit message string. Nothing before, nothing after.
|