whipped 0.1.0 → 0.1.2

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.
@@ -27169,389 +27169,953 @@ const ZodIssueCode = {
27169
27169
  function number$1(params) {
27170
27170
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
27171
27171
  }
27172
- const addProjectSchema = object({
27173
- repoPath: string$2().min(1, "Repository path is required"),
27174
- deliveryMode: _enum(["off", "pr", "yolo"]),
27175
- installCommand: string$2().optional()
27172
+ const runtimeAgentIdSchema = _enum(["claude", "codex", "opencode", "cursor"]);
27173
+ const AGENT_BINARY_OPTIONS = [
27174
+ { value: "claude", label: "claude" },
27175
+ { value: "codex", label: "codex" },
27176
+ { value: "opencode", label: "opencode" },
27177
+ { value: "cursor", label: "cursor" }
27178
+ ];
27179
+ const effortLevelSchema = _enum(["low", "medium", "high", "xhigh", "max"]);
27180
+ const EFFORT_OPTIONS = [
27181
+ { value: "low", label: "Low" },
27182
+ { value: "medium", label: "Medium" },
27183
+ { value: "high", label: "High" },
27184
+ { value: "xhigh", label: "Extra High" },
27185
+ { value: "max", label: "Max" }
27186
+ ];
27187
+ const MODEL_OPTIONS = {
27188
+ claude: [
27189
+ { value: "claude-opus-4-8", label: "Opus 4.8" },
27190
+ { value: "claude-opus-4-7", label: "Opus 4.7" },
27191
+ { value: "claude-opus-4-6", label: "Opus 4.6" },
27192
+ { value: "claude-sonnet-4-6", label: "Sonnet 4.6" },
27193
+ { value: "claude-sonnet-4-5", label: "Sonnet 4.5" },
27194
+ { value: "claude-haiku-4-5", label: "Haiku 4.5" }
27195
+ ],
27196
+ codex: [
27197
+ { value: "gpt-5.5", label: "GPT-5.5 (default)" },
27198
+ { value: "gpt-5.4", label: "GPT-5.4" },
27199
+ { value: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
27200
+ { value: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
27201
+ { value: "gpt-5.2", label: "GPT-5.2" }
27202
+ ],
27203
+ // opencode supports any provider/model string — no fixed presets.
27204
+ // The UI renders a free-form text input for opencode model selection.
27205
+ opencode: [],
27206
+ // cursor supports many models — no fixed presets.
27207
+ // The UI fetches the live list via agents.cursorModels and renders a Select.
27208
+ cursor: []
27209
+ };
27210
+ const agentModelChoiceSchema = object({
27211
+ agentId: runtimeAgentIdSchema.default("claude"),
27212
+ model: string$2().nullable().optional(),
27213
+ effort: effortLevelSchema.nullable().optional()
27176
27214
  });
27177
- function stringifyQuery(query) {
27178
- const parts = [];
27179
- for (const [key, value] of Object.entries(query)) {
27180
- if (value === void 0 || value === null || value === "") {
27181
- continue;
27182
- }
27183
- parts.push(
27184
- `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
27185
- );
27186
- }
27187
- return parts.join("&");
27188
- }
27189
- function buildUrl(baseUrl, path2, query) {
27190
- const isAbsolute = /^https?:\/\//.test(baseUrl);
27191
- if (isAbsolute) {
27192
- const normalizedBase = baseUrl.replace(/\/?$/, "/");
27193
- const url = new URL(path2.join("/"), normalizedBase);
27194
- if (query) {
27195
- url.search = stringifyQuery(query);
27196
- }
27197
- return url.toString();
27198
- }
27199
- const cleanBase = `/${baseUrl.replace(/^\/|\/$/g, "")}`;
27200
- const pathStr = path2.length > 0 ? `/${path2.join("/")}` : "";
27201
- const queryStr = query ? stringifyQuery(query) : "";
27202
- return `${cleanBase}${pathStr}${queryStr ? `?${queryStr}` : ""}`;
27203
- }
27204
- function __DEV__() {
27205
- return typeof process !== "undefined" && true;
27206
- }
27207
- function generateTags(path2) {
27208
- return path2.map((_2, i2) => path2.slice(0, i2 + 1).join("/"));
27209
- }
27210
- function containsFile(value) {
27211
- if (value instanceof File || value instanceof Blob) return true;
27212
- if (Array.isArray(value)) return value.some(containsFile);
27213
- if (value && typeof value === "object") {
27214
- return Object.values(value).some(containsFile);
27215
- }
27216
- return false;
27217
- }
27218
- function isJsonBody(body) {
27219
- if (body === null || body === void 0) return false;
27220
- if (body instanceof FormData) return false;
27221
- if (body instanceof Blob) return false;
27222
- if (body instanceof ArrayBuffer) return false;
27223
- if (body instanceof URLSearchParams) return false;
27224
- if (body instanceof ReadableStream) return false;
27225
- if (typeof body === "string") return false;
27226
- if (typeof body === "object") {
27227
- return true;
27228
- }
27229
- return false;
27230
- }
27231
- async function resolveHeaders(headers) {
27232
- if (!headers) return void 0;
27233
- if (typeof headers === "function") {
27234
- return await headers();
27235
- }
27236
- return headers;
27237
- }
27238
- function headersInitToRecord(headers) {
27239
- return Object.fromEntries(new Headers(headers));
27240
- }
27241
- async function resolveHeadersToRecord(headers) {
27242
- const resolved = await resolveHeaders(headers);
27243
- if (!resolved) return {};
27244
- return headersInitToRecord(resolved);
27245
- }
27246
- async function mergeHeaders(defaultHeaders, requestHeaders) {
27247
- const resolved1 = await resolveHeaders(defaultHeaders);
27248
- const resolved2 = await resolveHeaders(requestHeaders);
27249
- if (!resolved1 && !resolved2) return void 0;
27250
- if (!resolved1) return resolved2;
27251
- if (!resolved2) return resolved1;
27252
- return {
27253
- ...Object.fromEntries(new Headers(resolved1)),
27254
- ...Object.fromEntries(new Headers(resolved2))
27255
- };
27256
- }
27257
- function removeHeaderKeys(headers, keysToRemove) {
27258
- if (!headers) return void 0;
27259
- const headersObj = new Headers(headers);
27260
- for (const key of keysToRemove) {
27261
- headersObj.delete(key);
27262
- }
27263
- const entries = [...headersObj.entries()];
27264
- return entries.length > 0 ? Object.fromEntries(entries) : void 0;
27265
- }
27266
- function objectToFormData(obj) {
27267
- const formData = new FormData();
27268
- for (const [key, value] of Object.entries(obj)) {
27269
- if (value === null || value === void 0) {
27270
- continue;
27271
- }
27272
- if (value instanceof Blob || value instanceof File) {
27273
- formData.append(key, value);
27274
- } else if (Array.isArray(value)) {
27275
- for (const entry of value) {
27276
- if (entry instanceof Blob || entry instanceof File) {
27277
- formData.append(key, entry);
27278
- } else if (typeof entry === "object" && entry !== null) {
27279
- formData.append(key, JSON.stringify(entry));
27280
- } else {
27281
- formData.append(key, String(entry));
27282
- }
27283
- }
27284
- } else if (typeof value === "object") {
27285
- formData.append(key, JSON.stringify(value));
27286
- } else {
27287
- formData.append(key, String(value));
27288
- }
27289
- }
27290
- return formData;
27291
- }
27292
- function objectToUrlEncoded(obj) {
27293
- const params = new URLSearchParams();
27294
- for (const [key, value] of Object.entries(obj)) {
27295
- if (value === void 0 || value === null) {
27296
- continue;
27297
- }
27298
- if (Array.isArray(value)) {
27299
- for (const item of value) {
27300
- if (item !== void 0 && item !== null) {
27301
- params.append(key, String(item));
27302
- }
27303
- }
27304
- } else if (typeof value === "object") {
27305
- params.append(key, JSON.stringify(value));
27306
- } else {
27307
- params.append(key, String(value));
27308
- }
27309
- }
27310
- return params.toString();
27311
- }
27312
- function sortObjectKeys(obj, seen2 = /* @__PURE__ */ new WeakSet()) {
27313
- if (obj === null || typeof obj !== "object") return obj;
27314
- if (seen2.has(obj)) {
27315
- return "[Circular]";
27316
- }
27317
- seen2.add(obj);
27318
- if (Array.isArray(obj)) {
27319
- return obj.map((item) => sortObjectKeys(item, seen2));
27320
- }
27321
- return Object.keys(obj).sort().reduce(
27322
- (sorted, key) => {
27323
- sorted[key] = sortObjectKeys(
27324
- obj[key],
27325
- seen2
27326
- );
27327
- return sorted;
27328
- },
27329
- {}
27330
- );
27331
- }
27332
- function isSpooshBody(value) {
27333
- return typeof value === "object" && value !== null && "__spooshBody" in value && value.__spooshBody === true;
27334
- }
27335
- function resolveRequestBody(rawBody) {
27336
- if (rawBody === void 0 || rawBody === null) {
27337
- return void 0;
27338
- }
27339
- if (isSpooshBody(rawBody)) {
27340
- const body = rawBody;
27341
- switch (body.kind) {
27342
- case "form":
27343
- return {
27344
- body: objectToFormData(body.value),
27345
- removeHeaders: ["Content-Type"]
27346
- };
27347
- case "json":
27348
- return {
27349
- body: JSON.stringify(body.value),
27350
- headers: { "Content-Type": "application/json" }
27351
- };
27352
- case "urlencoded":
27353
- return {
27354
- body: objectToUrlEncoded(body.value),
27355
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
27356
- };
27357
- }
27358
- }
27359
- if (isJsonBody(rawBody)) {
27360
- if (__DEV__() && containsFile(rawBody)) {
27361
- console.warn(
27362
- "[spoosh] Plain object body contains File/Blob. Use form() wrapper for multipart upload."
27363
- );
27364
- }
27365
- return {
27366
- body: JSON.stringify(rawBody),
27367
- headers: { "Content-Type": "application/json" }
27368
- };
27369
- }
27370
- if (rawBody instanceof FormData) {
27371
- return { body: rawBody, removeHeaders: ["Content-Type"] };
27372
- }
27373
- return { body: rawBody };
27374
- }
27375
- function normalizeTag$1(tag) {
27376
- return tag.startsWith("/") ? tag.slice(1) : tag;
27377
- }
27378
- function matchTag(entryTag, pattern) {
27379
- const normalizedTag = normalizeTag$1(entryTag);
27380
- const normalizedPattern = normalizeTag$1(pattern);
27381
- if (normalizedPattern.endsWith("/*")) {
27382
- const prefix2 = normalizedPattern.slice(0, -2);
27383
- return prefix2 === "" ? normalizedTag.length > 0 : normalizedTag.startsWith(prefix2 + "/");
27384
- }
27385
- return normalizedTag === normalizedPattern;
27386
- }
27387
- function matchTags(entryTag, patterns) {
27388
- return patterns.some((pattern) => matchTag(entryTag, pattern));
27389
- }
27390
- function resolveTags(options, resolvedPath) {
27391
- const tagsOption = options == null ? void 0 : options.tags;
27392
- if (!tagsOption) {
27393
- const tag2 = resolvedPath.join("/");
27394
- return tag2 ? [normalizeTag$1(tag2)] : [];
27395
- }
27396
- if (typeof tagsOption === "string") {
27397
- return [normalizeTag$1(tagsOption)];
27398
- }
27399
- if (Array.isArray(tagsOption)) {
27400
- return [...new Set(tagsOption.map(normalizeTag$1))];
27401
- }
27402
- const tag = resolvedPath.join("/");
27403
- return tag ? [normalizeTag$1(tag)] : [];
27404
- }
27405
- function resolvePath(path2, params) {
27406
- if (!params) return path2;
27407
- return path2.map((segment) => {
27408
- if (segment.startsWith(":")) {
27409
- const paramName = segment.slice(1);
27410
- const value = params[paramName];
27411
- if (value === void 0) {
27412
- throw new Error(`Missing path parameter: ${paramName}`);
27413
- }
27414
- return String(value);
27415
- }
27416
- return segment;
27417
- });
27418
- }
27419
- function resolvePathString(path2, params) {
27420
- if (!params) return path2;
27421
- return path2.split("/").map((segment) => {
27422
- if (segment.startsWith(":")) {
27423
- const paramName = segment.slice(1);
27424
- const value = params[paramName];
27425
- return value !== void 0 ? String(value) : segment;
27426
- }
27427
- return segment;
27428
- }).join("/");
27429
- }
27430
- var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
27431
- var fetchTransport = async (url, init) => {
27432
- const res = await fetch(url, init);
27433
- const contentType = res.headers.get("content-type");
27434
- const isJson = contentType == null ? void 0 : contentType.includes("application/json");
27435
- const isText = (contentType == null ? void 0 : contentType.includes("text/")) || (contentType == null ? void 0 : contentType.includes("application/xml"));
27436
- let data;
27437
- if (isJson) {
27438
- data = await res.json();
27439
- } else if (isText) {
27440
- data = await res.text();
27441
- } else {
27442
- data = void 0;
27215
+ const DEFAULT_AGENT_MODEL_CHOICE = { agentId: "claude", model: null, effort: null };
27216
+ const workflowSlotTypeSchema = _enum(["dev", "review", "plan", "orch"]);
27217
+ const tierLevelSchema = _enum(["minimal", "low", "medium", "high", "max"]);
27218
+ const LEVEL_ORDER = ["minimal", "low", "medium", "high", "max"];
27219
+ const TIER_LEVEL_OPTIONS = [
27220
+ { value: "minimal", label: "Minimal" },
27221
+ { value: "low", label: "Low" },
27222
+ { value: "medium", label: "Medium" },
27223
+ { value: "high", label: "High" },
27224
+ { value: "max", label: "Max" }
27225
+ ];
27226
+ const modelPairSchema = object({
27227
+ id: string$2(),
27228
+ level: tierLevelSchema,
27229
+ isFree: boolean$1().default(false),
27230
+ binary: runtimeAgentIdSchema,
27231
+ model: string$2().nullable().optional(),
27232
+ effort: effortLevelSchema.nullable().optional()
27233
+ });
27234
+ const pairSelectionModeSchema = _enum(["auto", "preferFree", "freeOnly", "paidOnly"]);
27235
+ const PAIR_SELECTION_MODE_OPTIONS = [
27236
+ { value: "auto", label: "Auto (priority)" },
27237
+ { value: "preferFree", label: "Prefer free" },
27238
+ { value: "freeOnly", label: "Free only" },
27239
+ { value: "paidOnly", label: "Paid only" }
27240
+ ];
27241
+ const SLOT_TOOL_IDS = ["browser"];
27242
+ const slotToolSchema = _enum(SLOT_TOOL_IDS);
27243
+ const slotModelConfigSchema = object({
27244
+ pairs: array(modelPairSchema).min(1),
27245
+ mode: pairSelectionModeSchema.default("auto"),
27246
+ pinnedPairId: string$2().optional()
27247
+ });
27248
+ const cardModelConfigSchema = record(string$2(), slotModelConfigSchema);
27249
+ function pickByMode(candidates, mode) {
27250
+ switch (mode) {
27251
+ case "preferFree":
27252
+ return candidates.find((p) => p.isFree) ?? candidates[0];
27253
+ case "freeOnly":
27254
+ return candidates.find((p) => p.isFree);
27255
+ case "paidOnly":
27256
+ return candidates.find((p) => !p.isFree);
27257
+ default:
27258
+ return candidates[0];
27443
27259
  }
27444
- return { ok: res.ok, status: res.status, headers: res.headers, data };
27445
- };
27446
- var xhrTransport = (url, init, options) => {
27447
- return new Promise((resolve, reject) => {
27448
- const xhr = new XMLHttpRequest();
27449
- xhr.open(init.method ?? "GET", url);
27450
- if (init.headers) {
27451
- const headers = init.headers instanceof Headers ? init.headers : new Headers(init.headers);
27452
- headers.forEach((value, key) => {
27453
- xhr.setRequestHeader(key, value);
27454
- });
27455
- }
27456
- if (init.credentials === "include") {
27457
- xhr.withCredentials = true;
27458
- }
27459
- const onAbort = () => xhr.abort();
27460
- if (init.signal) {
27461
- if (init.signal.aborted) {
27462
- xhr.abort();
27463
- return;
27464
- }
27465
- init.signal.addEventListener("abort", onAbort);
27466
- }
27467
- const cleanup = () => {
27468
- var _a3;
27469
- (_a3 = init.signal) == null ? void 0 : _a3.removeEventListener("abort", onAbort);
27470
- };
27471
- if (options == null ? void 0 : options.onProgress) {
27472
- xhr.upload.addEventListener("progress", (event) => {
27473
- options.onProgress(event, xhr);
27474
- });
27475
- xhr.addEventListener("progress", (event) => {
27476
- options.onProgress(event, xhr);
27477
- });
27478
- }
27479
- xhr.addEventListener("load", () => {
27480
- cleanup();
27481
- const status = xhr.status;
27482
- const ok2 = status >= 200 && status < 300;
27483
- const responseHeaders = new Headers();
27484
- const rawHeaders = xhr.getAllResponseHeaders().trim();
27485
- if (rawHeaders) {
27486
- rawHeaders.split("\r\n").forEach((line) => {
27487
- const idx = line.indexOf(": ");
27488
- if (idx > 0) {
27489
- responseHeaders.append(
27490
- line.substring(0, idx),
27491
- line.substring(idx + 2)
27492
- );
27493
- }
27494
- });
27495
- }
27496
- const contentType = responseHeaders.get("content-type");
27497
- const isJson = contentType == null ? void 0 : contentType.includes("application/json");
27498
- let data;
27499
- try {
27500
- data = isJson ? JSON.parse(xhr.responseText) : xhr.responseText;
27501
- } catch {
27502
- data = xhr.responseText;
27503
- }
27504
- resolve({ ok: ok2, status, headers: responseHeaders, data });
27505
- });
27506
- xhr.addEventListener("error", () => {
27507
- cleanup();
27508
- reject(new TypeError("Network request failed"));
27509
- });
27510
- xhr.addEventListener("abort", () => {
27511
- cleanup();
27512
- reject(new DOMException("Aborted", "AbortError"));
27513
- });
27514
- xhr.send(init.body);
27515
- });
27516
- };
27517
- async function executeFetch(baseUrl, path2, method, defaultOptions2, requestOptions, nextTags) {
27518
- return executeCoreFetch({
27519
- baseUrl,
27520
- path: path2,
27521
- method,
27522
- defaultOptions: defaultOptions2,
27523
- requestOptions,
27524
- middlewareFetchInit: void 0,
27525
- nextTags
27526
- });
27527
27260
  }
27528
- function buildInputFields(requestOptions) {
27529
- const fields = {};
27530
- if ((requestOptions == null ? void 0 : requestOptions.query) !== void 0) {
27531
- fields.query = requestOptions.query;
27532
- }
27533
- if ((requestOptions == null ? void 0 : requestOptions.body) !== void 0) {
27534
- fields.body = requestOptions.body;
27261
+ function resolvePair(cfg, activeLevel) {
27262
+ if (cfg.pinnedPairId) {
27263
+ const pinned = cfg.pairs.find((p) => p.id === cfg.pinnedPairId);
27264
+ if (pinned) return pinned;
27535
27265
  }
27536
- if ((requestOptions == null ? void 0 : requestOptions.params) !== void 0) {
27537
- fields.params = requestOptions.params;
27266
+ const startIdx = LEVEL_ORDER.indexOf(activeLevel);
27267
+ const order2 = [];
27268
+ for (let i2 = startIdx; i2 < LEVEL_ORDER.length; i2++) order2.push(i2);
27269
+ for (let i2 = startIdx - 1; i2 >= 0; i2--) order2.push(i2);
27270
+ for (const i2 of order2) {
27271
+ const candidates = cfg.pairs.filter((p) => p.level === LEVEL_ORDER[i2]);
27272
+ if (candidates.length === 0) continue;
27273
+ const pick3 = pickByMode(candidates, cfg.mode);
27274
+ if (pick3) return pick3;
27538
27275
  }
27539
- if (Object.keys(fields).length === 0) {
27540
- return {};
27276
+ for (const i2 of order2) {
27277
+ const candidates = cfg.pairs.filter((p) => p.level === LEVEL_ORDER[i2]);
27278
+ if (candidates[0]) return candidates[0];
27541
27279
  }
27542
- return { input: fields };
27280
+ const fallback = cfg.pairs[0];
27281
+ if (!fallback) throw new Error("resolvePair: slot has no model pairs");
27282
+ return fallback;
27543
27283
  }
27544
- function resolveTransport(option) {
27545
- if (option === "xhr" && typeof XMLHttpRequest !== "undefined") {
27546
- return xhrTransport;
27284
+ const promptValueSchema = preprocess$1(
27285
+ (v2) => {
27286
+ if (typeof v2 === "string") return { source: "inline", text: v2 };
27287
+ return v2;
27288
+ },
27289
+ discriminatedUnion("source", [
27290
+ object({ source: literal("inline"), text: string$2() }),
27291
+ object({ source: literal("file"), path: string$2() })
27292
+ ])
27293
+ );
27294
+ const EMPTY_INLINE_PROMPT = { source: "inline", text: "" };
27295
+ const workflowSlotSchema = object({
27296
+ id: string$2(),
27297
+ type: workflowSlotTypeSchema,
27298
+ name: string$2(),
27299
+ order: number$2().int().nonnegative(),
27300
+ enabled: boolean$1(),
27301
+ prompt: promptValueSchema.default(EMPTY_INLINE_PROMPT),
27302
+ // Model tiers for this slot, in priority order (top = highest). Copied to the
27303
+ // card at creation; the card's active level + mode select which pair runs.
27304
+ pairs: array(modelPairSchema).min(1),
27305
+ mode: pairSelectionModeSchema.default("auto"),
27306
+ // Tools this slot may use (e.g. "browser"). Workflow-only, not ticket-editable.
27307
+ tools: array(slotToolSchema).default([]),
27308
+ // review slots only: may set the card's active level on reopen.
27309
+ canAdjustLevel: boolean$1().default(false),
27310
+ // plan slots only: re-run even if a plan already exists on the card.
27311
+ rerun: boolean$1().default(false)
27312
+ });
27313
+ const workflowSchema = object({
27314
+ id: string$2(),
27315
+ name: string$2(),
27316
+ isDefault: boolean$1().default(false),
27317
+ forStory: boolean$1().default(false),
27318
+ slots: array(workflowSlotSchema)
27319
+ });
27320
+ function highestWorkflowLevel(workflow) {
27321
+ let bestIdx = -1;
27322
+ for (const slot of (workflow == null ? void 0 : workflow.slots) ?? []) {
27323
+ for (const p of slot.pairs) bestIdx = Math.max(bestIdx, LEVEL_ORDER.indexOf(p.level));
27547
27324
  }
27548
- return fetchTransport;
27325
+ return LEVEL_ORDER[bestIdx] ?? "medium";
27549
27326
  }
27550
- async function executeCoreFetch(config2) {
27551
- const {
27552
- baseUrl,
27553
- path: path2,
27554
- method,
27327
+ const DEFAULT_GIT_INSTRUCTIONS = `# Git conventions
27328
+
27329
+ These rules govern how to write commit messages, PR titles, and PR
27330
+ descriptions.
27331
+
27332
+ ## PR title
27333
+ - Imperative, present tense: "Add board view", "Fix race in poller".
27334
+ Not past tense, not gerund.
27335
+ - ≤70 characters; aim for 50.
27336
+ - Describe what shipped, not the task. "Add board view" beats
27337
+ "Implement board view feature".
27338
+ - No prefixes like \`feat:\` / \`[FEAT]\` / \`fix:\`.
27339
+ - No ticket IDs in the title (put them in the description if needed).
27340
+ - No trailing period.
27341
+
27342
+ ## PR description
27343
+ Keep it focused. Two sections, nothing more unless genuinely useful:
27344
+
27345
+ ## Summary
27346
+ - What changed and why. Use as many bullets as the scope warrants —
27347
+ a one-line fix is one bullet; a refactor touching 20 files may
27348
+ need ten. Don't pad, don't truncate.
27349
+
27350
+ ## Test plan
27351
+ - What you actually ran or clicked, and the outcome.
27352
+ - Type-check and lint passing are not a test plan on their own.
27353
+ - If something couldn't be verified, say so in one line.
27354
+
27355
+ Do NOT include:
27356
+ - Iteration narration ("Round N", "addressed feedback", "after review").
27357
+ - Commit SHAs or branch names — GitHub already shows both.
27358
+ - Paths to internal planning docs, scratch files, or task tracker URLs.
27359
+ - "Verification:" sections that only list a passing type-check or lint.
27360
+ - Self-congratulation ("clean", "all checks pass", "ready to merge").
27361
+ - Restating the task description verbatim.
27362
+
27363
+ ## Commit messages
27364
+ - Short imperative subject line, ≤72 chars. That's usually enough.
27365
+ - Skip the body unless a reviewer reading the diff alone would be
27366
+ confused about *why* the change exists.
27367
+ - Reference an issue only if a concrete one exists to close
27368
+ (\`Closes #123\`). Never invent issue numbers.
27369
+ `;
27370
+ const runtimeBoardColumnIdSchema = _enum([
27371
+ "todo",
27372
+ "in_progress",
27373
+ "reopened",
27374
+ "ready_for_review",
27375
+ "blocked",
27376
+ "done"
27377
+ ]);
27378
+ const reviewActorSchema = object({
27379
+ type: _enum(["ai", "human", "external"]),
27380
+ id: string$2(),
27381
+ source: string$2().optional()
27382
+ });
27383
+ const reviewIssueSchema = object({
27384
+ file: string$2().optional(),
27385
+ line: number$2().optional(),
27386
+ severity: _enum(["blocking", "warning", "info"]),
27387
+ message: string$2()
27388
+ });
27389
+ const reviewAttachmentSchema = object({
27390
+ type: string$2(),
27391
+ // "image" | "file" | any mime category
27392
+ name: string$2(),
27393
+ mimeType: string$2(),
27394
+ path: string$2()
27395
+ // absolute path in ~/.whipped/attachments/
27396
+ });
27397
+ const runtimeReviewCommentSchema = object({
27398
+ id: string$2(),
27399
+ type: string$2(),
27400
+ actor: reviewActorSchema,
27401
+ status: _enum(["pass", "fail", "warning", "skipped"]).optional(),
27402
+ createdAt: number$2(),
27403
+ streamId: string$2().optional(),
27404
+ summary: string$2(),
27405
+ issues: array(reviewIssueSchema).optional(),
27406
+ attachments: array(reviewAttachmentSchema).optional(),
27407
+ metadata: record(string$2(), unknown$1()).optional()
27408
+ });
27409
+ const runtimeActivityEntrySchema = object({
27410
+ timestamp: number$2(),
27411
+ message: string$2()
27412
+ });
27413
+ const runtimeTaskSessionStateSchema = _enum(["running", "stopped", "completed", "failed", "killed"]);
27414
+ const runtimeTerminalSessionEntrySchema = object({
27415
+ streamId: string$2(),
27416
+ type: string$2(),
27417
+ startedAt: number$2(),
27418
+ endedAt: number$2().optional(),
27419
+ agentId: runtimeAgentIdSchema.optional(),
27420
+ state: runtimeTaskSessionStateSchema.optional()
27421
+ });
27422
+ const runtimeCardPrioritySchema = _enum(["urgent", "high", "medium", "low"]);
27423
+ const cardTypeSchema = _enum(["task", "story", "subtask"]);
27424
+ const runtimePrMetaSchema = object({
27425
+ url: string$2().optional(),
27426
+ title: string$2().optional(),
27427
+ description: string$2().optional(),
27428
+ updatedAt: number$2().optional(),
27429
+ updatedBy: string$2().optional()
27430
+ });
27431
+ const runtimeBoardCardSchema = object({
27432
+ id: string$2(),
27433
+ description: string$2(),
27434
+ descriptionAttachments: array(reviewAttachmentSchema).optional().default([]),
27435
+ columnId: runtimeBoardColumnIdSchema,
27436
+ type: cardTypeSchema.default("task"),
27437
+ readyForDev: boolean$1().default(false),
27438
+ agentId: runtimeAgentIdSchema.optional(),
27439
+ priority: runtimeCardPrioritySchema.optional(),
27440
+ // Single-parent stacking: this card continues in the parent's worktree/branch
27441
+ // and starts once the parent reaches ready_for_review. Mutually exclusive with waitsFor.
27442
+ dependsOn: string$2().optional(),
27443
+ // Many-parent gate (tasks only): this card starts only once ALL listed cards are
27444
+ // done (merged), in a fresh worktree branched from baseRef. Mutually exclusive with dependsOn.
27445
+ waitsFor: array(string$2()).default([]),
27446
+ // Story-only: the IDs of this story's subtasks. The story triggers its orchestrator
27447
+ // workflow once every subtask reaches ready_for_review.
27448
+ subtaskIds: array(string$2()).default([]),
27449
+ autoFixAttempts: number$2().int().nonnegative().default(0),
27450
+ baseRef: string$2(),
27451
+ createdAt: number$2(),
27452
+ updatedAt: number$2(),
27453
+ githubIssueUrl: string$2().optional(),
27454
+ pr: runtimePrMetaSchema.optional(),
27455
+ workflowId: string$2().optional(),
27456
+ // Plan written by the one-shot plan agent; injected into the dev agent's prompt.
27457
+ plan: string$2().optional(),
27458
+ // Workflow-wide capability level; every slot resolves it to its own pair.
27459
+ activeLevel: tierLevelSchema.default("medium"),
27460
+ // Per-slot model config, snapshotted from the workflow at creation and editable
27461
+ // per ticket (slotId → {pairs, mode, pinnedPairId}).
27462
+ modelConfig: cardModelConfigSchema.optional(),
27463
+ reviewComments: array(runtimeReviewCommentSchema).default([]),
27464
+ activityLog: array(runtimeActivityEntrySchema).default([]),
27465
+ terminalSessions: array(runtimeTerminalSessionEntrySchema).default([]),
27466
+ githubCommentIds: array(string$2()).default([]),
27467
+ worktreePath: string$2().optional(),
27468
+ branchName: string$2().optional(),
27469
+ slackMessageTs: string$2().optional(),
27470
+ slackChannelId: string$2().optional()
27471
+ });
27472
+ const runtimeBoardColumnSchema = object({
27473
+ id: runtimeBoardColumnIdSchema,
27474
+ title: string$2(),
27475
+ taskIds: array(string$2())
27476
+ });
27477
+ const runtimeBoardDataSchema = object({
27478
+ columns: array(runtimeBoardColumnSchema),
27479
+ cards: record(string$2(), runtimeBoardCardSchema)
27480
+ });
27481
+ const runtimeGlobalConfigSchema = object({
27482
+ defaultAgent: runtimeAgentIdSchema.default("claude"),
27483
+ maxParallelTasks: number$2().int().positive().default(4),
27484
+ maxParallelQA: number$2().int().positive().default(1),
27485
+ maxAutoFixAttempts: number$2().int().nonnegative().default(3),
27486
+ pollingIntervalSeconds: number$2().int().positive().default(30),
27487
+ prPollingIntervalSeconds: number$2().int().positive().default(60),
27488
+ terminalApp: string$2().optional(),
27489
+ slackEnabled: boolean$1().default(true),
27490
+ slackBotToken: string$2().optional(),
27491
+ slackSigningSecret: string$2().optional(),
27492
+ slackAppConfigToken: string$2().optional(),
27493
+ slackClientId: string$2().optional(),
27494
+ slackClientSecret: string$2().optional(),
27495
+ slackAppId: string$2().optional(),
27496
+ slackOauthAuthorizeUrl: string$2().optional(),
27497
+ slackPublicUrl: string$2().optional(),
27498
+ slackBotName: string$2().default("Whipped"),
27499
+ slackInstallerUserId: string$2().optional(),
27500
+ autoStartTunnel: boolean$1().default(false),
27501
+ tunnelId: string$2().optional(),
27502
+ tunnelDomain: string$2().optional(),
27503
+ tunnelName: string$2().default("whipped"),
27504
+ // Auth: single shared password (scrypt hash) + HMAC secret for signed session
27505
+ // cookies + machine token for local agent machinery (MCP/hooks). Never expose
27506
+ // these over the API — see configController's response.
27507
+ authPasswordHash: string$2().optional(),
27508
+ authSessionSecret: string$2().optional(),
27509
+ authMachineToken: string$2().optional()
27510
+ });
27511
+ const runtimeGithubConfigSchema = object({
27512
+ token: string$2()
27513
+ });
27514
+ const runtimeWorktreeSetupSchema = object({
27515
+ filesToCopy: array(string$2()).default([]),
27516
+ installCommand: string$2().default("")
27517
+ });
27518
+ const runtimeProjectSecretSchema = object({
27519
+ key: string$2().min(1),
27520
+ value: string$2()
27521
+ });
27522
+ const BUILTIN_SECRET_KEYS = ["GITHUB_TOKEN"];
27523
+ const runtimeProjectConfigSchema = object({
27524
+ name: string$2().optional(),
27525
+ defaultAgent: runtimeAgentIdSchema.optional(),
27526
+ maxParallelTasks: number$2().int().positive().optional(),
27527
+ maxAutoFixAttempts: number$2().int().nonnegative().optional(),
27528
+ pollingIntervalSeconds: number$2().int().positive().optional(),
27529
+ // What happens when a card passes review (polling/dispatch is always on; per-ticket
27530
+ // readyForDev gates pickup). "off" parks it in ready_for_review, "pr" auto-creates a
27531
+ // GitHub PR, "yolo" merges the branch straight into the local baseRef and pushes.
27532
+ deliveryMode: _enum(["off", "pr", "yolo"]).default("off"),
27533
+ autoCommit: boolean$1().default(true),
27534
+ defaultBaseBranch: string$2().optional(),
27535
+ github: runtimeGithubConfigSchema.optional(),
27536
+ worktreeSetup: runtimeWorktreeSetupSchema.optional(),
27537
+ startCommand: string$2().default(""),
27538
+ workflows: array(workflowSchema).default([]),
27539
+ secrets: array(runtimeProjectSecretSchema).default([]),
27540
+ systemPrompt: string$2().optional(),
27541
+ // Freeform instructions injected into the dev agent's prompt to shape PR
27542
+ // titles, descriptions, and commit messages. Empty/absent → daemon falls
27543
+ // back to DEFAULT_GIT_INSTRUCTIONS.
27544
+ gitInstructions: string$2().optional(),
27545
+ // Which agent binary/model/effort the assistant agent runs as. Absent → claude.
27546
+ assistantModel: agentModelChoiceSchema.optional()
27547
+ });
27548
+ object({
27549
+ workspaceId: string$2(),
27550
+ repoPath: string$2(),
27551
+ board: runtimeBoardDataSchema,
27552
+ revision: number$2(),
27553
+ projectConfig: runtimeProjectConfigSchema
27554
+ });
27555
+ object({
27556
+ board: runtimeBoardDataSchema,
27557
+ revision: number$2()
27558
+ });
27559
+ const runtimeVisualElementSchema = object({
27560
+ elementSelector: string$2().optional(),
27561
+ elementText: string$2().optional(),
27562
+ componentName: string$2().optional(),
27563
+ componentChain: array(string$2()).optional(),
27564
+ sourceFile: string$2().optional(),
27565
+ sourceLine: number$2().optional(),
27566
+ // The page the element was captured on. Selections can span pages, so this is
27567
+ // per-element rather than relying on the visualComment-level pageUrl.
27568
+ pageUrl: string$2().optional()
27569
+ });
27570
+ const runtimeVisualCommentSchema = object({
27571
+ pageUrl: string$2().optional(),
27572
+ elements: array(runtimeVisualElementSchema).default([])
27573
+ });
27574
+ const runtimeCardCreateRequestSchema = object({
27575
+ description: string$2(),
27576
+ type: cardTypeSchema.optional(),
27577
+ // Browser-extension element references; folded into the description server-side.
27578
+ visualComment: runtimeVisualCommentSchema.optional(),
27579
+ agentId: runtimeAgentIdSchema.optional(),
27580
+ priority: runtimeCardPrioritySchema.optional(),
27581
+ readyForDev: boolean$1().optional(),
27582
+ dependsOn: string$2().optional(),
27583
+ waitsFor: array(string$2()).optional(),
27584
+ subtaskIds: array(string$2()).optional(),
27585
+ columnId: runtimeBoardColumnIdSchema.optional(),
27586
+ baseRef: string$2().optional(),
27587
+ githubIssueUrl: string$2().optional(),
27588
+ workflowId: string$2().optional(),
27589
+ descriptionAttachments: array(reviewAttachmentSchema).optional(),
27590
+ branchName: string$2().optional(),
27591
+ // Optional per-ticket overrides edited before creation. When omitted, the card
27592
+ // snapshots the resolved workflow's pairs and defaults the active level to the
27593
+ // workflow's highest configured tier.
27594
+ modelConfig: cardModelConfigSchema.optional(),
27595
+ activeLevel: tierLevelSchema.optional()
27596
+ });
27597
+ object({
27598
+ cardId: string$2(),
27599
+ targetColumnId: runtimeBoardColumnIdSchema,
27600
+ targetIndex: number$2().int().nonnegative().optional(),
27601
+ revision: number$2()
27602
+ });
27603
+ object({
27604
+ cardId: string$2(),
27605
+ description: string$2().optional(),
27606
+ descriptionAttachments: array(reviewAttachmentSchema).optional(),
27607
+ type: cardTypeSchema.optional(),
27608
+ agentId: runtimeAgentIdSchema.optional(),
27609
+ priority: runtimeCardPrioritySchema.optional(),
27610
+ readyForDev: boolean$1().optional(),
27611
+ dependsOn: string$2().optional(),
27612
+ waitsFor: array(string$2()).optional(),
27613
+ subtaskIds: array(string$2()).optional(),
27614
+ workflowId: string$2().optional(),
27615
+ branchName: string$2().optional(),
27616
+ plan: string$2().optional(),
27617
+ activeLevel: tierLevelSchema.optional(),
27618
+ modelConfig: cardModelConfigSchema.optional(),
27619
+ revision: number$2()
27620
+ });
27621
+ const memoryScopeSchema = _enum(["global", "project"]);
27622
+ const memoryTypeSchema = _enum([
27623
+ "fact",
27624
+ "convention",
27625
+ "decision",
27626
+ "preference",
27627
+ "rule",
27628
+ "lesson",
27629
+ "sharp_edge"
27630
+ ]);
27631
+ const MEMORY_TYPE_OPTIONS = [
27632
+ { value: "fact", label: "Fact" },
27633
+ { value: "convention", label: "Convention" },
27634
+ { value: "decision", label: "Decision" },
27635
+ { value: "preference", label: "Preference" },
27636
+ { value: "rule", label: "Rule" },
27637
+ { value: "lesson", label: "Lesson" },
27638
+ { value: "sharp_edge", label: "Sharp edge" }
27639
+ ];
27640
+ const memorySourceTypeSchema = _enum(["user_correction", "explicit_save", "task_lesson", "manual_human"]);
27641
+ const memoryStatusSchema = _enum(["pending", "approved"]);
27642
+ const runtimeMemoryOriginAgentSchema = object({
27643
+ agent: string$2(),
27644
+ model: string$2().optional()
27645
+ });
27646
+ object({
27647
+ id: string$2(),
27648
+ scope: memoryScopeSchema,
27649
+ workspaceId: string$2().nullable(),
27650
+ originWorkspaceId: string$2().nullable().optional(),
27651
+ type: memoryTypeSchema,
27652
+ title: string$2(),
27653
+ content: string$2(),
27654
+ sourceType: memorySourceTypeSchema,
27655
+ importance: number$2().int().min(1).max(3).default(1),
27656
+ tags: array(string$2()).default([]),
27657
+ boundWorkspaceIds: array(string$2()).default([]),
27658
+ originCardId: string$2().nullable().optional(),
27659
+ originAgent: runtimeMemoryOriginAgentSchema.nullable().optional(),
27660
+ status: memoryStatusSchema.default("approved"),
27661
+ createdAt: number$2(),
27662
+ updatedAt: number$2()
27663
+ });
27664
+ function normalizeTag$1(raw2) {
27665
+ return raw2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
27666
+ }
27667
+ _enum(["interval", "calendar"]);
27668
+ const recurringScheduleSchema = discriminatedUnion("kind", [
27669
+ object({ kind: literal("interval"), intervalSeconds: number$2().int().positive() }),
27670
+ object({ kind: literal("calendar"), cronExpr: string$2().min(1), timezone: string$2().min(1) })
27671
+ ]);
27672
+ const recurringRunStatusSchema = _enum(["running", "ok", "error", "killed"]);
27673
+ const recurringRunTriggerSchema = _enum(["schedule", "manual"]);
27674
+ const recurringAgentRunSchema = object({
27675
+ id: string$2(),
27676
+ startedAt: number$2(),
27677
+ endedAt: number$2().optional(),
27678
+ status: recurringRunStatusSchema,
27679
+ summary: string$2().optional(),
27680
+ tokens: number$2().optional(),
27681
+ trigger: recurringRunTriggerSchema.default("schedule"),
27682
+ streamId: string$2().optional()
27683
+ });
27684
+ object({
27685
+ id: string$2(),
27686
+ name: string$2(),
27687
+ instructions: string$2().default(""),
27688
+ schedule: recurringScheduleSchema,
27689
+ model: agentModelChoiceSchema,
27690
+ enabled: boolean$1().default(true),
27691
+ lastRunAt: number$2().optional(),
27692
+ nextRunAt: number$2().optional(),
27693
+ journal: string$2().default(""),
27694
+ createdAt: number$2(),
27695
+ updatedAt: number$2(),
27696
+ recentRuns: array(recurringAgentRunSchema).default([])
27697
+ });
27698
+ object({
27699
+ name: string$2().min(1),
27700
+ instructions: string$2().optional(),
27701
+ schedule: recurringScheduleSchema,
27702
+ model: agentModelChoiceSchema.optional(),
27703
+ enabled: boolean$1().optional()
27704
+ });
27705
+ object({
27706
+ id: string$2(),
27707
+ name: string$2().min(1).optional(),
27708
+ instructions: string$2().optional(),
27709
+ schedule: recurringScheduleSchema.optional(),
27710
+ model: agentModelChoiceSchema.optional(),
27711
+ enabled: boolean$1().optional(),
27712
+ journal: string$2().optional()
27713
+ });
27714
+ const projectFolderSchema = object({
27715
+ id: string$2(),
27716
+ name: string$2(),
27717
+ collapsed: boolean$1().default(false),
27718
+ projectIds: array(string$2())
27719
+ });
27720
+ const topLevelItemSchema = discriminatedUnion("type", [
27721
+ object({ type: literal("folder"), id: string$2() }),
27722
+ object({ type: literal("project"), workspaceId: string$2() })
27723
+ ]);
27724
+ object({
27725
+ version: literal(1),
27726
+ topLevel: array(topLevelItemSchema),
27727
+ folders: record(string$2(), projectFolderSchema)
27728
+ });
27729
+ object({
27730
+ workspaceId: string$2(),
27731
+ repoPath: string$2(),
27732
+ name: string$2(),
27733
+ lastUpdated: number$2()
27734
+ });
27735
+ const addProjectSchema = object({
27736
+ repoPath: string$2().min(1, "Repository path is required"),
27737
+ deliveryMode: _enum(["off", "pr", "yolo"]),
27738
+ defaultBaseBranch: string$2().optional(),
27739
+ assistantModel: agentModelChoiceSchema
27740
+ });
27741
+ function stringifyQuery(query) {
27742
+ const parts = [];
27743
+ for (const [key, value] of Object.entries(query)) {
27744
+ if (value === void 0 || value === null || value === "") {
27745
+ continue;
27746
+ }
27747
+ parts.push(
27748
+ `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
27749
+ );
27750
+ }
27751
+ return parts.join("&");
27752
+ }
27753
+ function buildUrl(baseUrl, path2, query) {
27754
+ const isAbsolute = /^https?:\/\//.test(baseUrl);
27755
+ if (isAbsolute) {
27756
+ const normalizedBase = baseUrl.replace(/\/?$/, "/");
27757
+ const url = new URL(path2.join("/"), normalizedBase);
27758
+ if (query) {
27759
+ url.search = stringifyQuery(query);
27760
+ }
27761
+ return url.toString();
27762
+ }
27763
+ const cleanBase = `/${baseUrl.replace(/^\/|\/$/g, "")}`;
27764
+ const pathStr = path2.length > 0 ? `/${path2.join("/")}` : "";
27765
+ const queryStr = query ? stringifyQuery(query) : "";
27766
+ return `${cleanBase}${pathStr}${queryStr ? `?${queryStr}` : ""}`;
27767
+ }
27768
+ function __DEV__() {
27769
+ return typeof process !== "undefined" && true;
27770
+ }
27771
+ function generateTags(path2) {
27772
+ return path2.map((_2, i2) => path2.slice(0, i2 + 1).join("/"));
27773
+ }
27774
+ function containsFile(value) {
27775
+ if (value instanceof File || value instanceof Blob) return true;
27776
+ if (Array.isArray(value)) return value.some(containsFile);
27777
+ if (value && typeof value === "object") {
27778
+ return Object.values(value).some(containsFile);
27779
+ }
27780
+ return false;
27781
+ }
27782
+ function isJsonBody(body) {
27783
+ if (body === null || body === void 0) return false;
27784
+ if (body instanceof FormData) return false;
27785
+ if (body instanceof Blob) return false;
27786
+ if (body instanceof ArrayBuffer) return false;
27787
+ if (body instanceof URLSearchParams) return false;
27788
+ if (body instanceof ReadableStream) return false;
27789
+ if (typeof body === "string") return false;
27790
+ if (typeof body === "object") {
27791
+ return true;
27792
+ }
27793
+ return false;
27794
+ }
27795
+ async function resolveHeaders(headers) {
27796
+ if (!headers) return void 0;
27797
+ if (typeof headers === "function") {
27798
+ return await headers();
27799
+ }
27800
+ return headers;
27801
+ }
27802
+ function headersInitToRecord(headers) {
27803
+ return Object.fromEntries(new Headers(headers));
27804
+ }
27805
+ async function resolveHeadersToRecord(headers) {
27806
+ const resolved = await resolveHeaders(headers);
27807
+ if (!resolved) return {};
27808
+ return headersInitToRecord(resolved);
27809
+ }
27810
+ async function mergeHeaders(defaultHeaders, requestHeaders) {
27811
+ const resolved1 = await resolveHeaders(defaultHeaders);
27812
+ const resolved2 = await resolveHeaders(requestHeaders);
27813
+ if (!resolved1 && !resolved2) return void 0;
27814
+ if (!resolved1) return resolved2;
27815
+ if (!resolved2) return resolved1;
27816
+ return {
27817
+ ...Object.fromEntries(new Headers(resolved1)),
27818
+ ...Object.fromEntries(new Headers(resolved2))
27819
+ };
27820
+ }
27821
+ function removeHeaderKeys(headers, keysToRemove) {
27822
+ if (!headers) return void 0;
27823
+ const headersObj = new Headers(headers);
27824
+ for (const key of keysToRemove) {
27825
+ headersObj.delete(key);
27826
+ }
27827
+ const entries = [...headersObj.entries()];
27828
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
27829
+ }
27830
+ function objectToFormData(obj) {
27831
+ const formData = new FormData();
27832
+ for (const [key, value] of Object.entries(obj)) {
27833
+ if (value === null || value === void 0) {
27834
+ continue;
27835
+ }
27836
+ if (value instanceof Blob || value instanceof File) {
27837
+ formData.append(key, value);
27838
+ } else if (Array.isArray(value)) {
27839
+ for (const entry of value) {
27840
+ if (entry instanceof Blob || entry instanceof File) {
27841
+ formData.append(key, entry);
27842
+ } else if (typeof entry === "object" && entry !== null) {
27843
+ formData.append(key, JSON.stringify(entry));
27844
+ } else {
27845
+ formData.append(key, String(entry));
27846
+ }
27847
+ }
27848
+ } else if (typeof value === "object") {
27849
+ formData.append(key, JSON.stringify(value));
27850
+ } else {
27851
+ formData.append(key, String(value));
27852
+ }
27853
+ }
27854
+ return formData;
27855
+ }
27856
+ function objectToUrlEncoded(obj) {
27857
+ const params = new URLSearchParams();
27858
+ for (const [key, value] of Object.entries(obj)) {
27859
+ if (value === void 0 || value === null) {
27860
+ continue;
27861
+ }
27862
+ if (Array.isArray(value)) {
27863
+ for (const item of value) {
27864
+ if (item !== void 0 && item !== null) {
27865
+ params.append(key, String(item));
27866
+ }
27867
+ }
27868
+ } else if (typeof value === "object") {
27869
+ params.append(key, JSON.stringify(value));
27870
+ } else {
27871
+ params.append(key, String(value));
27872
+ }
27873
+ }
27874
+ return params.toString();
27875
+ }
27876
+ function sortObjectKeys(obj, seen2 = /* @__PURE__ */ new WeakSet()) {
27877
+ if (obj === null || typeof obj !== "object") return obj;
27878
+ if (seen2.has(obj)) {
27879
+ return "[Circular]";
27880
+ }
27881
+ seen2.add(obj);
27882
+ if (Array.isArray(obj)) {
27883
+ return obj.map((item) => sortObjectKeys(item, seen2));
27884
+ }
27885
+ return Object.keys(obj).sort().reduce(
27886
+ (sorted, key) => {
27887
+ sorted[key] = sortObjectKeys(
27888
+ obj[key],
27889
+ seen2
27890
+ );
27891
+ return sorted;
27892
+ },
27893
+ {}
27894
+ );
27895
+ }
27896
+ function isSpooshBody(value) {
27897
+ return typeof value === "object" && value !== null && "__spooshBody" in value && value.__spooshBody === true;
27898
+ }
27899
+ function resolveRequestBody(rawBody) {
27900
+ if (rawBody === void 0 || rawBody === null) {
27901
+ return void 0;
27902
+ }
27903
+ if (isSpooshBody(rawBody)) {
27904
+ const body = rawBody;
27905
+ switch (body.kind) {
27906
+ case "form":
27907
+ return {
27908
+ body: objectToFormData(body.value),
27909
+ removeHeaders: ["Content-Type"]
27910
+ };
27911
+ case "json":
27912
+ return {
27913
+ body: JSON.stringify(body.value),
27914
+ headers: { "Content-Type": "application/json" }
27915
+ };
27916
+ case "urlencoded":
27917
+ return {
27918
+ body: objectToUrlEncoded(body.value),
27919
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
27920
+ };
27921
+ }
27922
+ }
27923
+ if (isJsonBody(rawBody)) {
27924
+ if (__DEV__() && containsFile(rawBody)) {
27925
+ console.warn(
27926
+ "[spoosh] Plain object body contains File/Blob. Use form() wrapper for multipart upload."
27927
+ );
27928
+ }
27929
+ return {
27930
+ body: JSON.stringify(rawBody),
27931
+ headers: { "Content-Type": "application/json" }
27932
+ };
27933
+ }
27934
+ if (rawBody instanceof FormData) {
27935
+ return { body: rawBody, removeHeaders: ["Content-Type"] };
27936
+ }
27937
+ return { body: rawBody };
27938
+ }
27939
+ function normalizeTag(tag) {
27940
+ return tag.startsWith("/") ? tag.slice(1) : tag;
27941
+ }
27942
+ function matchTag(entryTag, pattern) {
27943
+ const normalizedTag = normalizeTag(entryTag);
27944
+ const normalizedPattern = normalizeTag(pattern);
27945
+ if (normalizedPattern.endsWith("/*")) {
27946
+ const prefix2 = normalizedPattern.slice(0, -2);
27947
+ return prefix2 === "" ? normalizedTag.length > 0 : normalizedTag.startsWith(prefix2 + "/");
27948
+ }
27949
+ return normalizedTag === normalizedPattern;
27950
+ }
27951
+ function matchTags(entryTag, patterns) {
27952
+ return patterns.some((pattern) => matchTag(entryTag, pattern));
27953
+ }
27954
+ function resolveTags(options, resolvedPath) {
27955
+ const tagsOption = options == null ? void 0 : options.tags;
27956
+ if (!tagsOption) {
27957
+ const tag2 = resolvedPath.join("/");
27958
+ return tag2 ? [normalizeTag(tag2)] : [];
27959
+ }
27960
+ if (typeof tagsOption === "string") {
27961
+ return [normalizeTag(tagsOption)];
27962
+ }
27963
+ if (Array.isArray(tagsOption)) {
27964
+ return [...new Set(tagsOption.map(normalizeTag))];
27965
+ }
27966
+ const tag = resolvedPath.join("/");
27967
+ return tag ? [normalizeTag(tag)] : [];
27968
+ }
27969
+ function resolvePath(path2, params) {
27970
+ if (!params) return path2;
27971
+ return path2.map((segment) => {
27972
+ if (segment.startsWith(":")) {
27973
+ const paramName = segment.slice(1);
27974
+ const value = params[paramName];
27975
+ if (value === void 0) {
27976
+ throw new Error(`Missing path parameter: ${paramName}`);
27977
+ }
27978
+ return String(value);
27979
+ }
27980
+ return segment;
27981
+ });
27982
+ }
27983
+ function resolvePathString(path2, params) {
27984
+ if (!params) return path2;
27985
+ return path2.split("/").map((segment) => {
27986
+ if (segment.startsWith(":")) {
27987
+ const paramName = segment.slice(1);
27988
+ const value = params[paramName];
27989
+ return value !== void 0 ? String(value) : segment;
27990
+ }
27991
+ return segment;
27992
+ }).join("/");
27993
+ }
27994
+ var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
27995
+ var fetchTransport = async (url, init) => {
27996
+ const res = await fetch(url, init);
27997
+ const contentType = res.headers.get("content-type");
27998
+ const isJson = contentType == null ? void 0 : contentType.includes("application/json");
27999
+ const isText = (contentType == null ? void 0 : contentType.includes("text/")) || (contentType == null ? void 0 : contentType.includes("application/xml"));
28000
+ let data;
28001
+ if (isJson) {
28002
+ data = await res.json();
28003
+ } else if (isText) {
28004
+ data = await res.text();
28005
+ } else {
28006
+ data = void 0;
28007
+ }
28008
+ return { ok: res.ok, status: res.status, headers: res.headers, data };
28009
+ };
28010
+ var xhrTransport = (url, init, options) => {
28011
+ return new Promise((resolve, reject) => {
28012
+ const xhr = new XMLHttpRequest();
28013
+ xhr.open(init.method ?? "GET", url);
28014
+ if (init.headers) {
28015
+ const headers = init.headers instanceof Headers ? init.headers : new Headers(init.headers);
28016
+ headers.forEach((value, key) => {
28017
+ xhr.setRequestHeader(key, value);
28018
+ });
28019
+ }
28020
+ if (init.credentials === "include") {
28021
+ xhr.withCredentials = true;
28022
+ }
28023
+ const onAbort = () => xhr.abort();
28024
+ if (init.signal) {
28025
+ if (init.signal.aborted) {
28026
+ xhr.abort();
28027
+ return;
28028
+ }
28029
+ init.signal.addEventListener("abort", onAbort);
28030
+ }
28031
+ const cleanup = () => {
28032
+ var _a3;
28033
+ (_a3 = init.signal) == null ? void 0 : _a3.removeEventListener("abort", onAbort);
28034
+ };
28035
+ if (options == null ? void 0 : options.onProgress) {
28036
+ xhr.upload.addEventListener("progress", (event) => {
28037
+ options.onProgress(event, xhr);
28038
+ });
28039
+ xhr.addEventListener("progress", (event) => {
28040
+ options.onProgress(event, xhr);
28041
+ });
28042
+ }
28043
+ xhr.addEventListener("load", () => {
28044
+ cleanup();
28045
+ const status = xhr.status;
28046
+ const ok2 = status >= 200 && status < 300;
28047
+ const responseHeaders = new Headers();
28048
+ const rawHeaders = xhr.getAllResponseHeaders().trim();
28049
+ if (rawHeaders) {
28050
+ rawHeaders.split("\r\n").forEach((line) => {
28051
+ const idx = line.indexOf(": ");
28052
+ if (idx > 0) {
28053
+ responseHeaders.append(
28054
+ line.substring(0, idx),
28055
+ line.substring(idx + 2)
28056
+ );
28057
+ }
28058
+ });
28059
+ }
28060
+ const contentType = responseHeaders.get("content-type");
28061
+ const isJson = contentType == null ? void 0 : contentType.includes("application/json");
28062
+ let data;
28063
+ try {
28064
+ data = isJson ? JSON.parse(xhr.responseText) : xhr.responseText;
28065
+ } catch {
28066
+ data = xhr.responseText;
28067
+ }
28068
+ resolve({ ok: ok2, status, headers: responseHeaders, data });
28069
+ });
28070
+ xhr.addEventListener("error", () => {
28071
+ cleanup();
28072
+ reject(new TypeError("Network request failed"));
28073
+ });
28074
+ xhr.addEventListener("abort", () => {
28075
+ cleanup();
28076
+ reject(new DOMException("Aborted", "AbortError"));
28077
+ });
28078
+ xhr.send(init.body);
28079
+ });
28080
+ };
28081
+ async function executeFetch(baseUrl, path2, method, defaultOptions2, requestOptions, nextTags) {
28082
+ return executeCoreFetch({
28083
+ baseUrl,
28084
+ path: path2,
28085
+ method,
28086
+ defaultOptions: defaultOptions2,
28087
+ requestOptions,
28088
+ middlewareFetchInit: void 0,
28089
+ nextTags
28090
+ });
28091
+ }
28092
+ function buildInputFields(requestOptions) {
28093
+ const fields = {};
28094
+ if ((requestOptions == null ? void 0 : requestOptions.query) !== void 0) {
28095
+ fields.query = requestOptions.query;
28096
+ }
28097
+ if ((requestOptions == null ? void 0 : requestOptions.body) !== void 0) {
28098
+ fields.body = requestOptions.body;
28099
+ }
28100
+ if ((requestOptions == null ? void 0 : requestOptions.params) !== void 0) {
28101
+ fields.params = requestOptions.params;
28102
+ }
28103
+ if (Object.keys(fields).length === 0) {
28104
+ return {};
28105
+ }
28106
+ return { input: fields };
28107
+ }
28108
+ function resolveTransport(option) {
28109
+ if (option === "xhr" && typeof XMLHttpRequest !== "undefined") {
28110
+ return xhrTransport;
28111
+ }
28112
+ return fetchTransport;
28113
+ }
28114
+ async function executeCoreFetch(config2) {
28115
+ const {
28116
+ baseUrl,
28117
+ path: path2,
28118
+ method,
27555
28119
  defaultOptions: defaultOptions2,
27556
28120
  requestOptions,
27557
28121
  middlewareFetchInit,
@@ -29595,19 +30159,19 @@ function resolveInvalidateTags(path2, params, invalidateOption, autoInvalidate,
29595
30159
  return ["*"];
29596
30160
  }
29597
30161
  if (typeof invalidateOption === "string") {
29598
- const normalized = normalizeTag$1(invalidateOption);
30162
+ const normalized = normalizeTag(invalidateOption);
29599
30163
  if (normalized === "*") {
29600
30164
  return ["*"];
29601
30165
  }
29602
30166
  return [normalized];
29603
30167
  }
29604
30168
  if (Array.isArray(invalidateOption)) {
29605
- return [...new Set(invalidateOption.map(normalizeTag$1))];
30169
+ return [...new Set(invalidateOption.map(normalizeTag))];
29606
30170
  }
29607
30171
  if (!autoInvalidate) {
29608
30172
  return false;
29609
30173
  }
29610
- const resolvedPath = normalizeTag$1(resolvePathString(path2, params));
30174
+ const resolvedPath = normalizeTag(resolvePathString(path2, params));
29611
30175
  const segments = resolvedPath.split("/");
29612
30176
  const depth = calculateSegmentDepth(resolvedPath, groups);
29613
30177
  const baseSegments = segments.slice(0, depth);
@@ -29669,7 +30233,7 @@ function invalidationPlugin(config2 = {}) {
29669
30233
  eventEmitter.emit("refetchAll", void 0);
29670
30234
  return;
29671
30235
  }
29672
- const patterns = rawPatterns.map(normalizeTag$1);
30236
+ const patterns = rawPatterns.map(normalizeTag);
29673
30237
  if (patterns.includes("*")) {
29674
30238
  et == null ? void 0 : et.emit("Refetch all (manual)", { color: "warning" });
29675
30239
  eventEmitter.emit("refetchAll", void 0);
@@ -31156,14 +31720,186 @@ function FolderPickerDialog({ initialPath, onSelect, onClose }) {
31156
31720
  }
31157
31721
  ) });
31158
31722
  }
31723
+ function AgentBinarySelect({
31724
+ value,
31725
+ onChange,
31726
+ floatingStrategy,
31727
+ menuClassName
31728
+ }) {
31729
+ const available = useRead((api) => api("agents/available").GET());
31730
+ const availableIds = new Set((available.data ?? []).map((a2) => a2.id));
31731
+ const isMissing = (id) => available.data != null && !availableIds.has(id);
31732
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1", children: [
31733
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
31734
+ Select_default,
31735
+ {
31736
+ value,
31737
+ floatingStrategy,
31738
+ menuClassName,
31739
+ onChange: (v2) => onChange(v2),
31740
+ children: AGENT_BINARY_OPTIONS.map((o2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: o2.value, label: o2.label, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5 w-full", children: [
31741
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: isMissing(o2.value) ? "text-[#8888a0]" : "", children: o2.label }),
31742
+ isMissing(o2.value) && /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 11, className: "text-amber-500 ml-auto shrink-0" })
31743
+ ] }) }, o2.value))
31744
+ }
31745
+ ),
31746
+ isMissing(value) && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-[10px] text-amber-400", children: [
31747
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 11, className: "shrink-0" }),
31748
+ "Not installed — this agent won't run."
31749
+ ] })
31750
+ ] });
31751
+ }
31752
+ function ModelSelect({
31753
+ agentId,
31754
+ value,
31755
+ onChange,
31756
+ floatingStrategy,
31757
+ menuClassName
31758
+ }) {
31759
+ const staticOptions = MODEL_OPTIONS[agentId];
31760
+ const isDynamic = agentId === "opencode" || agentId === "cursor";
31761
+ const modelsRead = useRead(
31762
+ (api) => api("agents/models").GET({
31763
+ query: { agent: agentId === "cursor" ? "cursor" : "opencode" }
31764
+ }),
31765
+ { enabled: isDynamic }
31766
+ );
31767
+ const dynamicModels = modelsRead.data ?? [];
31768
+ const isFetching = modelsRead.fetching;
31769
+ const options = isDynamic ? dynamicModels : staticOptions;
31770
+ const [customChosen, setCustomChosen] = reactExports.useState(false);
31771
+ const isPresetValue = value === "" || options.some((o2) => o2.value === value);
31772
+ const customMode = customChosen || !isPresetValue;
31773
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
31774
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
31775
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
31776
+ Select_default,
31777
+ {
31778
+ floatingStrategy,
31779
+ menuClassName,
31780
+ value: customMode ? "__custom__" : value,
31781
+ onChange: (v2) => {
31782
+ if (v2 === "__custom__") {
31783
+ setCustomChosen(true);
31784
+ } else {
31785
+ setCustomChosen(false);
31786
+ onChange(v2);
31787
+ }
31788
+ },
31789
+ filterable: true,
31790
+ children: [
31791
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: "", label: "Default" }),
31792
+ options.map((o2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: o2.value, label: o2.label }, o2.value)),
31793
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: "__custom__", label: "Custom..." })
31794
+ ]
31795
+ }
31796
+ ) }),
31797
+ isDynamic && /* @__PURE__ */ jsxRuntimeExports.jsx(
31798
+ "button",
31799
+ {
31800
+ type: "button",
31801
+ onClick: () => void modelsRead.trigger(),
31802
+ disabled: isFetching,
31803
+ title: "Refresh model list",
31804
+ className: "flex items-center justify-center px-2 rounded border border-[var(--color-border-secondary)] bg-[#1a1a1f] hover:bg-[var(--color-surface-hover)] disabled:opacity-50 transition-colors",
31805
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31806
+ "svg",
31807
+ {
31808
+ className: classNames("w-4 h-4", isFetching ? "animate-spin" : ""),
31809
+ fill: "none",
31810
+ viewBox: "0 0 24 24",
31811
+ stroke: "currentColor",
31812
+ strokeWidth: 2,
31813
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31814
+ "path",
31815
+ {
31816
+ strokeLinecap: "round",
31817
+ strokeLinejoin: "round",
31818
+ d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
31819
+ }
31820
+ )
31821
+ }
31822
+ )
31823
+ }
31824
+ )
31825
+ ] }),
31826
+ customMode && /* @__PURE__ */ jsxRuntimeExports.jsx(
31827
+ Input_default,
31828
+ {
31829
+ value,
31830
+ onChange: (e) => onChange(e.target.value),
31831
+ placeholder: agentId === "opencode" ? "e.g. anthropic/claude-opus-4-7" : agentId === "cursor" ? "e.g. claude-opus-4-7-thinking-max" : agentId === "claude" ? "e.g. claude-opus-4-7" : "e.g. gpt-5-codex"
31832
+ }
31833
+ )
31834
+ ] });
31835
+ }
31836
+ function AgentModelPicker({
31837
+ value,
31838
+ onChange,
31839
+ floatingStrategy,
31840
+ menuClassName
31841
+ }) {
31842
+ const agentId = value.agentId ?? "claude";
31843
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-start", children: [
31844
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full sm:w-32 shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31845
+ AgentBinarySelect,
31846
+ {
31847
+ value: agentId,
31848
+ floatingStrategy,
31849
+ menuClassName,
31850
+ onChange: (v2) => onChange({ ...value, agentId: v2, model: null })
31851
+ }
31852
+ ) }),
31853
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31854
+ ModelSelect,
31855
+ {
31856
+ agentId,
31857
+ value: value.model ?? "",
31858
+ onChange: (v2) => onChange({ ...value, model: v2 || null }),
31859
+ floatingStrategy,
31860
+ menuClassName
31861
+ }
31862
+ ) }),
31863
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full sm:w-36 shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
31864
+ Select_default,
31865
+ {
31866
+ value: value.effort ?? "",
31867
+ floatingStrategy,
31868
+ menuClassName,
31869
+ onChange: (v2) => onChange({ ...value, effort: v2 || null }),
31870
+ children: [
31871
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: "", label: "Default effort" }),
31872
+ EFFORT_OPTIONS.map((o2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: o2.value, label: o2.label }, o2.value))
31873
+ ]
31874
+ }
31875
+ ) })
31876
+ ] });
31877
+ }
31878
+ function BranchSelect({ branches, value, onChange, placeholder = "Select branch" }) {
31879
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
31880
+ Select_default,
31881
+ {
31882
+ value,
31883
+ onChange: (v2) => onChange(v2),
31884
+ placeholder,
31885
+ filterable: true,
31886
+ prefix: /* @__PURE__ */ jsxRuntimeExports.jsx(GitBranch, { size: 13, className: "text-[#8888a0]" }),
31887
+ children: branches.map((b2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: b2, label: b2 }, b2))
31888
+ }
31889
+ );
31890
+ }
31159
31891
  function ConfigureStep({
31160
31892
  repoPath,
31893
+ branches,
31161
31894
  adding,
31162
31895
  onBack,
31163
31896
  onAdd
31164
31897
  }) {
31898
+ const { setValue } = useFormContext();
31165
31899
  const folderName = repoPath.split("/").filter(Boolean).at(-1) ?? repoPath;
31166
31900
  const deliveryMode = useWatch({ name: "deliveryMode" });
31901
+ const defaultBaseBranch = useWatch({ name: "defaultBaseBranch" }) ?? "";
31902
+ const assistantModel = useWatch({ name: "assistantModel" }) ?? DEFAULT_AGENT_MODEL_CHOICE;
31167
31903
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col min-h-0", children: [
31168
31904
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto p-6 flex flex-col gap-5", children: [
31169
31905
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
@@ -31194,12 +31930,36 @@ function ConfigureStep({
31194
31930
  ] })
31195
31931
  ] })
31196
31932
  ] }),
31933
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3.5", children: [
31934
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-medium uppercase text-[#4a4a5a] tracking-[1px]", children: "Git defaults" }),
31935
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-3", children: [
31936
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
31937
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[13px] text-[#c0c0d0]", children: "Default base branch" }),
31938
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] mt-0.5 text-[#4a4a5a]", children: "Used when creating new tasks and stories." })
31939
+ ] }),
31940
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-40", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31941
+ BranchSelect,
31942
+ {
31943
+ branches,
31944
+ value: defaultBaseBranch,
31945
+ onChange: (v2) => setValue("defaultBaseBranch", v2 || void 0, { shouldDirty: true }),
31946
+ placeholder: "main"
31947
+ }
31948
+ ) })
31949
+ ] })
31950
+ ] }),
31197
31951
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2.5", children: [
31198
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-medium uppercase text-[#4a4a5a] tracking-[1px]", children: "Worktree setup" }),
31199
- /* @__PURE__ */ jsxRuntimeExports.jsxs(RHFInputGroup_default, { label: "Install command", labelClassName: "text-[12px] text-[#8888a0]", className: "flex flex-col", children: [
31200
- /* @__PURE__ */ jsxRuntimeExports.jsx(RHFInput_default, { name: "installCommand", placeholder: "pnpm install" }),
31201
- /* @__PURE__ */ jsxRuntimeExports.jsx(RHFError_default, { name: "installCommand", className: "text-[11px] text-[#ef4444] mt-1" }),
31202
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] mt-1 text-[#4a4a5a]", children: "Runs once when a new worktree is created for a task." })
31952
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-medium uppercase text-[#4a4a5a] tracking-[1px]", children: "Assistant" }),
31953
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5", children: [
31954
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[13px] text-[#c0c0d0]", children: "Agent & model" }),
31955
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-[#4a4a5a]", children: "Which agent runs the in-app assistant." }),
31956
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31957
+ AgentModelPicker,
31958
+ {
31959
+ value: assistantModel,
31960
+ onChange: (next2) => setValue("assistantModel", next2, { shouldDirty: true })
31961
+ }
31962
+ ) })
31203
31963
  ] })
31204
31964
  ] })
31205
31965
  ] }),
@@ -31341,7 +32101,8 @@ function AddProjectDialog({ onClose, onAdded }) {
31341
32101
  values: {
31342
32102
  repoPath: "",
31343
32103
  deliveryMode: "off",
31344
- installCommand: ""
32104
+ defaultBaseBranch: void 0,
32105
+ assistantModel: DEFAULT_AGENT_MODEL_CHOICE
31345
32106
  }
31346
32107
  });
31347
32108
  const { control, getValues, setValue } = methods;
@@ -31364,12 +32125,19 @@ function AddProjectDialog({ onClose, onAdded }) {
31364
32125
  const pathStatus = !trimmed ? "idle" : trimmed !== debouncedPath || checking ? "checking" : checkError ? "invalid" : pathData ? pathData.valid ? "valid" : "invalid" : "checking";
31365
32126
  const pathError = checkError ? "Failed to check path" : pathStatus === "invalid" ? (pathData == null ? void 0 : pathData.error) ?? null : null;
31366
32127
  const repoInfo = pathStatus === "valid" && pathData ? { name: pathData.name, branch: pathData.branch, remote: pathData.remote } : { name: null, branch: null, remote: null };
32128
+ const currentBranch = repoInfo.branch;
32129
+ reactExports.useEffect(() => {
32130
+ if (currentBranch && !getValues("defaultBaseBranch")) {
32131
+ setValue("defaultBaseBranch", currentBranch);
32132
+ }
32133
+ }, [currentBranch, getValues, setValue]);
31367
32134
  const addProjectWrite = useWrite((api) => api("projects").POST());
31368
32135
  const handleAdd = methods.handleSubmit(async (values) => {
31369
32136
  var _a3, _b2;
31370
32137
  const initialConfig = {
31371
32138
  deliveryMode: values.deliveryMode,
31372
- worktreeSetup: ((_a3 = values.installCommand) == null ? void 0 : _a3.trim()) ? { filesToCopy: [], installCommand: values.installCommand.trim() } : void 0
32139
+ defaultBaseBranch: ((_a3 = values.defaultBaseBranch) == null ? void 0 : _a3.trim()) || void 0,
32140
+ assistantModel: values.assistantModel
31373
32141
  };
31374
32142
  const res = await addProjectWrite.trigger({
31375
32143
  body: { repoPath: values.repoPath.trim(), initialConfig }
@@ -31471,6 +32239,7 @@ function AddProjectDialog({ onClose, onAdded }) {
31471
32239
  ConfigureStep,
31472
32240
  {
31473
32241
  repoPath: getValues("repoPath"),
32242
+ branches: (pathData == null ? void 0 : pathData.branches) ?? [],
31474
32243
  adding: addProjectWrite.loading,
31475
32244
  onBack: () => setStep("select"),
31476
32245
  onAdd: () => void handleAdd()
@@ -48654,195 +49423,34 @@ const ConnectedDroppable = connect_default(makeMapStateToProps, mapDispatchToPro
48654
49423
  return {
48655
49424
  ...attachDefaultPropsToOwnProps(ownProps),
48656
49425
  ...stateProps,
48657
- ...dispatchProps
48658
- };
48659
- }, {
48660
- context: StoreContext,
48661
- areStatePropsEqual: isStrictEqual
48662
- })(Droppable);
48663
- function EmptyFolderSlot({ dp }) {
48664
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: dp.innerRef, ...dp.draggableProps, ...dp.dragHandleProps, className: "pl-10 pr-2.5 py-[3px]", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-7 border border-dashed border-[#2a2a35] rounded-md flex items-center pl-2.5 gap-1.5", children: [
48665
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1 h-1 rounded-full bg-[#2a2a35]" }),
48666
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-[#3a3a45]", children: "Drop project here" })
48667
- ] }) });
48668
- }
48669
- function FolderHeader({
48670
- folderId,
48671
- folder,
48672
- dp,
48673
- snap,
48674
- expanded,
48675
- hovered,
48676
- editing,
48677
- editName,
48678
- editRef,
48679
- onToggleCollapse,
48680
- onStartRename,
48681
- onDeleteFolder,
48682
- onEditNameChange,
48683
- onCommitRename,
48684
- onCancelRename
48685
- }) {
48686
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
48687
- "div",
48688
- {
48689
- ref: dp.innerRef,
48690
- ...dp.draggableProps,
48691
- ...dp.dragHandleProps,
48692
- onClick: () => onToggleCollapse(folderId),
48693
- className: "group flex items-center gap-1.5 h-8 pl-2.5 pr-2 rounded-md my-px mx-1 cursor-pointer select-none transition-colors",
48694
- style: {
48695
- ...dp.draggableProps.style,
48696
- background: snap.isDragging ? "#1f1f28" : hovered ? "#7c6aff15" : "transparent"
48697
- },
48698
- children: [
48699
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0 flex items-center justify-center w-3.5 text-[#4a4a5a]", children: expanded ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 11 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 11 }) }),
48700
- /* @__PURE__ */ jsxRuntimeExports.jsx(Folder, { size: 13, className: classNames("shrink-0", hovered ? "text-[#7c6aff]" : "text-[#60607a]") }),
48701
- editing ? /* @__PURE__ */ jsxRuntimeExports.jsx(
48702
- "input",
48703
- {
48704
- ref: editRef,
48705
- value: editName,
48706
- onChange: (e) => onEditNameChange(e.target.value),
48707
- onBlur: onCommitRename,
48708
- onClick: (e) => e.stopPropagation(),
48709
- onKeyDown: (e) => {
48710
- if (e.key === "Enter") onCommitRename();
48711
- if (e.key === "Escape") onCancelRename();
48712
- },
48713
- className: "flex-1 min-w-0 outline-none text-[11px] rounded px-1 bg-[#0c0c0f] border border-[#3a3a55] text-[#f0f0f5]"
48714
- }
48715
- ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
48716
- "span",
48717
- {
48718
- className: classNames(
48719
- "flex-1 min-w-0 truncate text-[11px] font-medium tracking-[0.2px]",
48720
- hovered ? "text-[#c0c0d0]" : "text-[#8888a0]"
48721
- ),
48722
- onDoubleClick: (e) => {
48723
- e.stopPropagation();
48724
- onStartRename(folderId);
48725
- },
48726
- children: folder.name
48727
- }
48728
- ),
48729
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "shrink-0 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity", children: [
48730
- /* @__PURE__ */ jsxRuntimeExports.jsx(
48731
- "button",
48732
- {
48733
- onClick: (e) => {
48734
- e.stopPropagation();
48735
- onStartRename(folderId);
48736
- },
48737
- className: "flex items-center justify-center w-5 h-5 rounded hover:bg-[#2a2a35] transition-colors text-[#60607a]",
48738
- title: "Rename",
48739
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Pencil, { size: 10 })
48740
- }
48741
- ),
48742
- /* @__PURE__ */ jsxRuntimeExports.jsx(
48743
- "button",
48744
- {
48745
- onClick: (e) => {
48746
- e.stopPropagation();
48747
- onDeleteFolder(folderId);
48748
- },
48749
- className: "flex items-center justify-center w-5 h-5 rounded hover:bg-[#ef444420] transition-colors text-[#60607a]",
48750
- title: "Delete",
48751
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 10 })
48752
- }
48753
- )
48754
- ] })
48755
- ]
48756
- }
48757
- );
48758
- }
48759
- function genId() {
48760
- return Math.random().toString(36).slice(2, 10);
48761
- }
48762
- function buildFlat(layout, expandAll, isDragging2 = false) {
48763
- var _a3, _b2;
48764
- const flat = [];
48765
- for (const item of layout.topLevel) {
48766
- if (item.type === "folder") {
48767
- flat.push({ kind: "folder-header", folderId: item.id });
48768
- const expanded = expandAll || !((_a3 = layout.folders[item.id]) == null ? void 0 : _a3.collapsed);
48769
- if (expanded) {
48770
- const projectIds = ((_b2 = layout.folders[item.id]) == null ? void 0 : _b2.projectIds) ?? [];
48771
- for (const wsId of projectIds) {
48772
- flat.push({ kind: "project", workspaceId: wsId, folderId: item.id });
48773
- }
48774
- if (isDragging2 && projectIds.length === 0) {
48775
- flat.push({ kind: "empty-folder-slot", folderId: item.id });
48776
- }
48777
- }
48778
- } else {
48779
- flat.push({ kind: "project", workspaceId: item.workspaceId, folderId: null });
48780
- }
48781
- }
48782
- return flat;
48783
- }
48784
- function flatToLayout(flat, existing) {
48785
- var _a3;
48786
- const topLevel = [];
48787
- const folders = Object.fromEntries(
48788
- Object.entries(existing.folders).map(([k, v2]) => [k, { ...v2, projectIds: [] }])
48789
- );
48790
- const seen2 = /* @__PURE__ */ new Set();
48791
- for (const item of flat) {
48792
- if (item.kind === "empty-folder-slot") continue;
48793
- if (item.kind === "folder-header") {
48794
- if (!seen2.has(item.folderId)) {
48795
- seen2.add(item.folderId);
48796
- topLevel.push({ type: "folder", id: item.folderId });
48797
- }
48798
- } else if (item.folderId !== null) {
48799
- (_a3 = folders[item.folderId]) == null ? void 0 : _a3.projectIds.push(item.workspaceId);
48800
- } else {
48801
- topLevel.push({ type: "project", workspaceId: item.workspaceId });
48802
- }
48803
- }
48804
- return { ...existing, topLevel, folders };
48805
- }
48806
- function folderAtDest(flat, destIndex) {
48807
- const after = flat[destIndex];
48808
- if (!after) {
48809
- const prev = flat[destIndex - 1];
48810
- if (!prev || prev.kind === "folder-header") return null;
48811
- if (prev.kind === "empty-folder-slot") return prev.folderId;
48812
- return prev.folderId;
48813
- }
48814
- if (after.kind === "folder-header") return after.folderId;
48815
- if (after.kind === "empty-folder-slot") return after.folderId;
48816
- return after.folderId;
48817
- }
48818
- function syncLayout(layout, projects) {
48819
- const known = new Set(projects.map((p) => p.workspaceId));
48820
- const topLevel = layout.topLevel.filter((i2) => i2.type === "folder" || known.has(i2.workspaceId));
48821
- const folders = Object.fromEntries(
48822
- Object.entries(layout.folders).map(([id, f]) => [
48823
- id,
48824
- { ...f, projectIds: f.projectIds.filter((id2) => known.has(id2)) }
48825
- ])
48826
- );
48827
- const inLayout = /* @__PURE__ */ new Set();
48828
- for (const i2 of topLevel) {
48829
- if (i2.type === "project") inLayout.add(i2.workspaceId);
48830
- }
48831
- for (const f of Object.values(folders)) {
48832
- for (const id of f.projectIds) inLayout.add(id);
48833
- }
48834
- const newItems = projects.filter((p) => !inLayout.has(p.workspaceId)).map((p) => ({ type: "project", workspaceId: p.workspaceId }));
48835
- return { ...layout, topLevel: [...topLevel, ...newItems], folders };
49426
+ ...dispatchProps
49427
+ };
49428
+ }, {
49429
+ context: StoreContext,
49430
+ areStatePropsEqual: isStrictEqual
49431
+ })(Droppable);
49432
+ function EmptyFolderSlot({ dp }) {
49433
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: dp.innerRef, ...dp.draggableProps, ...dp.dragHandleProps, className: "pl-10 pr-2.5 py-[3px]", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-7 border border-dashed border-[#2a2a35] rounded-md flex items-center pl-2.5 gap-1.5", children: [
49434
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1 h-1 rounded-full bg-[#2a2a35]" }),
49435
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-[#3a3a45]", children: "Drop project here" })
49436
+ ] }) });
48836
49437
  }
48837
- function ProjectItem({
48838
- project,
48839
- workspaceId,
49438
+ function FolderHeader({
49439
+ folderId,
49440
+ folder,
48840
49441
  dp,
48841
49442
  snap,
48842
- isActive: isActive2,
48843
- indent: indent2,
48844
- onSwitch,
48845
- onRemove
49443
+ expanded,
49444
+ hovered,
49445
+ editing,
49446
+ editName,
49447
+ editRef,
49448
+ onToggleCollapse,
49449
+ onStartRename,
49450
+ onDeleteFolder,
49451
+ onEditNameChange,
49452
+ onCommitRename,
49453
+ onCancelRename
48846
49454
  }) {
48847
49455
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
48848
49456
  "div",
@@ -48850,916 +49458,514 @@ function ProjectItem({
48850
49458
  ref: dp.innerRef,
48851
49459
  ...dp.draggableProps,
48852
49460
  ...dp.dragHandleProps,
48853
- onClick: () => onSwitch(workspaceId),
48854
- className: classNames(
48855
- "group flex items-center gap-2 h-8 pr-2 my-px mx-1 rounded-md cursor-pointer select-none transition-colors",
48856
- indent2 ? "pl-10" : "pl-3",
48857
- snap.isDragging ? "opacity-70" : isActive2 ? "" : "hover:bg-[#1a1a1f]"
48858
- ),
49461
+ onClick: () => onToggleCollapse(folderId),
49462
+ className: "group flex items-center gap-1.5 h-8 pl-2.5 pr-2 rounded-md my-px mx-1 cursor-pointer select-none transition-colors",
48859
49463
  style: {
48860
49464
  ...dp.draggableProps.style,
48861
- background: isActive2 && !snap.isDragging ? "#7c6aff18" : "transparent",
48862
- borderLeft: isActive2 && !snap.isDragging ? "2px solid #7c6aff" : "2px solid transparent"
49465
+ background: snap.isDragging ? "#1f1f28" : hovered ? "#7c6aff15" : "transparent"
48863
49466
  },
48864
49467
  children: [
48865
- /* @__PURE__ */ jsxRuntimeExports.jsx(
48866
- "div",
49468
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0 flex items-center justify-center w-3.5 text-[#4a4a5a]", children: expanded ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 11 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 11 }) }),
49469
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Folder, { size: 13, className: classNames("shrink-0", hovered ? "text-[#7c6aff]" : "text-[#60607a]") }),
49470
+ editing ? /* @__PURE__ */ jsxRuntimeExports.jsx(
49471
+ "input",
48867
49472
  {
48868
- className: classNames("w-1.5 h-1.5 rounded-full shrink-0", isActive2 ? "bg-[#7c6aff]" : "bg-[#2a2a35]"),
48869
- style: isActive2 ? { boxShadow: "0 0 6px #7c6aff80" } : void 0
49473
+ ref: editRef,
49474
+ value: editName,
49475
+ onChange: (e) => onEditNameChange(e.target.value),
49476
+ onBlur: onCommitRename,
49477
+ onClick: (e) => e.stopPropagation(),
49478
+ onKeyDown: (e) => {
49479
+ if (e.key === "Enter") onCommitRename();
49480
+ if (e.key === "Escape") onCancelRename();
49481
+ },
49482
+ className: "flex-1 min-w-0 outline-none text-[11px] rounded px-1 bg-[#0c0c0f] border border-[#3a3a55] text-[#f0f0f5]"
48870
49483
  }
48871
- ),
48872
- /* @__PURE__ */ jsxRuntimeExports.jsx(
49484
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
48873
49485
  "span",
48874
49486
  {
48875
49487
  className: classNames(
48876
- "flex-1 min-w-0 truncate text-[12px]",
48877
- isActive2 ? "text-[#f0f0f5] font-medium" : "text-[#8888a0] font-normal"
49488
+ "flex-1 min-w-0 truncate text-[11px] font-medium tracking-[0.2px]",
49489
+ hovered ? "text-[#c0c0d0]" : "text-[#8888a0]"
48878
49490
  ),
48879
- children: project.name
48880
- }
48881
- ),
48882
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "shrink-0 flex items-center opacity-0 group-hover:opacity-100 transition-opacity", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
48883
- "button",
48884
- {
48885
- onClick: (e) => {
49491
+ onDoubleClick: (e) => {
48886
49492
  e.stopPropagation();
48887
- ConfirmDialog_default.show({
48888
- title: "Remove project",
48889
- content: `Remove "${project.name}" from Whipped? This will stop all running agents and delete all associated worktrees and data.`,
48890
- confirmButtonLabel: "Remove",
48891
- cancelButtonLabel: "Cancel",
48892
- onConfirm: async ({ dismiss: dismiss2 }) => {
48893
- await onRemove(workspaceId);
48894
- dismiss2();
48895
- }
48896
- });
49493
+ onStartRename(folderId);
48897
49494
  },
48898
- className: "flex items-center justify-center w-5 h-5 rounded hover:bg-[#ef444420] transition-colors text-[#60607a] hover:text-[#ef4444]",
48899
- title: "Remove project",
48900
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 10 })
48901
- }
48902
- ) })
48903
- ]
48904
- }
48905
- );
48906
- }
48907
- const ProjectsSidebar = React.forwardRef(function ProjectsSidebar2({ projects, activeWorkspaceId, onSwitch, onRemove }, ref) {
48908
- const [layout, setLayout] = reactExports.useState({ version: 1, topLevel: [], folders: {} });
48909
- const [isDragging2, setIsDragging] = reactExports.useState(false);
48910
- const [hoveredFolderId, setHoveredFolderId] = reactExports.useState(null);
48911
- const [editingId, setEditingId] = reactExports.useState(null);
48912
- const [editName, setEditName] = reactExports.useState("");
48913
- const editRef = reactExports.useRef(null);
48914
- const saveTimer = reactExports.useRef(null);
48915
- const { trigger: fetchLayout } = useRead((api) => api("projects/layout").GET(), { enabled: false });
48916
- const { trigger: saveLayout } = useWrite((api) => api("projects/layout").PUT());
48917
- reactExports.useEffect(() => {
48918
- fetchLayout().then((res) => setLayout((prev) => syncLayout(res.data ?? prev, projects))).catch(() => {
48919
- });
48920
- }, []);
48921
- reactExports.useEffect(() => {
48922
- setLayout((prev) => syncLayout(prev, projects));
48923
- }, [projects]);
48924
- const persist = (next2) => {
48925
- if (saveTimer.current) clearTimeout(saveTimer.current);
48926
- saveTimer.current = setTimeout(() => {
48927
- saveLayout({ body: next2 }).catch(() => {
48928
- });
48929
- }, 300);
48930
- };
48931
- const update2 = (next2) => {
48932
- setLayout(next2);
48933
- persist(next2);
48934
- };
48935
- const addFolder = () => {
48936
- const id = genId();
48937
- const next2 = {
48938
- ...layout,
48939
- topLevel: [...layout.topLevel, { type: "folder", id }],
48940
- folders: { ...layout.folders, [id]: { id, name: "New Folder", collapsed: false, projectIds: [] } }
48941
- };
48942
- update2(next2);
48943
- setEditingId(id);
48944
- setEditName("New Folder");
48945
- setTimeout(() => {
48946
- var _a3, _b2;
48947
- (_a3 = editRef.current) == null ? void 0 : _a3.focus();
48948
- (_b2 = editRef.current) == null ? void 0 : _b2.select();
48949
- }, 50);
48950
- };
48951
- reactExports.useImperativeHandle(ref, () => ({ addFolder }));
48952
- const startRename = (id) => {
48953
- var _a3;
48954
- setEditingId(id);
48955
- setEditName(((_a3 = layout.folders[id]) == null ? void 0 : _a3.name) ?? "");
48956
- setTimeout(() => {
48957
- var _a4, _b2;
48958
- (_a4 = editRef.current) == null ? void 0 : _a4.focus();
48959
- (_b2 = editRef.current) == null ? void 0 : _b2.select();
48960
- }, 50);
48961
- };
48962
- const commitRename = () => {
48963
- if (!editingId) return;
48964
- update2({
48965
- ...layout,
48966
- folders: {
48967
- ...layout.folders,
48968
- [editingId]: { ...layout.folders[editingId], name: editName.trim() || "Untitled" }
48969
- }
48970
- });
48971
- setEditingId(null);
48972
- };
48973
- const deleteFolder = (id) => {
48974
- const folder = layout.folders[id];
48975
- if (!folder) return;
48976
- const idx = layout.topLevel.findIndex((i2) => i2.type === "folder" && i2.id === id);
48977
- const returned = folder.projectIds.map((ws2) => ({ type: "project", workspaceId: ws2 }));
48978
- const topLevel = [...layout.topLevel];
48979
- topLevel.splice(idx, 1, ...returned);
48980
- const folders = { ...layout.folders };
48981
- delete folders[id];
48982
- update2({ ...layout, topLevel, folders });
48983
- };
48984
- const toggleCollapse = (id) => {
48985
- const f = layout.folders[id];
48986
- if (!f) return;
48987
- update2({ ...layout, folders: { ...layout.folders, [id]: { ...f, collapsed: !f.collapsed } } });
48988
- };
48989
- const onDragUpdate2 = reactExports.useCallback(
48990
- (update22) => {
48991
- if (!update22.destination) {
48992
- setHoveredFolderId(null);
48993
- return;
48994
- }
48995
- const flat2 = buildFlat(layout, true, true);
48996
- flat2.splice(update22.source.index, 1);
48997
- const folderId = folderAtDest(flat2, update22.destination.index);
48998
- setHoveredFolderId(folderId);
48999
- },
49000
- [layout]
49001
- );
49002
- const onDragEnd2 = (result) => {
49003
- setIsDragging(false);
49004
- setHoveredFolderId(null);
49005
- if (!result.destination) return;
49006
- const { draggableId, source, destination } = result;
49007
- if (source.index === destination.index) return;
49008
- const flat2 = buildFlat(layout, true, true);
49009
- if (draggableId.startsWith("fh:")) {
49010
- const folderId = draggableId.slice(3);
49011
- flat2.splice(source.index, 1);
49012
- const group = [];
49013
- while (flat2[source.index] && (flat2[source.index].kind === "project" && flat2[source.index].folderId === folderId || flat2[source.index].kind === "empty-folder-slot" && flat2[source.index].folderId === folderId)) {
49014
- group.push(flat2.splice(source.index, 1)[0]);
49015
- }
49016
- const at2 = Math.min(destination.index, flat2.length);
49017
- flat2.splice(at2, 0, { kind: "folder-header", folderId }, ...group);
49018
- } else {
49019
- const [moved] = flat2.splice(source.index, 1);
49020
- const newFolderId = folderAtDest(flat2, destination.index);
49021
- flat2.splice(destination.index, 0, { ...moved, folderId: newFolderId });
49022
- }
49023
- update2(flatToLayout(flat2, layout));
49024
- };
49025
- const projectMap = Object.fromEntries(projects.map((p) => [p.workspaceId, p]));
49026
- const flat = buildFlat(layout, isDragging2, isDragging2);
49027
- return /* @__PURE__ */ jsxRuntimeExports.jsx(DragDropContext, { onDragStart: () => setIsDragging(true), onDragUpdate: onDragUpdate2, onDragEnd: onDragEnd2, children: /* @__PURE__ */ jsxRuntimeExports.jsx(ConnectedDroppable, { droppableId: "sidebar", children: (provided) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: provided.innerRef, ...provided.droppableProps, className: "flex flex-col", children: [
49028
- flat.map((item, index2) => {
49029
- if (item.kind === "folder-header") {
49030
- const folder = layout.folders[item.folderId];
49031
- if (!folder) return null;
49032
- const expanded = !folder.collapsed || isDragging2;
49033
- return /* @__PURE__ */ jsxRuntimeExports.jsx(PublicDraggable, { draggableId: `fh:${item.folderId}`, index: index2, children: (dp, snap) => /* @__PURE__ */ jsxRuntimeExports.jsx(
49034
- FolderHeader,
49035
- {
49036
- folderId: item.folderId,
49037
- folder,
49038
- dp,
49039
- snap,
49040
- expanded,
49041
- hovered: hoveredFolderId === item.folderId,
49042
- editing: editingId === item.folderId,
49043
- editName,
49044
- editRef,
49045
- onToggleCollapse: toggleCollapse,
49046
- onStartRename: startRename,
49047
- onDeleteFolder: deleteFolder,
49048
- onEditNameChange: setEditName,
49049
- onCommitRename: commitRename,
49050
- onCancelRename: () => setEditingId(null)
49051
- }
49052
- ) }, `fh:${item.folderId}`);
49053
- }
49054
- if (item.kind === "empty-folder-slot") {
49055
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
49056
- PublicDraggable,
49057
- {
49058
- draggableId: `slot:${item.folderId}`,
49059
- index: index2,
49060
- isDragDisabled: true,
49061
- children: (dp) => /* @__PURE__ */ jsxRuntimeExports.jsx(EmptyFolderSlot, { dp })
49062
- },
49063
- `slot:${item.folderId}`
49064
- );
49065
- }
49066
- const project = projectMap[item.workspaceId];
49067
- if (!project) return null;
49068
- const isActive2 = item.workspaceId === activeWorkspaceId;
49069
- const indent2 = item.folderId !== null;
49070
- return /* @__PURE__ */ jsxRuntimeExports.jsx(PublicDraggable, { draggableId: `p:${item.workspaceId}`, index: index2, children: (dp, snap) => /* @__PURE__ */ jsxRuntimeExports.jsx(
49071
- ProjectItem,
49072
- {
49073
- project,
49074
- workspaceId: item.workspaceId,
49075
- dp,
49076
- snap,
49077
- isActive: isActive2,
49078
- indent: indent2,
49079
- onSwitch,
49080
- onRemove
49081
- }
49082
- ) }, `p:${item.workspaceId}`);
49083
- }),
49084
- provided.placeholder
49085
- ] }) }) });
49086
- });
49087
- function CardDetailHeader({
49088
- card,
49089
- workspaceId,
49090
- projectName,
49091
- externalUrl,
49092
- isStory,
49093
- isReadyForReview,
49094
- hasStartCommand = false,
49095
- merging,
49096
- onMerge,
49097
- onPR,
49098
- onDelete,
49099
- onClose
49100
- }) {
49101
- var _a3, _b2, _c3;
49102
- const { session: runSession, start: startRun, stop: stopRun } = useRunSession(workspaceId);
49103
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 px-6 py-2.5 border-b border-[#2a2a35] bg-[#141418] shrink-0", children: [
49104
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "text-[#60607a] hover:text-gray-300 transition-colors", title: "Back to board", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 18 }) }),
49105
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-[18px] bg-[#2a2a35] shrink-0" }),
49106
- projectName && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
49107
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-[#60607a]", children: projectName }),
49108
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-[#2a2a35]", children: "/" })
49109
- ] }),
49110
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[13px] font-semibold text-[#f0f0f5] truncate", children: ((_a3 = card.description) == null ? void 0 : _a3.split("\n")[0]) ?? card.id }),
49111
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1" }),
49112
- externalUrl && !((_b2 = card.pr) == null ? void 0 : _b2.url) && /* @__PURE__ */ jsxRuntimeExports.jsx(
49113
- "a",
49114
- {
49115
- href: externalUrl,
49116
- target: "_blank",
49117
- rel: "noreferrer",
49118
- className: "text-[#60607a] hover:text-gray-300 transition-colors",
49119
- title: "Open external link",
49120
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { size: 15 })
49121
- }
49122
- ),
49123
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-[18px] bg-[#2a2a35] shrink-0" }),
49124
- hasStartCommand && /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: runSession.status === "running" && runSession.cardId === card.id ? /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip_default, { delayDuration: 0, content: "Stop", side: "bottom", triggerAsChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49125
- "button",
49126
- {
49127
- onClick: () => void stopRun(),
49128
- className: "cursor-pointer text-[#60607a] hover:text-red-400 transition-colors",
49129
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Square, { size: 15, className: "fill-current" })
49130
- }
49131
- ) }) : /* @__PURE__ */ jsxRuntimeExports.jsx(
49132
- Tooltip_default,
49133
- {
49134
- delayDuration: 0,
49135
- content: runSession.status === "running" ? "Another task is running" : "Run",
49136
- side: "bottom",
49137
- triggerAsChild: true,
49138
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49139
- "button",
49140
- {
49141
- onClick: () => void startRun(card.id),
49142
- disabled: runSession.status === "running",
49143
- className: "cursor-pointer text-[#60607a] hover:text-emerald-400 transition-colors disabled:opacity-40 disabled:cursor-not-allowed",
49144
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 15 })
49495
+ children: folder.name
49145
49496
  }
49146
- )
49147
- }
49148
- ) }),
49149
- !isStory && isReadyForReview && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
49150
- /* @__PURE__ */ jsxRuntimeExports.jsx(
49151
- Tooltip_default,
49152
- {
49153
- delayDuration: 0,
49154
- content: merging ? "Merging..." : `Merge into ${card.baseRef}`,
49155
- side: "bottom",
49156
- triggerAsChild: true,
49157
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49497
+ ),
49498
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "shrink-0 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity", children: [
49499
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
49158
49500
  "button",
49159
49501
  {
49160
- onClick: onMerge,
49161
- disabled: merging,
49162
- className: "cursor-pointer text-[#60607a] hover:text-emerald-400 transition-colors disabled:opacity-40",
49163
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(GitMerge, { size: 15 })
49502
+ onClick: (e) => {
49503
+ e.stopPropagation();
49504
+ onStartRename(folderId);
49505
+ },
49506
+ className: "flex items-center justify-center w-5 h-5 rounded hover:bg-[#2a2a35] transition-colors text-[#60607a]",
49507
+ title: "Rename",
49508
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Pencil, { size: 10 })
49509
+ }
49510
+ ),
49511
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
49512
+ "button",
49513
+ {
49514
+ onClick: (e) => {
49515
+ e.stopPropagation();
49516
+ onDeleteFolder(folderId);
49517
+ },
49518
+ className: "flex items-center justify-center w-5 h-5 rounded hover:bg-[#ef444420] transition-colors text-[#60607a]",
49519
+ title: "Delete",
49520
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 10 })
49164
49521
  }
49165
49522
  )
49166
- }
49167
- ),
49168
- ((_c3 = card.pr) == null ? void 0 : _c3.url) ? /* @__PURE__ */ jsxRuntimeExports.jsx(
49169
- "a",
49170
- {
49171
- href: card.pr.url,
49172
- target: "_blank",
49173
- rel: "noreferrer",
49174
- title: "Open Pull Request",
49175
- className: "cursor-pointer text-green-400 hover:text-green-300 transition-colors",
49176
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(GitPullRequest, { size: 15 })
49177
- }
49178
- ) : /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip_default, { delayDuration: 0, content: `Create PR against ${card.baseRef}`, side: "bottom", triggerAsChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49179
- "button",
49180
- {
49181
- onClick: onPR,
49182
- disabled: merging,
49183
- className: "cursor-pointer text-[#60607a] hover:text-green-400 transition-colors disabled:opacity-40",
49184
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(GitPullRequest, { size: 15 })
49185
- }
49186
- ) })
49187
- ] }),
49188
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-[18px] bg-[#2a2a35] shrink-0" }),
49189
- /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip_default, { delayDuration: 0, content: "Delete task", side: "bottom", triggerAsChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49190
- "button",
49191
- {
49192
- onClick: onDelete,
49193
- className: "cursor-pointer text-[#60607a] hover:text-red-400 transition-colors",
49194
- title: "Delete task",
49195
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 15 })
49196
- }
49197
- ) })
49198
- ] });
49523
+ ] })
49524
+ ]
49525
+ }
49526
+ );
49199
49527
  }
49200
- const runtimeAgentIdSchema = _enum(["claude", "codex", "opencode", "cursor"]);
49201
- const AGENT_BINARY_OPTIONS = [
49202
- { value: "claude", label: "claude" },
49203
- { value: "codex", label: "codex" },
49204
- { value: "opencode", label: "opencode" },
49205
- { value: "cursor", label: "cursor" }
49206
- ];
49207
- const effortLevelSchema = _enum(["low", "medium", "high", "xhigh", "max"]);
49208
- const EFFORT_OPTIONS = [
49209
- { value: "low", label: "Low" },
49210
- { value: "medium", label: "Medium" },
49211
- { value: "high", label: "High" },
49212
- { value: "xhigh", label: "Extra High" },
49213
- { value: "max", label: "Max" }
49214
- ];
49215
- const MODEL_OPTIONS = {
49216
- claude: [
49217
- { value: "claude-opus-4-8", label: "Opus 4.8" },
49218
- { value: "claude-opus-4-7", label: "Opus 4.7" },
49219
- { value: "claude-opus-4-6", label: "Opus 4.6" },
49220
- { value: "claude-sonnet-4-6", label: "Sonnet 4.6" },
49221
- { value: "claude-sonnet-4-5", label: "Sonnet 4.5" },
49222
- { value: "claude-haiku-4-5", label: "Haiku 4.5" }
49223
- ],
49224
- codex: [
49225
- { value: "gpt-5.5", label: "GPT-5.5 (default)" },
49226
- { value: "gpt-5.4", label: "GPT-5.4" },
49227
- { value: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
49228
- { value: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
49229
- { value: "gpt-5.2", label: "GPT-5.2" }
49230
- ],
49231
- // opencode supports any provider/model string — no fixed presets.
49232
- // The UI renders a free-form text input for opencode model selection.
49233
- opencode: [],
49234
- // cursor supports many models — no fixed presets.
49235
- // The UI fetches the live list via agents.cursorModels and renders a Select.
49236
- cursor: []
49237
- };
49238
- const agentModelChoiceSchema = object({
49239
- agentId: runtimeAgentIdSchema.default("claude"),
49240
- model: string$2().nullable().optional(),
49241
- effort: effortLevelSchema.nullable().optional()
49242
- });
49243
- const DEFAULT_AGENT_MODEL_CHOICE = { agentId: "claude", model: null, effort: null };
49244
- const workflowSlotTypeSchema = _enum(["dev", "review", "plan", "orch"]);
49245
- const tierLevelSchema = _enum(["minimal", "low", "medium", "high", "max"]);
49246
- const LEVEL_ORDER = ["minimal", "low", "medium", "high", "max"];
49247
- const TIER_LEVEL_OPTIONS = [
49248
- { value: "minimal", label: "Minimal" },
49249
- { value: "low", label: "Low" },
49250
- { value: "medium", label: "Medium" },
49251
- { value: "high", label: "High" },
49252
- { value: "max", label: "Max" }
49253
- ];
49254
- const modelPairSchema = object({
49255
- id: string$2(),
49256
- level: tierLevelSchema,
49257
- isFree: boolean$1().default(false),
49258
- binary: runtimeAgentIdSchema,
49259
- model: string$2().nullable().optional(),
49260
- effort: effortLevelSchema.nullable().optional()
49261
- });
49262
- const pairSelectionModeSchema = _enum(["auto", "preferFree", "freeOnly", "paidOnly"]);
49263
- const PAIR_SELECTION_MODE_OPTIONS = [
49264
- { value: "auto", label: "Auto (priority)" },
49265
- { value: "preferFree", label: "Prefer free" },
49266
- { value: "freeOnly", label: "Free only" },
49267
- { value: "paidOnly", label: "Paid only" }
49268
- ];
49269
- const SLOT_TOOL_IDS = ["browser"];
49270
- const slotToolSchema = _enum(SLOT_TOOL_IDS);
49271
- const slotModelConfigSchema = object({
49272
- pairs: array(modelPairSchema).min(1),
49273
- mode: pairSelectionModeSchema.default("auto"),
49274
- pinnedPairId: string$2().optional()
49275
- });
49276
- const cardModelConfigSchema = record(string$2(), slotModelConfigSchema);
49277
- function pickByMode(candidates, mode) {
49278
- switch (mode) {
49279
- case "preferFree":
49280
- return candidates.find((p) => p.isFree) ?? candidates[0];
49281
- case "freeOnly":
49282
- return candidates.find((p) => p.isFree);
49283
- case "paidOnly":
49284
- return candidates.find((p) => !p.isFree);
49285
- default:
49286
- return candidates[0];
49528
+ function genId() {
49529
+ return Math.random().toString(36).slice(2, 10);
49530
+ }
49531
+ function buildFlat(layout, expandAll, isDragging2 = false) {
49532
+ var _a3, _b2;
49533
+ const flat = [];
49534
+ for (const item of layout.topLevel) {
49535
+ if (item.type === "folder") {
49536
+ flat.push({ kind: "folder-header", folderId: item.id });
49537
+ const expanded = expandAll || !((_a3 = layout.folders[item.id]) == null ? void 0 : _a3.collapsed);
49538
+ if (expanded) {
49539
+ const projectIds = ((_b2 = layout.folders[item.id]) == null ? void 0 : _b2.projectIds) ?? [];
49540
+ for (const wsId of projectIds) {
49541
+ flat.push({ kind: "project", workspaceId: wsId, folderId: item.id });
49542
+ }
49543
+ if (isDragging2 && projectIds.length === 0) {
49544
+ flat.push({ kind: "empty-folder-slot", folderId: item.id });
49545
+ }
49546
+ }
49547
+ } else {
49548
+ flat.push({ kind: "project", workspaceId: item.workspaceId, folderId: null });
49549
+ }
49287
49550
  }
49551
+ return flat;
49288
49552
  }
49289
- function resolvePair(cfg, activeLevel) {
49290
- if (cfg.pinnedPairId) {
49291
- const pinned = cfg.pairs.find((p) => p.id === cfg.pinnedPairId);
49292
- if (pinned) return pinned;
49293
- }
49294
- const startIdx = LEVEL_ORDER.indexOf(activeLevel);
49295
- const order2 = [];
49296
- for (let i2 = startIdx; i2 < LEVEL_ORDER.length; i2++) order2.push(i2);
49297
- for (let i2 = startIdx - 1; i2 >= 0; i2--) order2.push(i2);
49298
- for (const i2 of order2) {
49299
- const candidates = cfg.pairs.filter((p) => p.level === LEVEL_ORDER[i2]);
49300
- if (candidates.length === 0) continue;
49301
- const pick3 = pickByMode(candidates, cfg.mode);
49302
- if (pick3) return pick3;
49553
+ function flatToLayout(flat, existing) {
49554
+ var _a3;
49555
+ const topLevel = [];
49556
+ const folders = Object.fromEntries(
49557
+ Object.entries(existing.folders).map(([k, v2]) => [k, { ...v2, projectIds: [] }])
49558
+ );
49559
+ const seen2 = /* @__PURE__ */ new Set();
49560
+ for (const item of flat) {
49561
+ if (item.kind === "empty-folder-slot") continue;
49562
+ if (item.kind === "folder-header") {
49563
+ if (!seen2.has(item.folderId)) {
49564
+ seen2.add(item.folderId);
49565
+ topLevel.push({ type: "folder", id: item.folderId });
49566
+ }
49567
+ } else if (item.folderId !== null) {
49568
+ (_a3 = folders[item.folderId]) == null ? void 0 : _a3.projectIds.push(item.workspaceId);
49569
+ } else {
49570
+ topLevel.push({ type: "project", workspaceId: item.workspaceId });
49571
+ }
49303
49572
  }
49304
- for (const i2 of order2) {
49305
- const candidates = cfg.pairs.filter((p) => p.level === LEVEL_ORDER[i2]);
49306
- if (candidates[0]) return candidates[0];
49573
+ return { ...existing, topLevel, folders };
49574
+ }
49575
+ function folderAtDest(flat, destIndex) {
49576
+ const after = flat[destIndex];
49577
+ if (!after) {
49578
+ const prev = flat[destIndex - 1];
49579
+ if (!prev || prev.kind === "folder-header") return null;
49580
+ if (prev.kind === "empty-folder-slot") return prev.folderId;
49581
+ return prev.folderId;
49307
49582
  }
49308
- const fallback = cfg.pairs[0];
49309
- if (!fallback) throw new Error("resolvePair: slot has no model pairs");
49310
- return fallback;
49583
+ if (after.kind === "folder-header") return after.folderId;
49584
+ if (after.kind === "empty-folder-slot") return after.folderId;
49585
+ return after.folderId;
49311
49586
  }
49312
- const promptValueSchema = preprocess$1(
49313
- (v2) => {
49314
- if (typeof v2 === "string") return { source: "inline", text: v2 };
49315
- return v2;
49316
- },
49317
- discriminatedUnion("source", [
49318
- object({ source: literal("inline"), text: string$2() }),
49319
- object({ source: literal("file"), path: string$2() })
49320
- ])
49321
- );
49322
- const EMPTY_INLINE_PROMPT = { source: "inline", text: "" };
49323
- const workflowSlotSchema = object({
49324
- id: string$2(),
49325
- type: workflowSlotTypeSchema,
49326
- name: string$2(),
49327
- order: number$2().int().nonnegative(),
49328
- enabled: boolean$1(),
49329
- prompt: promptValueSchema.default(EMPTY_INLINE_PROMPT),
49330
- // Model tiers for this slot, in priority order (top = highest). Copied to the
49331
- // card at creation; the card's active level + mode select which pair runs.
49332
- pairs: array(modelPairSchema).min(1),
49333
- mode: pairSelectionModeSchema.default("auto"),
49334
- // Tools this slot may use (e.g. "browser"). Workflow-only, not ticket-editable.
49335
- tools: array(slotToolSchema).default([]),
49336
- // review slots only: may set the card's active level on reopen.
49337
- canAdjustLevel: boolean$1().default(false),
49338
- // plan slots only: re-run even if a plan already exists on the card.
49339
- rerun: boolean$1().default(false)
49340
- });
49341
- const workflowSchema = object({
49342
- id: string$2(),
49343
- name: string$2(),
49344
- isDefault: boolean$1().default(false),
49345
- forStory: boolean$1().default(false),
49346
- slots: array(workflowSlotSchema)
49347
- });
49348
- function highestWorkflowLevel(workflow) {
49349
- let bestIdx = -1;
49350
- for (const slot of (workflow == null ? void 0 : workflow.slots) ?? []) {
49351
- for (const p of slot.pairs) bestIdx = Math.max(bestIdx, LEVEL_ORDER.indexOf(p.level));
49587
+ function syncLayout(layout, projects) {
49588
+ const known = new Set(projects.map((p) => p.workspaceId));
49589
+ const topLevel = layout.topLevel.filter((i2) => i2.type === "folder" || known.has(i2.workspaceId));
49590
+ const folders = Object.fromEntries(
49591
+ Object.entries(layout.folders).map(([id, f]) => [
49592
+ id,
49593
+ { ...f, projectIds: f.projectIds.filter((id2) => known.has(id2)) }
49594
+ ])
49595
+ );
49596
+ const inLayout = /* @__PURE__ */ new Set();
49597
+ for (const i2 of topLevel) {
49598
+ if (i2.type === "project") inLayout.add(i2.workspaceId);
49352
49599
  }
49353
- return LEVEL_ORDER[bestIdx] ?? "medium";
49600
+ for (const f of Object.values(folders)) {
49601
+ for (const id of f.projectIds) inLayout.add(id);
49602
+ }
49603
+ const newItems = projects.filter((p) => !inLayout.has(p.workspaceId)).map((p) => ({ type: "project", workspaceId: p.workspaceId }));
49604
+ return { ...layout, topLevel: [...topLevel, ...newItems], folders };
49354
49605
  }
49355
- const DEFAULT_GIT_INSTRUCTIONS = `# Git conventions
49356
-
49357
- These rules govern how to write commit messages, PR titles, and PR
49358
- descriptions.
49359
-
49360
- ## PR title
49361
- - Imperative, present tense: "Add board view", "Fix race in poller".
49362
- Not past tense, not gerund.
49363
- - ≤70 characters; aim for 50.
49364
- - Describe what shipped, not the task. "Add board view" beats
49365
- "Implement board view feature".
49366
- - No prefixes like \`feat:\` / \`[FEAT]\` / \`fix:\`.
49367
- - No ticket IDs in the title (put them in the description if needed).
49368
- - No trailing period.
49369
-
49370
- ## PR description
49371
- Keep it focused. Two sections, nothing more unless genuinely useful:
49372
-
49373
- ## Summary
49374
- - What changed and why. Use as many bullets as the scope warrants —
49375
- a one-line fix is one bullet; a refactor touching 20 files may
49376
- need ten. Don't pad, don't truncate.
49377
-
49378
- ## Test plan
49379
- - What you actually ran or clicked, and the outcome.
49380
- - Type-check and lint passing are not a test plan on their own.
49381
- - If something couldn't be verified, say so in one line.
49382
-
49383
- Do NOT include:
49384
- - Iteration narration ("Round N", "addressed feedback", "after review").
49385
- - Commit SHAs or branch names — GitHub already shows both.
49386
- - Paths to internal planning docs, scratch files, or task tracker URLs.
49387
- - "Verification:" sections that only list a passing type-check or lint.
49388
- - Self-congratulation ("clean", "all checks pass", "ready to merge").
49389
- - Restating the task description verbatim.
49390
-
49391
- ## Commit messages
49392
- - Short imperative subject line, ≤72 chars. That's usually enough.
49393
- - Skip the body unless a reviewer reading the diff alone would be
49394
- confused about *why* the change exists.
49395
- - Reference an issue only if a concrete one exists to close
49396
- (\`Closes #123\`). Never invent issue numbers.
49397
- `;
49398
- const runtimeBoardColumnIdSchema = _enum([
49399
- "todo",
49400
- "in_progress",
49401
- "reopened",
49402
- "ready_for_review",
49403
- "blocked",
49404
- "done"
49405
- ]);
49406
- const reviewActorSchema = object({
49407
- type: _enum(["ai", "human", "external"]),
49408
- id: string$2(),
49409
- source: string$2().optional()
49410
- });
49411
- const reviewIssueSchema = object({
49412
- file: string$2().optional(),
49413
- line: number$2().optional(),
49414
- severity: _enum(["blocking", "warning", "info"]),
49415
- message: string$2()
49416
- });
49417
- const reviewAttachmentSchema = object({
49418
- type: string$2(),
49419
- // "image" | "file" | any mime category
49420
- name: string$2(),
49421
- mimeType: string$2(),
49422
- path: string$2()
49423
- // absolute path in ~/.whipped/attachments/
49424
- });
49425
- const runtimeReviewCommentSchema = object({
49426
- id: string$2(),
49427
- type: string$2(),
49428
- actor: reviewActorSchema,
49429
- status: _enum(["pass", "fail", "warning", "skipped"]).optional(),
49430
- createdAt: number$2(),
49431
- streamId: string$2().optional(),
49432
- summary: string$2(),
49433
- issues: array(reviewIssueSchema).optional(),
49434
- attachments: array(reviewAttachmentSchema).optional(),
49435
- metadata: record(string$2(), unknown$1()).optional()
49436
- });
49437
- const runtimeActivityEntrySchema = object({
49438
- timestamp: number$2(),
49439
- message: string$2()
49440
- });
49441
- const runtimeTaskSessionStateSchema = _enum(["running", "stopped", "completed", "failed", "killed"]);
49442
- const runtimeTerminalSessionEntrySchema = object({
49443
- streamId: string$2(),
49444
- type: string$2(),
49445
- startedAt: number$2(),
49446
- endedAt: number$2().optional(),
49447
- agentId: runtimeAgentIdSchema.optional(),
49448
- state: runtimeTaskSessionStateSchema.optional()
49449
- });
49450
- const runtimeCardPrioritySchema = _enum(["urgent", "high", "medium", "low"]);
49451
- const cardTypeSchema = _enum(["task", "story", "subtask"]);
49452
- const runtimePrMetaSchema = object({
49453
- url: string$2().optional(),
49454
- title: string$2().optional(),
49455
- description: string$2().optional(),
49456
- updatedAt: number$2().optional(),
49457
- updatedBy: string$2().optional()
49458
- });
49459
- const runtimeBoardCardSchema = object({
49460
- id: string$2(),
49461
- description: string$2(),
49462
- descriptionAttachments: array(reviewAttachmentSchema).optional().default([]),
49463
- columnId: runtimeBoardColumnIdSchema,
49464
- type: cardTypeSchema.default("task"),
49465
- readyForDev: boolean$1().default(false),
49466
- agentId: runtimeAgentIdSchema.optional(),
49467
- priority: runtimeCardPrioritySchema.optional(),
49468
- // Single-parent stacking: this card continues in the parent's worktree/branch
49469
- // and starts once the parent reaches ready_for_review. Mutually exclusive with waitsFor.
49470
- dependsOn: string$2().optional(),
49471
- // Many-parent gate (tasks only): this card starts only once ALL listed cards are
49472
- // done (merged), in a fresh worktree branched from baseRef. Mutually exclusive with dependsOn.
49473
- waitsFor: array(string$2()).default([]),
49474
- // Story-only: the IDs of this story's subtasks. The story triggers its orchestrator
49475
- // workflow once every subtask reaches ready_for_review.
49476
- subtaskIds: array(string$2()).default([]),
49477
- autoFixAttempts: number$2().int().nonnegative().default(0),
49478
- baseRef: string$2(),
49479
- createdAt: number$2(),
49480
- updatedAt: number$2(),
49481
- githubIssueUrl: string$2().optional(),
49482
- pr: runtimePrMetaSchema.optional(),
49483
- workflowId: string$2().optional(),
49484
- // Plan written by the one-shot plan agent; injected into the dev agent's prompt.
49485
- plan: string$2().optional(),
49486
- // Workflow-wide capability level; every slot resolves it to its own pair.
49487
- activeLevel: tierLevelSchema.default("medium"),
49488
- // Per-slot model config, snapshotted from the workflow at creation and editable
49489
- // per ticket (slotId → {pairs, mode, pinnedPairId}).
49490
- modelConfig: cardModelConfigSchema.optional(),
49491
- reviewComments: array(runtimeReviewCommentSchema).default([]),
49492
- activityLog: array(runtimeActivityEntrySchema).default([]),
49493
- terminalSessions: array(runtimeTerminalSessionEntrySchema).default([]),
49494
- githubCommentIds: array(string$2()).default([]),
49495
- worktreePath: string$2().optional(),
49496
- branchName: string$2().optional(),
49497
- slackMessageTs: string$2().optional(),
49498
- slackChannelId: string$2().optional()
49499
- });
49500
- const runtimeBoardColumnSchema = object({
49501
- id: runtimeBoardColumnIdSchema,
49502
- title: string$2(),
49503
- taskIds: array(string$2())
49504
- });
49505
- const runtimeBoardDataSchema = object({
49506
- columns: array(runtimeBoardColumnSchema),
49507
- cards: record(string$2(), runtimeBoardCardSchema)
49508
- });
49509
- const runtimeGlobalConfigSchema = object({
49510
- defaultAgent: runtimeAgentIdSchema.default("claude"),
49511
- maxParallelTasks: number$2().int().positive().default(4),
49512
- maxParallelQA: number$2().int().positive().default(1),
49513
- maxAutoFixAttempts: number$2().int().nonnegative().default(3),
49514
- pollingIntervalSeconds: number$2().int().positive().default(30),
49515
- prPollingIntervalSeconds: number$2().int().positive().default(60),
49516
- terminalApp: string$2().optional(),
49517
- slackEnabled: boolean$1().default(true),
49518
- slackBotToken: string$2().optional(),
49519
- slackSigningSecret: string$2().optional(),
49520
- slackAppConfigToken: string$2().optional(),
49521
- slackClientId: string$2().optional(),
49522
- slackClientSecret: string$2().optional(),
49523
- slackAppId: string$2().optional(),
49524
- slackOauthAuthorizeUrl: string$2().optional(),
49525
- slackPublicUrl: string$2().optional(),
49526
- slackBotName: string$2().default("Whipped"),
49527
- slackInstallerUserId: string$2().optional(),
49528
- autoStartTunnel: boolean$1().default(false),
49529
- tunnelId: string$2().optional(),
49530
- tunnelDomain: string$2().optional(),
49531
- tunnelName: string$2().default("whipped"),
49532
- // Auth: single shared password (scrypt hash) + HMAC secret for signed session
49533
- // cookies + machine token for local agent machinery (MCP/hooks). Never expose
49534
- // these over the API — see configController's response.
49535
- authPasswordHash: string$2().optional(),
49536
- authSessionSecret: string$2().optional(),
49537
- authMachineToken: string$2().optional()
49538
- });
49539
- const runtimeGithubConfigSchema = object({
49540
- token: string$2()
49541
- });
49542
- const runtimeWorktreeSetupSchema = object({
49543
- filesToCopy: array(string$2()).default([]),
49544
- installCommand: string$2().default("")
49545
- });
49546
- const runtimeProjectSecretSchema = object({
49547
- key: string$2().min(1),
49548
- value: string$2()
49549
- });
49550
- const BUILTIN_SECRET_KEYS = ["GITHUB_TOKEN"];
49551
- const runtimeProjectConfigSchema = object({
49552
- name: string$2().optional(),
49553
- defaultAgent: runtimeAgentIdSchema.optional(),
49554
- maxParallelTasks: number$2().int().positive().optional(),
49555
- maxAutoFixAttempts: number$2().int().nonnegative().optional(),
49556
- pollingIntervalSeconds: number$2().int().positive().optional(),
49557
- // What happens when a card passes review (polling/dispatch is always on; per-ticket
49558
- // readyForDev gates pickup). "off" parks it in ready_for_review, "pr" auto-creates a
49559
- // GitHub PR, "yolo" merges the branch straight into the local baseRef and pushes.
49560
- deliveryMode: _enum(["off", "pr", "yolo"]).default("off"),
49561
- autoCommit: boolean$1().default(true),
49562
- defaultBaseBranch: string$2().optional(),
49563
- github: runtimeGithubConfigSchema.optional(),
49564
- worktreeSetup: runtimeWorktreeSetupSchema.optional(),
49565
- startCommand: string$2().default(""),
49566
- workflows: array(workflowSchema).default([]),
49567
- secrets: array(runtimeProjectSecretSchema).default([]),
49568
- systemPrompt: string$2().optional(),
49569
- // Freeform instructions injected into the dev agent's prompt to shape PR
49570
- // titles, descriptions, and commit messages. Empty/absent → daemon falls
49571
- // back to DEFAULT_GIT_INSTRUCTIONS.
49572
- gitInstructions: string$2().optional(),
49573
- // Which agent binary/model/effort the assistant agent runs as. Absent → claude.
49574
- assistantModel: agentModelChoiceSchema.optional()
49575
- });
49576
- object({
49577
- workspaceId: string$2(),
49578
- repoPath: string$2(),
49579
- board: runtimeBoardDataSchema,
49580
- revision: number$2(),
49581
- projectConfig: runtimeProjectConfigSchema
49582
- });
49583
- object({
49584
- board: runtimeBoardDataSchema,
49585
- revision: number$2()
49586
- });
49587
- const runtimeVisualElementSchema = object({
49588
- elementSelector: string$2().optional(),
49589
- elementText: string$2().optional(),
49590
- componentName: string$2().optional(),
49591
- componentChain: array(string$2()).optional(),
49592
- sourceFile: string$2().optional(),
49593
- sourceLine: number$2().optional(),
49594
- // The page the element was captured on. Selections can span pages, so this is
49595
- // per-element rather than relying on the visualComment-level pageUrl.
49596
- pageUrl: string$2().optional()
49597
- });
49598
- const runtimeVisualCommentSchema = object({
49599
- pageUrl: string$2().optional(),
49600
- elements: array(runtimeVisualElementSchema).default([])
49601
- });
49602
- const runtimeCardCreateRequestSchema = object({
49603
- description: string$2(),
49604
- type: cardTypeSchema.optional(),
49605
- // Browser-extension element references; folded into the description server-side.
49606
- visualComment: runtimeVisualCommentSchema.optional(),
49607
- agentId: runtimeAgentIdSchema.optional(),
49608
- priority: runtimeCardPrioritySchema.optional(),
49609
- readyForDev: boolean$1().optional(),
49610
- dependsOn: string$2().optional(),
49611
- waitsFor: array(string$2()).optional(),
49612
- subtaskIds: array(string$2()).optional(),
49613
- columnId: runtimeBoardColumnIdSchema.optional(),
49614
- baseRef: string$2().optional(),
49615
- githubIssueUrl: string$2().optional(),
49616
- workflowId: string$2().optional(),
49617
- descriptionAttachments: array(reviewAttachmentSchema).optional(),
49618
- branchName: string$2().optional(),
49619
- // Optional per-ticket overrides edited before creation. When omitted, the card
49620
- // snapshots the resolved workflow's pairs and defaults the active level to the
49621
- // workflow's highest configured tier.
49622
- modelConfig: cardModelConfigSchema.optional(),
49623
- activeLevel: tierLevelSchema.optional()
49624
- });
49625
- object({
49626
- cardId: string$2(),
49627
- targetColumnId: runtimeBoardColumnIdSchema,
49628
- targetIndex: number$2().int().nonnegative().optional(),
49629
- revision: number$2()
49630
- });
49631
- object({
49632
- cardId: string$2(),
49633
- description: string$2().optional(),
49634
- descriptionAttachments: array(reviewAttachmentSchema).optional(),
49635
- type: cardTypeSchema.optional(),
49636
- agentId: runtimeAgentIdSchema.optional(),
49637
- priority: runtimeCardPrioritySchema.optional(),
49638
- readyForDev: boolean$1().optional(),
49639
- dependsOn: string$2().optional(),
49640
- waitsFor: array(string$2()).optional(),
49641
- subtaskIds: array(string$2()).optional(),
49642
- workflowId: string$2().optional(),
49643
- branchName: string$2().optional(),
49644
- plan: string$2().optional(),
49645
- activeLevel: tierLevelSchema.optional(),
49646
- modelConfig: cardModelConfigSchema.optional(),
49647
- revision: number$2()
49648
- });
49649
- const memoryScopeSchema = _enum(["global", "project"]);
49650
- const memoryTypeSchema = _enum([
49651
- "fact",
49652
- "convention",
49653
- "decision",
49654
- "preference",
49655
- "rule",
49656
- "lesson",
49657
- "sharp_edge"
49658
- ]);
49659
- const MEMORY_TYPE_OPTIONS = [
49660
- { value: "fact", label: "Fact" },
49661
- { value: "convention", label: "Convention" },
49662
- { value: "decision", label: "Decision" },
49663
- { value: "preference", label: "Preference" },
49664
- { value: "rule", label: "Rule" },
49665
- { value: "lesson", label: "Lesson" },
49666
- { value: "sharp_edge", label: "Sharp edge" }
49667
- ];
49668
- const memorySourceTypeSchema = _enum(["user_correction", "explicit_save", "task_lesson", "manual_human"]);
49669
- const memoryStatusSchema = _enum(["pending", "approved"]);
49670
- const runtimeMemoryOriginAgentSchema = object({
49671
- agent: string$2(),
49672
- model: string$2().optional()
49673
- });
49674
- object({
49675
- id: string$2(),
49676
- scope: memoryScopeSchema,
49677
- workspaceId: string$2().nullable(),
49678
- originWorkspaceId: string$2().nullable().optional(),
49679
- type: memoryTypeSchema,
49680
- title: string$2(),
49681
- content: string$2(),
49682
- sourceType: memorySourceTypeSchema,
49683
- importance: number$2().int().min(1).max(3).default(1),
49684
- tags: array(string$2()).default([]),
49685
- boundWorkspaceIds: array(string$2()).default([]),
49686
- originCardId: string$2().nullable().optional(),
49687
- originAgent: runtimeMemoryOriginAgentSchema.nullable().optional(),
49688
- status: memoryStatusSchema.default("approved"),
49689
- createdAt: number$2(),
49690
- updatedAt: number$2()
49691
- });
49692
- function normalizeTag(raw2) {
49693
- return raw2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
49606
+ function ProjectItem({
49607
+ project,
49608
+ workspaceId,
49609
+ dp,
49610
+ snap,
49611
+ isActive: isActive2,
49612
+ indent: indent2,
49613
+ onSwitch,
49614
+ onRemove
49615
+ }) {
49616
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
49617
+ "div",
49618
+ {
49619
+ ref: dp.innerRef,
49620
+ ...dp.draggableProps,
49621
+ ...dp.dragHandleProps,
49622
+ onClick: () => onSwitch(workspaceId),
49623
+ className: classNames(
49624
+ "group flex items-center gap-2 h-8 pr-2 my-px mx-1 rounded-md cursor-pointer select-none transition-colors",
49625
+ indent2 ? "pl-10" : "pl-3",
49626
+ snap.isDragging ? "opacity-70" : isActive2 ? "" : "hover:bg-[#1a1a1f]"
49627
+ ),
49628
+ style: {
49629
+ ...dp.draggableProps.style,
49630
+ background: isActive2 && !snap.isDragging ? "#7c6aff18" : "transparent",
49631
+ borderLeft: isActive2 && !snap.isDragging ? "2px solid #7c6aff" : "2px solid transparent"
49632
+ },
49633
+ children: [
49634
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
49635
+ "div",
49636
+ {
49637
+ className: classNames("w-1.5 h-1.5 rounded-full shrink-0", isActive2 ? "bg-[#7c6aff]" : "bg-[#2a2a35]"),
49638
+ style: isActive2 ? { boxShadow: "0 0 6px #7c6aff80" } : void 0
49639
+ }
49640
+ ),
49641
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
49642
+ "span",
49643
+ {
49644
+ className: classNames(
49645
+ "flex-1 min-w-0 truncate text-[12px]",
49646
+ isActive2 ? "text-[#f0f0f5] font-medium" : "text-[#8888a0] font-normal"
49647
+ ),
49648
+ children: project.name
49649
+ }
49650
+ ),
49651
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "shrink-0 flex items-center opacity-0 group-hover:opacity-100 transition-opacity", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49652
+ "button",
49653
+ {
49654
+ onClick: (e) => {
49655
+ e.stopPropagation();
49656
+ ConfirmDialog_default.show({
49657
+ title: "Remove project",
49658
+ content: `Remove "${project.name}" from Whipped? This will stop all running agents and delete all associated worktrees and data.`,
49659
+ confirmButtonLabel: "Remove",
49660
+ cancelButtonLabel: "Cancel",
49661
+ onConfirm: async ({ dismiss: dismiss2 }) => {
49662
+ await onRemove(workspaceId);
49663
+ dismiss2();
49664
+ }
49665
+ });
49666
+ },
49667
+ className: "flex items-center justify-center w-5 h-5 rounded hover:bg-[#ef444420] transition-colors text-[#60607a] hover:text-[#ef4444]",
49668
+ title: "Remove project",
49669
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 10 })
49670
+ }
49671
+ ) })
49672
+ ]
49673
+ }
49674
+ );
49694
49675
  }
49695
- _enum(["interval", "calendar"]);
49696
- const recurringScheduleSchema = discriminatedUnion("kind", [
49697
- object({ kind: literal("interval"), intervalSeconds: number$2().int().positive() }),
49698
- object({ kind: literal("calendar"), cronExpr: string$2().min(1), timezone: string$2().min(1) })
49699
- ]);
49700
- const recurringRunStatusSchema = _enum(["running", "ok", "error", "killed"]);
49701
- const recurringRunTriggerSchema = _enum(["schedule", "manual"]);
49702
- const recurringAgentRunSchema = object({
49703
- id: string$2(),
49704
- startedAt: number$2(),
49705
- endedAt: number$2().optional(),
49706
- status: recurringRunStatusSchema,
49707
- summary: string$2().optional(),
49708
- tokens: number$2().optional(),
49709
- trigger: recurringRunTriggerSchema.default("schedule"),
49710
- streamId: string$2().optional()
49711
- });
49712
- object({
49713
- id: string$2(),
49714
- name: string$2(),
49715
- instructions: string$2().default(""),
49716
- schedule: recurringScheduleSchema,
49717
- model: agentModelChoiceSchema,
49718
- enabled: boolean$1().default(true),
49719
- lastRunAt: number$2().optional(),
49720
- nextRunAt: number$2().optional(),
49721
- journal: string$2().default(""),
49722
- createdAt: number$2(),
49723
- updatedAt: number$2(),
49724
- recentRuns: array(recurringAgentRunSchema).default([])
49725
- });
49726
- object({
49727
- name: string$2().min(1),
49728
- instructions: string$2().optional(),
49729
- schedule: recurringScheduleSchema,
49730
- model: agentModelChoiceSchema.optional(),
49731
- enabled: boolean$1().optional()
49732
- });
49733
- object({
49734
- id: string$2(),
49735
- name: string$2().min(1).optional(),
49736
- instructions: string$2().optional(),
49737
- schedule: recurringScheduleSchema.optional(),
49738
- model: agentModelChoiceSchema.optional(),
49739
- enabled: boolean$1().optional(),
49740
- journal: string$2().optional()
49741
- });
49742
- const projectFolderSchema = object({
49743
- id: string$2(),
49744
- name: string$2(),
49745
- collapsed: boolean$1().default(false),
49746
- projectIds: array(string$2())
49747
- });
49748
- const topLevelItemSchema = discriminatedUnion("type", [
49749
- object({ type: literal("folder"), id: string$2() }),
49750
- object({ type: literal("project"), workspaceId: string$2() })
49751
- ]);
49752
- object({
49753
- version: literal(1),
49754
- topLevel: array(topLevelItemSchema),
49755
- folders: record(string$2(), projectFolderSchema)
49756
- });
49757
- object({
49758
- workspaceId: string$2(),
49759
- repoPath: string$2(),
49760
- name: string$2(),
49761
- lastUpdated: number$2()
49676
+ const ProjectsSidebar = React.forwardRef(function ProjectsSidebar2({ projects, activeWorkspaceId, onSwitch, onRemove }, ref) {
49677
+ const [layout, setLayout] = reactExports.useState({ version: 1, topLevel: [], folders: {} });
49678
+ const [isDragging2, setIsDragging] = reactExports.useState(false);
49679
+ const [hoveredFolderId, setHoveredFolderId] = reactExports.useState(null);
49680
+ const [editingId, setEditingId] = reactExports.useState(null);
49681
+ const [editName, setEditName] = reactExports.useState("");
49682
+ const editRef = reactExports.useRef(null);
49683
+ const saveTimer = reactExports.useRef(null);
49684
+ const { trigger: fetchLayout } = useRead((api) => api("projects/layout").GET(), { enabled: false });
49685
+ const { trigger: saveLayout } = useWrite((api) => api("projects/layout").PUT());
49686
+ reactExports.useEffect(() => {
49687
+ fetchLayout().then((res) => setLayout((prev) => syncLayout(res.data ?? prev, projects))).catch(() => {
49688
+ });
49689
+ }, []);
49690
+ reactExports.useEffect(() => {
49691
+ setLayout((prev) => syncLayout(prev, projects));
49692
+ }, [projects]);
49693
+ const persist = (next2) => {
49694
+ if (saveTimer.current) clearTimeout(saveTimer.current);
49695
+ saveTimer.current = setTimeout(() => {
49696
+ saveLayout({ body: next2 }).catch(() => {
49697
+ });
49698
+ }, 300);
49699
+ };
49700
+ const update2 = (next2) => {
49701
+ setLayout(next2);
49702
+ persist(next2);
49703
+ };
49704
+ const addFolder = () => {
49705
+ const id = genId();
49706
+ const next2 = {
49707
+ ...layout,
49708
+ topLevel: [...layout.topLevel, { type: "folder", id }],
49709
+ folders: { ...layout.folders, [id]: { id, name: "New Folder", collapsed: false, projectIds: [] } }
49710
+ };
49711
+ update2(next2);
49712
+ setEditingId(id);
49713
+ setEditName("New Folder");
49714
+ setTimeout(() => {
49715
+ var _a3, _b2;
49716
+ (_a3 = editRef.current) == null ? void 0 : _a3.focus();
49717
+ (_b2 = editRef.current) == null ? void 0 : _b2.select();
49718
+ }, 50);
49719
+ };
49720
+ reactExports.useImperativeHandle(ref, () => ({ addFolder }));
49721
+ const startRename = (id) => {
49722
+ var _a3;
49723
+ setEditingId(id);
49724
+ setEditName(((_a3 = layout.folders[id]) == null ? void 0 : _a3.name) ?? "");
49725
+ setTimeout(() => {
49726
+ var _a4, _b2;
49727
+ (_a4 = editRef.current) == null ? void 0 : _a4.focus();
49728
+ (_b2 = editRef.current) == null ? void 0 : _b2.select();
49729
+ }, 50);
49730
+ };
49731
+ const commitRename = () => {
49732
+ if (!editingId) return;
49733
+ update2({
49734
+ ...layout,
49735
+ folders: {
49736
+ ...layout.folders,
49737
+ [editingId]: { ...layout.folders[editingId], name: editName.trim() || "Untitled" }
49738
+ }
49739
+ });
49740
+ setEditingId(null);
49741
+ };
49742
+ const deleteFolder = (id) => {
49743
+ const folder = layout.folders[id];
49744
+ if (!folder) return;
49745
+ const idx = layout.topLevel.findIndex((i2) => i2.type === "folder" && i2.id === id);
49746
+ const returned = folder.projectIds.map((ws2) => ({ type: "project", workspaceId: ws2 }));
49747
+ const topLevel = [...layout.topLevel];
49748
+ topLevel.splice(idx, 1, ...returned);
49749
+ const folders = { ...layout.folders };
49750
+ delete folders[id];
49751
+ update2({ ...layout, topLevel, folders });
49752
+ };
49753
+ const toggleCollapse = (id) => {
49754
+ const f = layout.folders[id];
49755
+ if (!f) return;
49756
+ update2({ ...layout, folders: { ...layout.folders, [id]: { ...f, collapsed: !f.collapsed } } });
49757
+ };
49758
+ const onDragUpdate2 = reactExports.useCallback(
49759
+ (update22) => {
49760
+ if (!update22.destination) {
49761
+ setHoveredFolderId(null);
49762
+ return;
49763
+ }
49764
+ const flat2 = buildFlat(layout, true, true);
49765
+ flat2.splice(update22.source.index, 1);
49766
+ const folderId = folderAtDest(flat2, update22.destination.index);
49767
+ setHoveredFolderId(folderId);
49768
+ },
49769
+ [layout]
49770
+ );
49771
+ const onDragEnd2 = (result) => {
49772
+ setIsDragging(false);
49773
+ setHoveredFolderId(null);
49774
+ if (!result.destination) return;
49775
+ const { draggableId, source, destination } = result;
49776
+ if (source.index === destination.index) return;
49777
+ const flat2 = buildFlat(layout, true, true);
49778
+ if (draggableId.startsWith("fh:")) {
49779
+ const folderId = draggableId.slice(3);
49780
+ flat2.splice(source.index, 1);
49781
+ const group = [];
49782
+ while (flat2[source.index] && (flat2[source.index].kind === "project" && flat2[source.index].folderId === folderId || flat2[source.index].kind === "empty-folder-slot" && flat2[source.index].folderId === folderId)) {
49783
+ group.push(flat2.splice(source.index, 1)[0]);
49784
+ }
49785
+ const at2 = Math.min(destination.index, flat2.length);
49786
+ flat2.splice(at2, 0, { kind: "folder-header", folderId }, ...group);
49787
+ } else {
49788
+ const [moved] = flat2.splice(source.index, 1);
49789
+ const newFolderId = folderAtDest(flat2, destination.index);
49790
+ flat2.splice(destination.index, 0, { ...moved, folderId: newFolderId });
49791
+ }
49792
+ update2(flatToLayout(flat2, layout));
49793
+ };
49794
+ const projectMap = Object.fromEntries(projects.map((p) => [p.workspaceId, p]));
49795
+ const flat = buildFlat(layout, isDragging2, isDragging2);
49796
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(DragDropContext, { onDragStart: () => setIsDragging(true), onDragUpdate: onDragUpdate2, onDragEnd: onDragEnd2, children: /* @__PURE__ */ jsxRuntimeExports.jsx(ConnectedDroppable, { droppableId: "sidebar", children: (provided) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: provided.innerRef, ...provided.droppableProps, className: "flex flex-col", children: [
49797
+ flat.map((item, index2) => {
49798
+ if (item.kind === "folder-header") {
49799
+ const folder = layout.folders[item.folderId];
49800
+ if (!folder) return null;
49801
+ const expanded = !folder.collapsed || isDragging2;
49802
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(PublicDraggable, { draggableId: `fh:${item.folderId}`, index: index2, children: (dp, snap) => /* @__PURE__ */ jsxRuntimeExports.jsx(
49803
+ FolderHeader,
49804
+ {
49805
+ folderId: item.folderId,
49806
+ folder,
49807
+ dp,
49808
+ snap,
49809
+ expanded,
49810
+ hovered: hoveredFolderId === item.folderId,
49811
+ editing: editingId === item.folderId,
49812
+ editName,
49813
+ editRef,
49814
+ onToggleCollapse: toggleCollapse,
49815
+ onStartRename: startRename,
49816
+ onDeleteFolder: deleteFolder,
49817
+ onEditNameChange: setEditName,
49818
+ onCommitRename: commitRename,
49819
+ onCancelRename: () => setEditingId(null)
49820
+ }
49821
+ ) }, `fh:${item.folderId}`);
49822
+ }
49823
+ if (item.kind === "empty-folder-slot") {
49824
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
49825
+ PublicDraggable,
49826
+ {
49827
+ draggableId: `slot:${item.folderId}`,
49828
+ index: index2,
49829
+ isDragDisabled: true,
49830
+ children: (dp) => /* @__PURE__ */ jsxRuntimeExports.jsx(EmptyFolderSlot, { dp })
49831
+ },
49832
+ `slot:${item.folderId}`
49833
+ );
49834
+ }
49835
+ const project = projectMap[item.workspaceId];
49836
+ if (!project) return null;
49837
+ const isActive2 = item.workspaceId === activeWorkspaceId;
49838
+ const indent2 = item.folderId !== null;
49839
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(PublicDraggable, { draggableId: `p:${item.workspaceId}`, index: index2, children: (dp, snap) => /* @__PURE__ */ jsxRuntimeExports.jsx(
49840
+ ProjectItem,
49841
+ {
49842
+ project,
49843
+ workspaceId: item.workspaceId,
49844
+ dp,
49845
+ snap,
49846
+ isActive: isActive2,
49847
+ indent: indent2,
49848
+ onSwitch,
49849
+ onRemove
49850
+ }
49851
+ ) }, `p:${item.workspaceId}`);
49852
+ }),
49853
+ provided.placeholder
49854
+ ] }) }) });
49762
49855
  });
49856
+ function CardDetailHeader({
49857
+ card,
49858
+ workspaceId,
49859
+ projectName,
49860
+ externalUrl,
49861
+ isStory,
49862
+ isReadyForReview,
49863
+ hasStartCommand = false,
49864
+ merging,
49865
+ onMerge,
49866
+ onPR,
49867
+ onDelete,
49868
+ onClose
49869
+ }) {
49870
+ var _a3, _b2, _c3;
49871
+ const { session: runSession, start: startRun, stop: stopRun } = useRunSession(workspaceId);
49872
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 px-6 py-2.5 border-b border-[#2a2a35] bg-[#141418] shrink-0", children: [
49873
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "text-[#60607a] hover:text-gray-300 transition-colors", title: "Back to board", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 18 }) }),
49874
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-[18px] bg-[#2a2a35] shrink-0" }),
49875
+ projectName && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
49876
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-[#60607a]", children: projectName }),
49877
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-[#2a2a35]", children: "/" })
49878
+ ] }),
49879
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[13px] font-semibold text-[#f0f0f5] truncate", children: ((_a3 = card.description) == null ? void 0 : _a3.split("\n")[0]) ?? card.id }),
49880
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1" }),
49881
+ externalUrl && !((_b2 = card.pr) == null ? void 0 : _b2.url) && /* @__PURE__ */ jsxRuntimeExports.jsx(
49882
+ "a",
49883
+ {
49884
+ href: externalUrl,
49885
+ target: "_blank",
49886
+ rel: "noreferrer",
49887
+ className: "text-[#60607a] hover:text-gray-300 transition-colors",
49888
+ title: "Open external link",
49889
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { size: 15 })
49890
+ }
49891
+ ),
49892
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-[18px] bg-[#2a2a35] shrink-0" }),
49893
+ hasStartCommand && /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: runSession.status === "running" && runSession.cardId === card.id ? /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip_default, { delayDuration: 0, content: "Stop", side: "bottom", triggerAsChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49894
+ "button",
49895
+ {
49896
+ onClick: () => void stopRun(),
49897
+ className: "cursor-pointer text-[#60607a] hover:text-red-400 transition-colors",
49898
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Square, { size: 15, className: "fill-current" })
49899
+ }
49900
+ ) }) : /* @__PURE__ */ jsxRuntimeExports.jsx(
49901
+ Tooltip_default,
49902
+ {
49903
+ delayDuration: 0,
49904
+ content: runSession.status === "running" ? "Another task is running" : "Run",
49905
+ side: "bottom",
49906
+ triggerAsChild: true,
49907
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49908
+ "button",
49909
+ {
49910
+ onClick: () => void startRun(card.id),
49911
+ disabled: runSession.status === "running",
49912
+ className: "cursor-pointer text-[#60607a] hover:text-emerald-400 transition-colors disabled:opacity-40 disabled:cursor-not-allowed",
49913
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 15 })
49914
+ }
49915
+ )
49916
+ }
49917
+ ) }),
49918
+ !isStory && isReadyForReview && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
49919
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
49920
+ Tooltip_default,
49921
+ {
49922
+ delayDuration: 0,
49923
+ content: merging ? "Merging..." : `Merge into ${card.baseRef}`,
49924
+ side: "bottom",
49925
+ triggerAsChild: true,
49926
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49927
+ "button",
49928
+ {
49929
+ onClick: onMerge,
49930
+ disabled: merging,
49931
+ className: "cursor-pointer text-[#60607a] hover:text-emerald-400 transition-colors disabled:opacity-40",
49932
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(GitMerge, { size: 15 })
49933
+ }
49934
+ )
49935
+ }
49936
+ ),
49937
+ ((_c3 = card.pr) == null ? void 0 : _c3.url) ? /* @__PURE__ */ jsxRuntimeExports.jsx(
49938
+ "a",
49939
+ {
49940
+ href: card.pr.url,
49941
+ target: "_blank",
49942
+ rel: "noreferrer",
49943
+ title: "Open Pull Request",
49944
+ className: "cursor-pointer text-green-400 hover:text-green-300 transition-colors",
49945
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(GitPullRequest, { size: 15 })
49946
+ }
49947
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip_default, { delayDuration: 0, content: `Create PR against ${card.baseRef}`, side: "bottom", triggerAsChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49948
+ "button",
49949
+ {
49950
+ onClick: onPR,
49951
+ disabled: merging,
49952
+ className: "cursor-pointer text-[#60607a] hover:text-green-400 transition-colors disabled:opacity-40",
49953
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(GitPullRequest, { size: 15 })
49954
+ }
49955
+ ) })
49956
+ ] }),
49957
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-[18px] bg-[#2a2a35] shrink-0" }),
49958
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip_default, { delayDuration: 0, content: "Delete task", side: "bottom", triggerAsChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49959
+ "button",
49960
+ {
49961
+ onClick: onDelete,
49962
+ className: "cursor-pointer text-[#60607a] hover:text-red-400 transition-colors",
49963
+ title: "Delete task",
49964
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 15 })
49965
+ }
49966
+ ) })
49967
+ ] });
49968
+ }
49763
49969
  const COLUMN_LABELS = {
49764
49970
  todo: "Todo",
49765
49971
  in_progress: "In Progress",
@@ -73705,19 +73911,6 @@ function CommitMsgDialog({
73705
73911
  ] })
73706
73912
  ] });
73707
73913
  }
73708
- function BranchSelect({ branches, value, onChange, placeholder = "Select branch" }) {
73709
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
73710
- Select_default,
73711
- {
73712
- value,
73713
- onChange: (v2) => onChange(v2),
73714
- placeholder,
73715
- filterable: true,
73716
- prefix: /* @__PURE__ */ jsxRuntimeExports.jsx(GitBranch, { size: 13, className: "text-[#8888a0]" }),
73717
- children: branches.map((b2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: b2, label: b2 }, b2))
73718
- }
73719
- );
73720
- }
73721
73914
  function CreatePRDialog({
73722
73915
  dismiss: dismiss2,
73723
73916
  workspaceId,
@@ -74358,90 +74551,6 @@ function cardToFormModelConfig(cfg) {
74358
74551
  }
74359
74552
  return out;
74360
74553
  }
74361
- function ModelSelect({
74362
- agentId,
74363
- value,
74364
- onChange,
74365
- floatingStrategy,
74366
- menuClassName
74367
- }) {
74368
- const staticOptions = MODEL_OPTIONS[agentId];
74369
- const isDynamic = agentId === "opencode" || agentId === "cursor";
74370
- const modelsRead = useRead(
74371
- (api) => api("agents/models").GET({
74372
- query: { agent: agentId === "cursor" ? "cursor" : "opencode" }
74373
- }),
74374
- { enabled: isDynamic }
74375
- );
74376
- const dynamicModels = modelsRead.data ?? [];
74377
- const isFetching = modelsRead.fetching;
74378
- const options = isDynamic ? dynamicModels : staticOptions;
74379
- const [customChosen, setCustomChosen] = reactExports.useState(false);
74380
- const isPresetValue = value === "" || options.some((o2) => o2.value === value);
74381
- const customMode = customChosen || !isPresetValue;
74382
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
74383
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
74384
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
74385
- Select_default,
74386
- {
74387
- floatingStrategy,
74388
- menuClassName,
74389
- value: customMode ? "__custom__" : value,
74390
- onChange: (v2) => {
74391
- if (v2 === "__custom__") {
74392
- setCustomChosen(true);
74393
- } else {
74394
- setCustomChosen(false);
74395
- onChange(v2);
74396
- }
74397
- },
74398
- filterable: true,
74399
- children: [
74400
- /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: "", label: "Default" }),
74401
- options.map((o2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: o2.value, label: o2.label }, o2.value)),
74402
- /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: "__custom__", label: "Custom..." })
74403
- ]
74404
- }
74405
- ) }),
74406
- isDynamic && /* @__PURE__ */ jsxRuntimeExports.jsx(
74407
- "button",
74408
- {
74409
- type: "button",
74410
- onClick: () => void modelsRead.trigger(),
74411
- disabled: isFetching,
74412
- title: "Refresh model list",
74413
- className: "flex items-center justify-center px-2 rounded border border-[var(--color-border-secondary)] bg-[#1a1a1f] hover:bg-[var(--color-surface-hover)] disabled:opacity-50 transition-colors",
74414
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(
74415
- "svg",
74416
- {
74417
- className: classNames("w-4 h-4", isFetching ? "animate-spin" : ""),
74418
- fill: "none",
74419
- viewBox: "0 0 24 24",
74420
- stroke: "currentColor",
74421
- strokeWidth: 2,
74422
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(
74423
- "path",
74424
- {
74425
- strokeLinecap: "round",
74426
- strokeLinejoin: "round",
74427
- d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
74428
- }
74429
- )
74430
- }
74431
- )
74432
- }
74433
- )
74434
- ] }),
74435
- customMode && /* @__PURE__ */ jsxRuntimeExports.jsx(
74436
- Input_default,
74437
- {
74438
- value,
74439
- onChange: (e) => onChange(e.target.value),
74440
- placeholder: agentId === "opencode" ? "e.g. anthropic/claude-opus-4-7" : agentId === "cursor" ? "e.g. claude-opus-4-7-thinking-max" : agentId === "claude" ? "e.g. claude-opus-4-7" : "e.g. gpt-5-codex"
74441
- }
74442
- )
74443
- ] });
74444
- }
74445
74554
  function ModelTiersDialog({
74446
74555
  pairs,
74447
74556
  defaultBinary,
@@ -74564,13 +74673,12 @@ function TierRow({
74564
74673
  }
74565
74674
  ),
74566
74675
  /* @__PURE__ */ jsxRuntimeExports.jsx(
74567
- Select_default,
74676
+ AgentBinarySelect,
74568
74677
  {
74569
74678
  floatingStrategy: "fixed",
74570
74679
  value: pair.binary,
74571
74680
  onChange: (v2) => onPatch({ binary: v2, model: null }),
74572
- menuClassName: "w-fit",
74573
- children: AGENT_BINARY_OPTIONS.map((o2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: o2.value, label: o2.label }, o2.value))
74681
+ menuClassName: "w-fit"
74574
74682
  }
74575
74683
  ),
74576
74684
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -79344,7 +79452,7 @@ function TagInput({
79344
79452
  }) {
79345
79453
  const [draft, setDraft] = reactExports.useState("");
79346
79454
  const add2 = (raw2) => {
79347
- const tag = normalizeTag(raw2);
79455
+ const tag = normalizeTag$1(raw2);
79348
79456
  if (!tag || value.includes(tag)) return;
79349
79457
  onChange([...value, tag]);
79350
79458
  setDraft("");
@@ -79764,49 +79872,6 @@ function MemorySection({ workspaceId }) {
79764
79872
  ] })
79765
79873
  ] });
79766
79874
  }
79767
- function AgentModelPicker({
79768
- value,
79769
- onChange,
79770
- floatingStrategy,
79771
- menuClassName
79772
- }) {
79773
- const agentId = value.agentId ?? "claude";
79774
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-start", children: [
79775
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full sm:w-32 shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
79776
- Select_default,
79777
- {
79778
- value: agentId,
79779
- floatingStrategy,
79780
- menuClassName,
79781
- onChange: (v2) => onChange({ ...value, agentId: v2, model: null }),
79782
- children: AGENT_BINARY_OPTIONS.map((o2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: o2.value, label: o2.label }, o2.value))
79783
- }
79784
- ) }),
79785
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
79786
- ModelSelect,
79787
- {
79788
- agentId,
79789
- value: value.model ?? "",
79790
- onChange: (v2) => onChange({ ...value, model: v2 || null }),
79791
- floatingStrategy,
79792
- menuClassName
79793
- }
79794
- ) }),
79795
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full sm:w-36 shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
79796
- Select_default,
79797
- {
79798
- value: value.effort ?? "",
79799
- floatingStrategy,
79800
- menuClassName,
79801
- onChange: (v2) => onChange({ ...value, effort: v2 || null }),
79802
- children: [
79803
- /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: "", label: "Default effort" }),
79804
- EFFORT_OPTIONS.map((o2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: o2.value, label: o2.label }, o2.value))
79805
- ]
79806
- }
79807
- ) })
79808
- ] });
79809
- }
79810
79875
  function SaveRow({ saving, onSave }) {
79811
79876
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end pt-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Button_default, { onClick: onSave, disabled: saving, children: saving ? "Saving..." : "Save" }) });
79812
79877
  }