whipped 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27169,389 +27169,954 @@ 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-fable-5", label: "Fable 5" },
27190
+ { value: "claude-opus-4-8", label: "Opus 4.8" },
27191
+ { value: "claude-opus-4-7", label: "Opus 4.7" },
27192
+ { value: "claude-opus-4-6", label: "Opus 4.6" },
27193
+ { value: "claude-sonnet-4-6", label: "Sonnet 4.6" },
27194
+ { value: "claude-sonnet-4-5", label: "Sonnet 4.5" },
27195
+ { value: "claude-haiku-4-5", label: "Haiku 4.5" }
27196
+ ],
27197
+ codex: [
27198
+ { value: "gpt-5.5", label: "GPT-5.5 (default)" },
27199
+ { value: "gpt-5.4", label: "GPT-5.4" },
27200
+ { value: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
27201
+ { value: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
27202
+ { value: "gpt-5.2", label: "GPT-5.2" }
27203
+ ],
27204
+ // opencode supports any provider/model string — no fixed presets.
27205
+ // The UI renders a free-form text input for opencode model selection.
27206
+ opencode: [],
27207
+ // cursor supports many models — no fixed presets.
27208
+ // The UI fetches the live list via agents.cursorModels and renders a Select.
27209
+ cursor: []
27210
+ };
27211
+ const agentModelChoiceSchema = object({
27212
+ agentId: runtimeAgentIdSchema.default("claude"),
27213
+ model: string$2().nullable().optional(),
27214
+ effort: effortLevelSchema.nullable().optional()
27176
27215
  });
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;
27216
+ const DEFAULT_AGENT_MODEL_CHOICE = { agentId: "claude", model: null, effort: null };
27217
+ const workflowSlotTypeSchema = _enum(["dev", "review", "plan", "orch"]);
27218
+ const tierLevelSchema = _enum(["minimal", "low", "medium", "high", "max"]);
27219
+ const LEVEL_ORDER = ["minimal", "low", "medium", "high", "max"];
27220
+ const TIER_LEVEL_OPTIONS = [
27221
+ { value: "minimal", label: "Minimal" },
27222
+ { value: "low", label: "Low" },
27223
+ { value: "medium", label: "Medium" },
27224
+ { value: "high", label: "High" },
27225
+ { value: "max", label: "Max" }
27226
+ ];
27227
+ const modelPairSchema = object({
27228
+ id: string$2(),
27229
+ level: tierLevelSchema,
27230
+ isFree: boolean$1().default(false),
27231
+ binary: runtimeAgentIdSchema,
27232
+ model: string$2().nullable().optional(),
27233
+ effort: effortLevelSchema.nullable().optional()
27234
+ });
27235
+ const pairSelectionModeSchema = _enum(["auto", "preferFree", "freeOnly", "paidOnly"]);
27236
+ const PAIR_SELECTION_MODE_OPTIONS = [
27237
+ { value: "auto", label: "Auto (priority)" },
27238
+ { value: "preferFree", label: "Prefer free" },
27239
+ { value: "freeOnly", label: "Free only" },
27240
+ { value: "paidOnly", label: "Paid only" }
27241
+ ];
27242
+ const SLOT_TOOL_IDS = ["browser"];
27243
+ const slotToolSchema = _enum(SLOT_TOOL_IDS);
27244
+ const slotModelConfigSchema = object({
27245
+ pairs: array(modelPairSchema).min(1),
27246
+ mode: pairSelectionModeSchema.default("auto"),
27247
+ pinnedPairId: string$2().optional()
27248
+ });
27249
+ const cardModelConfigSchema = record(string$2(), slotModelConfigSchema);
27250
+ function pickByMode(candidates, mode) {
27251
+ switch (mode) {
27252
+ case "preferFree":
27253
+ return candidates.find((p) => p.isFree) ?? candidates[0];
27254
+ case "freeOnly":
27255
+ return candidates.find((p) => p.isFree);
27256
+ case "paidOnly":
27257
+ return candidates.find((p) => !p.isFree);
27258
+ default:
27259
+ return candidates[0];
27443
27260
  }
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
27261
  }
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;
27262
+ function resolvePair(cfg, activeLevel) {
27263
+ if (cfg.pinnedPairId) {
27264
+ const pinned = cfg.pairs.find((p) => p.id === cfg.pinnedPairId);
27265
+ if (pinned) return pinned;
27535
27266
  }
27536
- if ((requestOptions == null ? void 0 : requestOptions.params) !== void 0) {
27537
- fields.params = requestOptions.params;
27267
+ const startIdx = LEVEL_ORDER.indexOf(activeLevel);
27268
+ const order2 = [];
27269
+ for (let i2 = startIdx; i2 < LEVEL_ORDER.length; i2++) order2.push(i2);
27270
+ for (let i2 = startIdx - 1; i2 >= 0; i2--) order2.push(i2);
27271
+ for (const i2 of order2) {
27272
+ const candidates = cfg.pairs.filter((p) => p.level === LEVEL_ORDER[i2]);
27273
+ if (candidates.length === 0) continue;
27274
+ const pick3 = pickByMode(candidates, cfg.mode);
27275
+ if (pick3) return pick3;
27538
27276
  }
27539
- if (Object.keys(fields).length === 0) {
27540
- return {};
27277
+ for (const i2 of order2) {
27278
+ const candidates = cfg.pairs.filter((p) => p.level === LEVEL_ORDER[i2]);
27279
+ if (candidates[0]) return candidates[0];
27541
27280
  }
27542
- return { input: fields };
27281
+ const fallback = cfg.pairs[0];
27282
+ if (!fallback) throw new Error("resolvePair: slot has no model pairs");
27283
+ return fallback;
27543
27284
  }
27544
- function resolveTransport(option) {
27545
- if (option === "xhr" && typeof XMLHttpRequest !== "undefined") {
27546
- return xhrTransport;
27285
+ const promptValueSchema = preprocess$1(
27286
+ (v2) => {
27287
+ if (typeof v2 === "string") return { source: "inline", text: v2 };
27288
+ return v2;
27289
+ },
27290
+ discriminatedUnion("source", [
27291
+ object({ source: literal("inline"), text: string$2() }),
27292
+ object({ source: literal("file"), path: string$2() })
27293
+ ])
27294
+ );
27295
+ const EMPTY_INLINE_PROMPT = { source: "inline", text: "" };
27296
+ const workflowSlotSchema = object({
27297
+ id: string$2(),
27298
+ type: workflowSlotTypeSchema,
27299
+ name: string$2(),
27300
+ order: number$2().int().nonnegative(),
27301
+ enabled: boolean$1(),
27302
+ prompt: promptValueSchema.default(EMPTY_INLINE_PROMPT),
27303
+ // Model tiers for this slot, in priority order (top = highest). Copied to the
27304
+ // card at creation; the card's active level + mode select which pair runs.
27305
+ pairs: array(modelPairSchema).min(1),
27306
+ mode: pairSelectionModeSchema.default("auto"),
27307
+ // Tools this slot may use (e.g. "browser"). Workflow-only, not ticket-editable.
27308
+ tools: array(slotToolSchema).default([]),
27309
+ // review slots only: may set the card's active level on reopen.
27310
+ canAdjustLevel: boolean$1().default(false),
27311
+ // plan slots only: re-run even if a plan already exists on the card.
27312
+ rerun: boolean$1().default(false)
27313
+ });
27314
+ const workflowSchema = object({
27315
+ id: string$2(),
27316
+ name: string$2(),
27317
+ isDefault: boolean$1().default(false),
27318
+ forStory: boolean$1().default(false),
27319
+ slots: array(workflowSlotSchema)
27320
+ });
27321
+ function highestWorkflowLevel(workflow) {
27322
+ let bestIdx = -1;
27323
+ for (const slot of (workflow == null ? void 0 : workflow.slots) ?? []) {
27324
+ for (const p of slot.pairs) bestIdx = Math.max(bestIdx, LEVEL_ORDER.indexOf(p.level));
27547
27325
  }
27548
- return fetchTransport;
27326
+ return LEVEL_ORDER[bestIdx] ?? "medium";
27549
27327
  }
27550
- async function executeCoreFetch(config2) {
27551
- const {
27552
- baseUrl,
27553
- path: path2,
27554
- method,
27328
+ const DEFAULT_GIT_INSTRUCTIONS = `# Git conventions
27329
+
27330
+ These rules govern how to write commit messages, PR titles, and PR
27331
+ descriptions.
27332
+
27333
+ ## PR title
27334
+ - Imperative, present tense: "Add board view", "Fix race in poller".
27335
+ Not past tense, not gerund.
27336
+ - ≤70 characters; aim for 50.
27337
+ - Describe what shipped, not the task. "Add board view" beats
27338
+ "Implement board view feature".
27339
+ - No prefixes like \`feat:\` / \`[FEAT]\` / \`fix:\`.
27340
+ - No ticket IDs in the title (put them in the description if needed).
27341
+ - No trailing period.
27342
+
27343
+ ## PR description
27344
+ Keep it focused. Two sections, nothing more unless genuinely useful:
27345
+
27346
+ ## Summary
27347
+ - What changed and why. Use as many bullets as the scope warrants —
27348
+ a one-line fix is one bullet; a refactor touching 20 files may
27349
+ need ten. Don't pad, don't truncate.
27350
+
27351
+ ## Test plan
27352
+ - What you actually ran or clicked, and the outcome.
27353
+ - Type-check and lint passing are not a test plan on their own.
27354
+ - If something couldn't be verified, say so in one line.
27355
+
27356
+ Do NOT include:
27357
+ - Iteration narration ("Round N", "addressed feedback", "after review").
27358
+ - Commit SHAs or branch names — GitHub already shows both.
27359
+ - Paths to internal planning docs, scratch files, or task tracker URLs.
27360
+ - "Verification:" sections that only list a passing type-check or lint.
27361
+ - Self-congratulation ("clean", "all checks pass", "ready to merge").
27362
+ - Restating the task description verbatim.
27363
+
27364
+ ## Commit messages
27365
+ - Short imperative subject line, ≤72 chars. That's usually enough.
27366
+ - Skip the body unless a reviewer reading the diff alone would be
27367
+ confused about *why* the change exists.
27368
+ - Reference an issue only if a concrete one exists to close
27369
+ (\`Closes #123\`). Never invent issue numbers.
27370
+ `;
27371
+ const runtimeBoardColumnIdSchema = _enum([
27372
+ "todo",
27373
+ "in_progress",
27374
+ "reopened",
27375
+ "ready_for_review",
27376
+ "blocked",
27377
+ "done"
27378
+ ]);
27379
+ const reviewActorSchema = object({
27380
+ type: _enum(["ai", "human", "external"]),
27381
+ id: string$2(),
27382
+ source: string$2().optional()
27383
+ });
27384
+ const reviewIssueSchema = object({
27385
+ file: string$2().optional(),
27386
+ line: number$2().optional(),
27387
+ severity: _enum(["blocking", "warning", "info"]),
27388
+ message: string$2()
27389
+ });
27390
+ const reviewAttachmentSchema = object({
27391
+ type: string$2(),
27392
+ // "image" | "file" | any mime category
27393
+ name: string$2(),
27394
+ mimeType: string$2(),
27395
+ path: string$2()
27396
+ // absolute path in ~/.whipped/attachments/
27397
+ });
27398
+ const runtimeReviewCommentSchema = object({
27399
+ id: string$2(),
27400
+ type: string$2(),
27401
+ actor: reviewActorSchema,
27402
+ status: _enum(["pass", "fail", "warning", "skipped"]).optional(),
27403
+ createdAt: number$2(),
27404
+ streamId: string$2().optional(),
27405
+ summary: string$2(),
27406
+ issues: array(reviewIssueSchema).optional(),
27407
+ attachments: array(reviewAttachmentSchema).optional(),
27408
+ metadata: record(string$2(), unknown$1()).optional()
27409
+ });
27410
+ const runtimeActivityEntrySchema = object({
27411
+ timestamp: number$2(),
27412
+ message: string$2()
27413
+ });
27414
+ const runtimeTaskSessionStateSchema = _enum(["running", "stopped", "completed", "failed", "killed"]);
27415
+ const runtimeTerminalSessionEntrySchema = object({
27416
+ streamId: string$2(),
27417
+ type: string$2(),
27418
+ startedAt: number$2(),
27419
+ endedAt: number$2().optional(),
27420
+ agentId: runtimeAgentIdSchema.optional(),
27421
+ state: runtimeTaskSessionStateSchema.optional()
27422
+ });
27423
+ const runtimeCardPrioritySchema = _enum(["urgent", "high", "medium", "low"]);
27424
+ const cardTypeSchema = _enum(["task", "story", "subtask"]);
27425
+ const runtimePrMetaSchema = object({
27426
+ url: string$2().optional(),
27427
+ title: string$2().optional(),
27428
+ description: string$2().optional(),
27429
+ updatedAt: number$2().optional(),
27430
+ updatedBy: string$2().optional()
27431
+ });
27432
+ const runtimeBoardCardSchema = object({
27433
+ id: string$2(),
27434
+ description: string$2(),
27435
+ descriptionAttachments: array(reviewAttachmentSchema).optional().default([]),
27436
+ columnId: runtimeBoardColumnIdSchema,
27437
+ type: cardTypeSchema.default("task"),
27438
+ readyForDev: boolean$1().default(false),
27439
+ agentId: runtimeAgentIdSchema.optional(),
27440
+ priority: runtimeCardPrioritySchema.optional(),
27441
+ // Single-parent stacking: this card continues in the parent's worktree/branch
27442
+ // and starts once the parent reaches ready_for_review. Mutually exclusive with waitsFor.
27443
+ dependsOn: string$2().optional(),
27444
+ // Many-parent gate (tasks only): this card starts only once ALL listed cards are
27445
+ // done (merged), in a fresh worktree branched from baseRef. Mutually exclusive with dependsOn.
27446
+ waitsFor: array(string$2()).default([]),
27447
+ // Story-only: the IDs of this story's subtasks. The story triggers its orchestrator
27448
+ // workflow once every subtask reaches ready_for_review.
27449
+ subtaskIds: array(string$2()).default([]),
27450
+ autoFixAttempts: number$2().int().nonnegative().default(0),
27451
+ baseRef: string$2(),
27452
+ createdAt: number$2(),
27453
+ updatedAt: number$2(),
27454
+ githubIssueUrl: string$2().optional(),
27455
+ pr: runtimePrMetaSchema.optional(),
27456
+ workflowId: string$2().optional(),
27457
+ // Plan written by the one-shot plan agent; injected into the dev agent's prompt.
27458
+ plan: string$2().optional(),
27459
+ // Workflow-wide capability level; every slot resolves it to its own pair.
27460
+ activeLevel: tierLevelSchema.default("medium"),
27461
+ // Per-slot model config, snapshotted from the workflow at creation and editable
27462
+ // per ticket (slotId → {pairs, mode, pinnedPairId}).
27463
+ modelConfig: cardModelConfigSchema.optional(),
27464
+ reviewComments: array(runtimeReviewCommentSchema).default([]),
27465
+ activityLog: array(runtimeActivityEntrySchema).default([]),
27466
+ terminalSessions: array(runtimeTerminalSessionEntrySchema).default([]),
27467
+ githubCommentIds: array(string$2()).default([]),
27468
+ worktreePath: string$2().optional(),
27469
+ branchName: string$2().optional(),
27470
+ slackMessageTs: string$2().optional(),
27471
+ slackChannelId: string$2().optional()
27472
+ });
27473
+ const runtimeBoardColumnSchema = object({
27474
+ id: runtimeBoardColumnIdSchema,
27475
+ title: string$2(),
27476
+ taskIds: array(string$2())
27477
+ });
27478
+ const runtimeBoardDataSchema = object({
27479
+ columns: array(runtimeBoardColumnSchema),
27480
+ cards: record(string$2(), runtimeBoardCardSchema)
27481
+ });
27482
+ const runtimeGlobalConfigSchema = object({
27483
+ defaultAgent: runtimeAgentIdSchema.default("claude"),
27484
+ maxParallelTasks: number$2().int().positive().default(4),
27485
+ maxParallelQA: number$2().int().positive().default(1),
27486
+ maxAutoFixAttempts: number$2().int().nonnegative().default(3),
27487
+ pollingIntervalSeconds: number$2().int().positive().default(30),
27488
+ prPollingIntervalSeconds: number$2().int().positive().default(60),
27489
+ terminalApp: string$2().optional(),
27490
+ slackEnabled: boolean$1().default(true),
27491
+ slackBotToken: string$2().optional(),
27492
+ slackSigningSecret: string$2().optional(),
27493
+ slackAppConfigToken: string$2().optional(),
27494
+ slackClientId: string$2().optional(),
27495
+ slackClientSecret: string$2().optional(),
27496
+ slackAppId: string$2().optional(),
27497
+ slackOauthAuthorizeUrl: string$2().optional(),
27498
+ slackPublicUrl: string$2().optional(),
27499
+ slackBotName: string$2().default("Whipped"),
27500
+ slackInstallerUserId: string$2().optional(),
27501
+ autoStartTunnel: boolean$1().default(false),
27502
+ tunnelId: string$2().optional(),
27503
+ tunnelDomain: string$2().optional(),
27504
+ tunnelName: string$2().default("whipped"),
27505
+ // Auth: single shared password (scrypt hash) + HMAC secret for signed session
27506
+ // cookies + machine token for local agent machinery (MCP/hooks). Never expose
27507
+ // these over the API — see configController's response.
27508
+ authPasswordHash: string$2().optional(),
27509
+ authSessionSecret: string$2().optional(),
27510
+ authMachineToken: string$2().optional()
27511
+ });
27512
+ const runtimeGithubConfigSchema = object({
27513
+ token: string$2()
27514
+ });
27515
+ const runtimeWorktreeSetupSchema = object({
27516
+ filesToCopy: array(string$2()).default([]),
27517
+ installCommand: string$2().default("")
27518
+ });
27519
+ const runtimeProjectSecretSchema = object({
27520
+ key: string$2().min(1),
27521
+ value: string$2()
27522
+ });
27523
+ const BUILTIN_SECRET_KEYS = ["GITHUB_TOKEN"];
27524
+ const runtimeProjectConfigSchema = object({
27525
+ name: string$2().optional(),
27526
+ defaultAgent: runtimeAgentIdSchema.optional(),
27527
+ maxParallelTasks: number$2().int().positive().optional(),
27528
+ maxAutoFixAttempts: number$2().int().nonnegative().optional(),
27529
+ pollingIntervalSeconds: number$2().int().positive().optional(),
27530
+ // What happens when a card passes review (polling/dispatch is always on; per-ticket
27531
+ // readyForDev gates pickup). "off" parks it in ready_for_review, "pr" auto-creates a
27532
+ // GitHub PR, "yolo" merges the branch straight into the local baseRef and pushes.
27533
+ deliveryMode: _enum(["off", "pr", "yolo"]).default("off"),
27534
+ autoCommit: boolean$1().default(true),
27535
+ defaultBaseBranch: string$2().optional(),
27536
+ github: runtimeGithubConfigSchema.optional(),
27537
+ worktreeSetup: runtimeWorktreeSetupSchema.optional(),
27538
+ startCommand: string$2().default(""),
27539
+ workflows: array(workflowSchema).default([]),
27540
+ secrets: array(runtimeProjectSecretSchema).default([]),
27541
+ systemPrompt: string$2().optional(),
27542
+ // Freeform instructions injected into the dev agent's prompt to shape PR
27543
+ // titles, descriptions, and commit messages. Empty/absent → daemon falls
27544
+ // back to DEFAULT_GIT_INSTRUCTIONS.
27545
+ gitInstructions: string$2().optional(),
27546
+ // Which agent binary/model/effort the assistant agent runs as. Absent → claude.
27547
+ assistantModel: agentModelChoiceSchema.optional()
27548
+ });
27549
+ object({
27550
+ workspaceId: string$2(),
27551
+ repoPath: string$2(),
27552
+ board: runtimeBoardDataSchema,
27553
+ revision: number$2(),
27554
+ projectConfig: runtimeProjectConfigSchema
27555
+ });
27556
+ object({
27557
+ board: runtimeBoardDataSchema,
27558
+ revision: number$2()
27559
+ });
27560
+ const runtimeVisualElementSchema = object({
27561
+ elementSelector: string$2().optional(),
27562
+ elementText: string$2().optional(),
27563
+ componentName: string$2().optional(),
27564
+ componentChain: array(string$2()).optional(),
27565
+ sourceFile: string$2().optional(),
27566
+ sourceLine: number$2().optional(),
27567
+ // The page the element was captured on. Selections can span pages, so this is
27568
+ // per-element rather than relying on the visualComment-level pageUrl.
27569
+ pageUrl: string$2().optional()
27570
+ });
27571
+ const runtimeVisualCommentSchema = object({
27572
+ pageUrl: string$2().optional(),
27573
+ elements: array(runtimeVisualElementSchema).default([])
27574
+ });
27575
+ const runtimeCardCreateRequestSchema = object({
27576
+ description: string$2(),
27577
+ type: cardTypeSchema.optional(),
27578
+ // Browser-extension element references; folded into the description server-side.
27579
+ visualComment: runtimeVisualCommentSchema.optional(),
27580
+ agentId: runtimeAgentIdSchema.optional(),
27581
+ priority: runtimeCardPrioritySchema.optional(),
27582
+ readyForDev: boolean$1().optional(),
27583
+ dependsOn: string$2().optional(),
27584
+ waitsFor: array(string$2()).optional(),
27585
+ subtaskIds: array(string$2()).optional(),
27586
+ columnId: runtimeBoardColumnIdSchema.optional(),
27587
+ baseRef: string$2().optional(),
27588
+ githubIssueUrl: string$2().optional(),
27589
+ workflowId: string$2().optional(),
27590
+ descriptionAttachments: array(reviewAttachmentSchema).optional(),
27591
+ branchName: string$2().optional(),
27592
+ // Optional per-ticket overrides edited before creation. When omitted, the card
27593
+ // snapshots the resolved workflow's pairs and defaults the active level to the
27594
+ // workflow's highest configured tier.
27595
+ modelConfig: cardModelConfigSchema.optional(),
27596
+ activeLevel: tierLevelSchema.optional()
27597
+ });
27598
+ object({
27599
+ cardId: string$2(),
27600
+ targetColumnId: runtimeBoardColumnIdSchema,
27601
+ targetIndex: number$2().int().nonnegative().optional(),
27602
+ revision: number$2()
27603
+ });
27604
+ object({
27605
+ cardId: string$2(),
27606
+ description: string$2().optional(),
27607
+ descriptionAttachments: array(reviewAttachmentSchema).optional(),
27608
+ type: cardTypeSchema.optional(),
27609
+ agentId: runtimeAgentIdSchema.optional(),
27610
+ priority: runtimeCardPrioritySchema.optional(),
27611
+ readyForDev: boolean$1().optional(),
27612
+ dependsOn: string$2().optional(),
27613
+ waitsFor: array(string$2()).optional(),
27614
+ subtaskIds: array(string$2()).optional(),
27615
+ workflowId: string$2().optional(),
27616
+ branchName: string$2().optional(),
27617
+ plan: string$2().optional(),
27618
+ activeLevel: tierLevelSchema.optional(),
27619
+ modelConfig: cardModelConfigSchema.optional(),
27620
+ revision: number$2()
27621
+ });
27622
+ const memoryScopeSchema = _enum(["global", "project"]);
27623
+ const memoryTypeSchema = _enum([
27624
+ "fact",
27625
+ "convention",
27626
+ "decision",
27627
+ "preference",
27628
+ "rule",
27629
+ "lesson",
27630
+ "sharp_edge"
27631
+ ]);
27632
+ const MEMORY_TYPE_OPTIONS = [
27633
+ { value: "fact", label: "Fact" },
27634
+ { value: "convention", label: "Convention" },
27635
+ { value: "decision", label: "Decision" },
27636
+ { value: "preference", label: "Preference" },
27637
+ { value: "rule", label: "Rule" },
27638
+ { value: "lesson", label: "Lesson" },
27639
+ { value: "sharp_edge", label: "Sharp edge" }
27640
+ ];
27641
+ const memorySourceTypeSchema = _enum(["user_correction", "explicit_save", "task_lesson", "manual_human"]);
27642
+ const memoryStatusSchema = _enum(["pending", "approved"]);
27643
+ const runtimeMemoryOriginAgentSchema = object({
27644
+ agent: string$2(),
27645
+ model: string$2().optional()
27646
+ });
27647
+ object({
27648
+ id: string$2(),
27649
+ scope: memoryScopeSchema,
27650
+ workspaceId: string$2().nullable(),
27651
+ originWorkspaceId: string$2().nullable().optional(),
27652
+ type: memoryTypeSchema,
27653
+ title: string$2(),
27654
+ content: string$2(),
27655
+ sourceType: memorySourceTypeSchema,
27656
+ importance: number$2().int().min(1).max(3).default(1),
27657
+ tags: array(string$2()).default([]),
27658
+ boundWorkspaceIds: array(string$2()).default([]),
27659
+ originCardId: string$2().nullable().optional(),
27660
+ originAgent: runtimeMemoryOriginAgentSchema.nullable().optional(),
27661
+ status: memoryStatusSchema.default("approved"),
27662
+ createdAt: number$2(),
27663
+ updatedAt: number$2()
27664
+ });
27665
+ function normalizeTag$1(raw2) {
27666
+ return raw2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
27667
+ }
27668
+ _enum(["interval", "calendar"]);
27669
+ const recurringScheduleSchema = discriminatedUnion("kind", [
27670
+ object({ kind: literal("interval"), intervalSeconds: number$2().int().positive() }),
27671
+ object({ kind: literal("calendar"), cronExpr: string$2().min(1), timezone: string$2().min(1) })
27672
+ ]);
27673
+ const recurringRunStatusSchema = _enum(["running", "ok", "error", "killed"]);
27674
+ const recurringRunTriggerSchema = _enum(["schedule", "manual"]);
27675
+ const recurringAgentRunSchema = object({
27676
+ id: string$2(),
27677
+ startedAt: number$2(),
27678
+ endedAt: number$2().optional(),
27679
+ status: recurringRunStatusSchema,
27680
+ summary: string$2().optional(),
27681
+ tokens: number$2().optional(),
27682
+ trigger: recurringRunTriggerSchema.default("schedule"),
27683
+ streamId: string$2().optional()
27684
+ });
27685
+ object({
27686
+ id: string$2(),
27687
+ name: string$2(),
27688
+ instructions: string$2().default(""),
27689
+ schedule: recurringScheduleSchema,
27690
+ model: agentModelChoiceSchema,
27691
+ enabled: boolean$1().default(true),
27692
+ lastRunAt: number$2().optional(),
27693
+ nextRunAt: number$2().optional(),
27694
+ journal: string$2().default(""),
27695
+ createdAt: number$2(),
27696
+ updatedAt: number$2(),
27697
+ recentRuns: array(recurringAgentRunSchema).default([])
27698
+ });
27699
+ object({
27700
+ name: string$2().min(1),
27701
+ instructions: string$2().optional(),
27702
+ schedule: recurringScheduleSchema,
27703
+ model: agentModelChoiceSchema.optional(),
27704
+ enabled: boolean$1().optional()
27705
+ });
27706
+ object({
27707
+ id: string$2(),
27708
+ name: string$2().min(1).optional(),
27709
+ instructions: string$2().optional(),
27710
+ schedule: recurringScheduleSchema.optional(),
27711
+ model: agentModelChoiceSchema.optional(),
27712
+ enabled: boolean$1().optional(),
27713
+ journal: string$2().optional()
27714
+ });
27715
+ const projectFolderSchema = object({
27716
+ id: string$2(),
27717
+ name: string$2(),
27718
+ collapsed: boolean$1().default(false),
27719
+ projectIds: array(string$2())
27720
+ });
27721
+ const topLevelItemSchema = discriminatedUnion("type", [
27722
+ object({ type: literal("folder"), id: string$2() }),
27723
+ object({ type: literal("project"), workspaceId: string$2() })
27724
+ ]);
27725
+ object({
27726
+ version: literal(1),
27727
+ topLevel: array(topLevelItemSchema),
27728
+ folders: record(string$2(), projectFolderSchema)
27729
+ });
27730
+ object({
27731
+ workspaceId: string$2(),
27732
+ repoPath: string$2(),
27733
+ name: string$2(),
27734
+ lastUpdated: number$2()
27735
+ });
27736
+ const addProjectSchema = object({
27737
+ repoPath: string$2().min(1, "Repository path is required"),
27738
+ deliveryMode: _enum(["off", "pr", "yolo"]),
27739
+ defaultBaseBranch: string$2().optional(),
27740
+ assistantModel: agentModelChoiceSchema
27741
+ });
27742
+ function stringifyQuery(query) {
27743
+ const parts = [];
27744
+ for (const [key, value] of Object.entries(query)) {
27745
+ if (value === void 0 || value === null || value === "") {
27746
+ continue;
27747
+ }
27748
+ parts.push(
27749
+ `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
27750
+ );
27751
+ }
27752
+ return parts.join("&");
27753
+ }
27754
+ function buildUrl(baseUrl, path2, query) {
27755
+ const isAbsolute = /^https?:\/\//.test(baseUrl);
27756
+ if (isAbsolute) {
27757
+ const normalizedBase = baseUrl.replace(/\/?$/, "/");
27758
+ const url = new URL(path2.join("/"), normalizedBase);
27759
+ if (query) {
27760
+ url.search = stringifyQuery(query);
27761
+ }
27762
+ return url.toString();
27763
+ }
27764
+ const cleanBase = `/${baseUrl.replace(/^\/|\/$/g, "")}`;
27765
+ const pathStr = path2.length > 0 ? `/${path2.join("/")}` : "";
27766
+ const queryStr = query ? stringifyQuery(query) : "";
27767
+ return `${cleanBase}${pathStr}${queryStr ? `?${queryStr}` : ""}`;
27768
+ }
27769
+ function __DEV__() {
27770
+ return typeof process !== "undefined" && true;
27771
+ }
27772
+ function generateTags(path2) {
27773
+ return path2.map((_2, i2) => path2.slice(0, i2 + 1).join("/"));
27774
+ }
27775
+ function containsFile(value) {
27776
+ if (value instanceof File || value instanceof Blob) return true;
27777
+ if (Array.isArray(value)) return value.some(containsFile);
27778
+ if (value && typeof value === "object") {
27779
+ return Object.values(value).some(containsFile);
27780
+ }
27781
+ return false;
27782
+ }
27783
+ function isJsonBody(body) {
27784
+ if (body === null || body === void 0) return false;
27785
+ if (body instanceof FormData) return false;
27786
+ if (body instanceof Blob) return false;
27787
+ if (body instanceof ArrayBuffer) return false;
27788
+ if (body instanceof URLSearchParams) return false;
27789
+ if (body instanceof ReadableStream) return false;
27790
+ if (typeof body === "string") return false;
27791
+ if (typeof body === "object") {
27792
+ return true;
27793
+ }
27794
+ return false;
27795
+ }
27796
+ async function resolveHeaders(headers) {
27797
+ if (!headers) return void 0;
27798
+ if (typeof headers === "function") {
27799
+ return await headers();
27800
+ }
27801
+ return headers;
27802
+ }
27803
+ function headersInitToRecord(headers) {
27804
+ return Object.fromEntries(new Headers(headers));
27805
+ }
27806
+ async function resolveHeadersToRecord(headers) {
27807
+ const resolved = await resolveHeaders(headers);
27808
+ if (!resolved) return {};
27809
+ return headersInitToRecord(resolved);
27810
+ }
27811
+ async function mergeHeaders(defaultHeaders, requestHeaders) {
27812
+ const resolved1 = await resolveHeaders(defaultHeaders);
27813
+ const resolved2 = await resolveHeaders(requestHeaders);
27814
+ if (!resolved1 && !resolved2) return void 0;
27815
+ if (!resolved1) return resolved2;
27816
+ if (!resolved2) return resolved1;
27817
+ return {
27818
+ ...Object.fromEntries(new Headers(resolved1)),
27819
+ ...Object.fromEntries(new Headers(resolved2))
27820
+ };
27821
+ }
27822
+ function removeHeaderKeys(headers, keysToRemove) {
27823
+ if (!headers) return void 0;
27824
+ const headersObj = new Headers(headers);
27825
+ for (const key of keysToRemove) {
27826
+ headersObj.delete(key);
27827
+ }
27828
+ const entries = [...headersObj.entries()];
27829
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
27830
+ }
27831
+ function objectToFormData(obj) {
27832
+ const formData = new FormData();
27833
+ for (const [key, value] of Object.entries(obj)) {
27834
+ if (value === null || value === void 0) {
27835
+ continue;
27836
+ }
27837
+ if (value instanceof Blob || value instanceof File) {
27838
+ formData.append(key, value);
27839
+ } else if (Array.isArray(value)) {
27840
+ for (const entry of value) {
27841
+ if (entry instanceof Blob || entry instanceof File) {
27842
+ formData.append(key, entry);
27843
+ } else if (typeof entry === "object" && entry !== null) {
27844
+ formData.append(key, JSON.stringify(entry));
27845
+ } else {
27846
+ formData.append(key, String(entry));
27847
+ }
27848
+ }
27849
+ } else if (typeof value === "object") {
27850
+ formData.append(key, JSON.stringify(value));
27851
+ } else {
27852
+ formData.append(key, String(value));
27853
+ }
27854
+ }
27855
+ return formData;
27856
+ }
27857
+ function objectToUrlEncoded(obj) {
27858
+ const params = new URLSearchParams();
27859
+ for (const [key, value] of Object.entries(obj)) {
27860
+ if (value === void 0 || value === null) {
27861
+ continue;
27862
+ }
27863
+ if (Array.isArray(value)) {
27864
+ for (const item of value) {
27865
+ if (item !== void 0 && item !== null) {
27866
+ params.append(key, String(item));
27867
+ }
27868
+ }
27869
+ } else if (typeof value === "object") {
27870
+ params.append(key, JSON.stringify(value));
27871
+ } else {
27872
+ params.append(key, String(value));
27873
+ }
27874
+ }
27875
+ return params.toString();
27876
+ }
27877
+ function sortObjectKeys(obj, seen2 = /* @__PURE__ */ new WeakSet()) {
27878
+ if (obj === null || typeof obj !== "object") return obj;
27879
+ if (seen2.has(obj)) {
27880
+ return "[Circular]";
27881
+ }
27882
+ seen2.add(obj);
27883
+ if (Array.isArray(obj)) {
27884
+ return obj.map((item) => sortObjectKeys(item, seen2));
27885
+ }
27886
+ return Object.keys(obj).sort().reduce(
27887
+ (sorted, key) => {
27888
+ sorted[key] = sortObjectKeys(
27889
+ obj[key],
27890
+ seen2
27891
+ );
27892
+ return sorted;
27893
+ },
27894
+ {}
27895
+ );
27896
+ }
27897
+ function isSpooshBody(value) {
27898
+ return typeof value === "object" && value !== null && "__spooshBody" in value && value.__spooshBody === true;
27899
+ }
27900
+ function resolveRequestBody(rawBody) {
27901
+ if (rawBody === void 0 || rawBody === null) {
27902
+ return void 0;
27903
+ }
27904
+ if (isSpooshBody(rawBody)) {
27905
+ const body = rawBody;
27906
+ switch (body.kind) {
27907
+ case "form":
27908
+ return {
27909
+ body: objectToFormData(body.value),
27910
+ removeHeaders: ["Content-Type"]
27911
+ };
27912
+ case "json":
27913
+ return {
27914
+ body: JSON.stringify(body.value),
27915
+ headers: { "Content-Type": "application/json" }
27916
+ };
27917
+ case "urlencoded":
27918
+ return {
27919
+ body: objectToUrlEncoded(body.value),
27920
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
27921
+ };
27922
+ }
27923
+ }
27924
+ if (isJsonBody(rawBody)) {
27925
+ if (__DEV__() && containsFile(rawBody)) {
27926
+ console.warn(
27927
+ "[spoosh] Plain object body contains File/Blob. Use form() wrapper for multipart upload."
27928
+ );
27929
+ }
27930
+ return {
27931
+ body: JSON.stringify(rawBody),
27932
+ headers: { "Content-Type": "application/json" }
27933
+ };
27934
+ }
27935
+ if (rawBody instanceof FormData) {
27936
+ return { body: rawBody, removeHeaders: ["Content-Type"] };
27937
+ }
27938
+ return { body: rawBody };
27939
+ }
27940
+ function normalizeTag(tag) {
27941
+ return tag.startsWith("/") ? tag.slice(1) : tag;
27942
+ }
27943
+ function matchTag(entryTag, pattern) {
27944
+ const normalizedTag = normalizeTag(entryTag);
27945
+ const normalizedPattern = normalizeTag(pattern);
27946
+ if (normalizedPattern.endsWith("/*")) {
27947
+ const prefix2 = normalizedPattern.slice(0, -2);
27948
+ return prefix2 === "" ? normalizedTag.length > 0 : normalizedTag.startsWith(prefix2 + "/");
27949
+ }
27950
+ return normalizedTag === normalizedPattern;
27951
+ }
27952
+ function matchTags(entryTag, patterns) {
27953
+ return patterns.some((pattern) => matchTag(entryTag, pattern));
27954
+ }
27955
+ function resolveTags(options, resolvedPath) {
27956
+ const tagsOption = options == null ? void 0 : options.tags;
27957
+ if (!tagsOption) {
27958
+ const tag2 = resolvedPath.join("/");
27959
+ return tag2 ? [normalizeTag(tag2)] : [];
27960
+ }
27961
+ if (typeof tagsOption === "string") {
27962
+ return [normalizeTag(tagsOption)];
27963
+ }
27964
+ if (Array.isArray(tagsOption)) {
27965
+ return [...new Set(tagsOption.map(normalizeTag))];
27966
+ }
27967
+ const tag = resolvedPath.join("/");
27968
+ return tag ? [normalizeTag(tag)] : [];
27969
+ }
27970
+ function resolvePath(path2, params) {
27971
+ if (!params) return path2;
27972
+ return path2.map((segment) => {
27973
+ if (segment.startsWith(":")) {
27974
+ const paramName = segment.slice(1);
27975
+ const value = params[paramName];
27976
+ if (value === void 0) {
27977
+ throw new Error(`Missing path parameter: ${paramName}`);
27978
+ }
27979
+ return String(value);
27980
+ }
27981
+ return segment;
27982
+ });
27983
+ }
27984
+ function resolvePathString(path2, params) {
27985
+ if (!params) return path2;
27986
+ return path2.split("/").map((segment) => {
27987
+ if (segment.startsWith(":")) {
27988
+ const paramName = segment.slice(1);
27989
+ const value = params[paramName];
27990
+ return value !== void 0 ? String(value) : segment;
27991
+ }
27992
+ return segment;
27993
+ }).join("/");
27994
+ }
27995
+ var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
27996
+ var fetchTransport = async (url, init) => {
27997
+ const res = await fetch(url, init);
27998
+ const contentType = res.headers.get("content-type");
27999
+ const isJson = contentType == null ? void 0 : contentType.includes("application/json");
28000
+ const isText = (contentType == null ? void 0 : contentType.includes("text/")) || (contentType == null ? void 0 : contentType.includes("application/xml"));
28001
+ let data;
28002
+ if (isJson) {
28003
+ data = await res.json();
28004
+ } else if (isText) {
28005
+ data = await res.text();
28006
+ } else {
28007
+ data = void 0;
28008
+ }
28009
+ return { ok: res.ok, status: res.status, headers: res.headers, data };
28010
+ };
28011
+ var xhrTransport = (url, init, options) => {
28012
+ return new Promise((resolve, reject) => {
28013
+ const xhr = new XMLHttpRequest();
28014
+ xhr.open(init.method ?? "GET", url);
28015
+ if (init.headers) {
28016
+ const headers = init.headers instanceof Headers ? init.headers : new Headers(init.headers);
28017
+ headers.forEach((value, key) => {
28018
+ xhr.setRequestHeader(key, value);
28019
+ });
28020
+ }
28021
+ if (init.credentials === "include") {
28022
+ xhr.withCredentials = true;
28023
+ }
28024
+ const onAbort = () => xhr.abort();
28025
+ if (init.signal) {
28026
+ if (init.signal.aborted) {
28027
+ xhr.abort();
28028
+ return;
28029
+ }
28030
+ init.signal.addEventListener("abort", onAbort);
28031
+ }
28032
+ const cleanup = () => {
28033
+ var _a3;
28034
+ (_a3 = init.signal) == null ? void 0 : _a3.removeEventListener("abort", onAbort);
28035
+ };
28036
+ if (options == null ? void 0 : options.onProgress) {
28037
+ xhr.upload.addEventListener("progress", (event) => {
28038
+ options.onProgress(event, xhr);
28039
+ });
28040
+ xhr.addEventListener("progress", (event) => {
28041
+ options.onProgress(event, xhr);
28042
+ });
28043
+ }
28044
+ xhr.addEventListener("load", () => {
28045
+ cleanup();
28046
+ const status = xhr.status;
28047
+ const ok2 = status >= 200 && status < 300;
28048
+ const responseHeaders = new Headers();
28049
+ const rawHeaders = xhr.getAllResponseHeaders().trim();
28050
+ if (rawHeaders) {
28051
+ rawHeaders.split("\r\n").forEach((line) => {
28052
+ const idx = line.indexOf(": ");
28053
+ if (idx > 0) {
28054
+ responseHeaders.append(
28055
+ line.substring(0, idx),
28056
+ line.substring(idx + 2)
28057
+ );
28058
+ }
28059
+ });
28060
+ }
28061
+ const contentType = responseHeaders.get("content-type");
28062
+ const isJson = contentType == null ? void 0 : contentType.includes("application/json");
28063
+ let data;
28064
+ try {
28065
+ data = isJson ? JSON.parse(xhr.responseText) : xhr.responseText;
28066
+ } catch {
28067
+ data = xhr.responseText;
28068
+ }
28069
+ resolve({ ok: ok2, status, headers: responseHeaders, data });
28070
+ });
28071
+ xhr.addEventListener("error", () => {
28072
+ cleanup();
28073
+ reject(new TypeError("Network request failed"));
28074
+ });
28075
+ xhr.addEventListener("abort", () => {
28076
+ cleanup();
28077
+ reject(new DOMException("Aborted", "AbortError"));
28078
+ });
28079
+ xhr.send(init.body);
28080
+ });
28081
+ };
28082
+ async function executeFetch(baseUrl, path2, method, defaultOptions2, requestOptions, nextTags) {
28083
+ return executeCoreFetch({
28084
+ baseUrl,
28085
+ path: path2,
28086
+ method,
28087
+ defaultOptions: defaultOptions2,
28088
+ requestOptions,
28089
+ middlewareFetchInit: void 0,
28090
+ nextTags
28091
+ });
28092
+ }
28093
+ function buildInputFields(requestOptions) {
28094
+ const fields = {};
28095
+ if ((requestOptions == null ? void 0 : requestOptions.query) !== void 0) {
28096
+ fields.query = requestOptions.query;
28097
+ }
28098
+ if ((requestOptions == null ? void 0 : requestOptions.body) !== void 0) {
28099
+ fields.body = requestOptions.body;
28100
+ }
28101
+ if ((requestOptions == null ? void 0 : requestOptions.params) !== void 0) {
28102
+ fields.params = requestOptions.params;
28103
+ }
28104
+ if (Object.keys(fields).length === 0) {
28105
+ return {};
28106
+ }
28107
+ return { input: fields };
28108
+ }
28109
+ function resolveTransport(option) {
28110
+ if (option === "xhr" && typeof XMLHttpRequest !== "undefined") {
28111
+ return xhrTransport;
28112
+ }
28113
+ return fetchTransport;
28114
+ }
28115
+ async function executeCoreFetch(config2) {
28116
+ const {
28117
+ baseUrl,
28118
+ path: path2,
28119
+ method,
27555
28120
  defaultOptions: defaultOptions2,
27556
28121
  requestOptions,
27557
28122
  middlewareFetchInit,
@@ -29595,19 +30160,19 @@ function resolveInvalidateTags(path2, params, invalidateOption, autoInvalidate,
29595
30160
  return ["*"];
29596
30161
  }
29597
30162
  if (typeof invalidateOption === "string") {
29598
- const normalized = normalizeTag$1(invalidateOption);
30163
+ const normalized = normalizeTag(invalidateOption);
29599
30164
  if (normalized === "*") {
29600
30165
  return ["*"];
29601
30166
  }
29602
30167
  return [normalized];
29603
30168
  }
29604
30169
  if (Array.isArray(invalidateOption)) {
29605
- return [...new Set(invalidateOption.map(normalizeTag$1))];
30170
+ return [...new Set(invalidateOption.map(normalizeTag))];
29606
30171
  }
29607
30172
  if (!autoInvalidate) {
29608
30173
  return false;
29609
30174
  }
29610
- const resolvedPath = normalizeTag$1(resolvePathString(path2, params));
30175
+ const resolvedPath = normalizeTag(resolvePathString(path2, params));
29611
30176
  const segments = resolvedPath.split("/");
29612
30177
  const depth = calculateSegmentDepth(resolvedPath, groups);
29613
30178
  const baseSegments = segments.slice(0, depth);
@@ -29669,7 +30234,7 @@ function invalidationPlugin(config2 = {}) {
29669
30234
  eventEmitter.emit("refetchAll", void 0);
29670
30235
  return;
29671
30236
  }
29672
- const patterns = rawPatterns.map(normalizeTag$1);
30237
+ const patterns = rawPatterns.map(normalizeTag);
29673
30238
  if (patterns.includes("*")) {
29674
30239
  et == null ? void 0 : et.emit("Refetch all (manual)", { color: "warning" });
29675
30240
  eventEmitter.emit("refetchAll", void 0);
@@ -31156,14 +31721,186 @@ function FolderPickerDialog({ initialPath, onSelect, onClose }) {
31156
31721
  }
31157
31722
  ) });
31158
31723
  }
31724
+ function AgentBinarySelect({
31725
+ value,
31726
+ onChange,
31727
+ floatingStrategy,
31728
+ menuClassName
31729
+ }) {
31730
+ const available = useRead((api) => api("agents/available").GET());
31731
+ const availableIds = new Set((available.data ?? []).map((a2) => a2.id));
31732
+ const isMissing = (id) => available.data != null && !availableIds.has(id);
31733
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1", children: [
31734
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
31735
+ Select_default,
31736
+ {
31737
+ value,
31738
+ floatingStrategy,
31739
+ menuClassName,
31740
+ onChange: (v2) => onChange(v2),
31741
+ 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: [
31742
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: isMissing(o2.value) ? "text-[#8888a0]" : "", children: o2.label }),
31743
+ isMissing(o2.value) && /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 11, className: "text-amber-500 ml-auto shrink-0" })
31744
+ ] }) }, o2.value))
31745
+ }
31746
+ ),
31747
+ isMissing(value) && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-[10px] text-amber-400", children: [
31748
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 11, className: "shrink-0" }),
31749
+ "Not installed — this agent won't run."
31750
+ ] })
31751
+ ] });
31752
+ }
31753
+ function ModelSelect({
31754
+ agentId,
31755
+ value,
31756
+ onChange,
31757
+ floatingStrategy,
31758
+ menuClassName
31759
+ }) {
31760
+ const staticOptions = MODEL_OPTIONS[agentId];
31761
+ const isDynamic = agentId === "opencode" || agentId === "cursor";
31762
+ const modelsRead = useRead(
31763
+ (api) => api("agents/models").GET({
31764
+ query: { agent: agentId === "cursor" ? "cursor" : "opencode" }
31765
+ }),
31766
+ { enabled: isDynamic }
31767
+ );
31768
+ const dynamicModels = modelsRead.data ?? [];
31769
+ const isFetching = modelsRead.fetching;
31770
+ const options = isDynamic ? dynamicModels : staticOptions;
31771
+ const [customChosen, setCustomChosen] = reactExports.useState(false);
31772
+ const isPresetValue = value === "" || options.some((o2) => o2.value === value);
31773
+ const customMode = customChosen || !isPresetValue;
31774
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
31775
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
31776
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
31777
+ Select_default,
31778
+ {
31779
+ floatingStrategy,
31780
+ menuClassName,
31781
+ value: customMode ? "__custom__" : value,
31782
+ onChange: (v2) => {
31783
+ if (v2 === "__custom__") {
31784
+ setCustomChosen(true);
31785
+ } else {
31786
+ setCustomChosen(false);
31787
+ onChange(v2);
31788
+ }
31789
+ },
31790
+ filterable: true,
31791
+ children: [
31792
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: "", label: "Default" }),
31793
+ options.map((o2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: o2.value, label: o2.label }, o2.value)),
31794
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: "__custom__", label: "Custom..." })
31795
+ ]
31796
+ }
31797
+ ) }),
31798
+ isDynamic && /* @__PURE__ */ jsxRuntimeExports.jsx(
31799
+ "button",
31800
+ {
31801
+ type: "button",
31802
+ onClick: () => void modelsRead.trigger(),
31803
+ disabled: isFetching,
31804
+ title: "Refresh model list",
31805
+ 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",
31806
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31807
+ "svg",
31808
+ {
31809
+ className: classNames("w-4 h-4", isFetching ? "animate-spin" : ""),
31810
+ fill: "none",
31811
+ viewBox: "0 0 24 24",
31812
+ stroke: "currentColor",
31813
+ strokeWidth: 2,
31814
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31815
+ "path",
31816
+ {
31817
+ strokeLinecap: "round",
31818
+ strokeLinejoin: "round",
31819
+ 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"
31820
+ }
31821
+ )
31822
+ }
31823
+ )
31824
+ }
31825
+ )
31826
+ ] }),
31827
+ customMode && /* @__PURE__ */ jsxRuntimeExports.jsx(
31828
+ Input_default,
31829
+ {
31830
+ value,
31831
+ onChange: (e) => onChange(e.target.value),
31832
+ 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"
31833
+ }
31834
+ )
31835
+ ] });
31836
+ }
31837
+ function AgentModelPicker({
31838
+ value,
31839
+ onChange,
31840
+ floatingStrategy,
31841
+ menuClassName
31842
+ }) {
31843
+ const agentId = value.agentId ?? "claude";
31844
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-start", children: [
31845
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full sm:w-32 shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31846
+ AgentBinarySelect,
31847
+ {
31848
+ value: agentId,
31849
+ floatingStrategy,
31850
+ menuClassName,
31851
+ onChange: (v2) => onChange({ ...value, agentId: v2, model: null })
31852
+ }
31853
+ ) }),
31854
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31855
+ ModelSelect,
31856
+ {
31857
+ agentId,
31858
+ value: value.model ?? "",
31859
+ onChange: (v2) => onChange({ ...value, model: v2 || null }),
31860
+ floatingStrategy,
31861
+ menuClassName
31862
+ }
31863
+ ) }),
31864
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full sm:w-36 shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
31865
+ Select_default,
31866
+ {
31867
+ value: value.effort ?? "",
31868
+ floatingStrategy,
31869
+ menuClassName,
31870
+ onChange: (v2) => onChange({ ...value, effort: v2 || null }),
31871
+ children: [
31872
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: "", label: "Default effort" }),
31873
+ EFFORT_OPTIONS.map((o2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: o2.value, label: o2.label }, o2.value))
31874
+ ]
31875
+ }
31876
+ ) })
31877
+ ] });
31878
+ }
31879
+ function BranchSelect({ branches, value, onChange, placeholder = "Select branch" }) {
31880
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
31881
+ Select_default,
31882
+ {
31883
+ value,
31884
+ onChange: (v2) => onChange(v2),
31885
+ placeholder,
31886
+ filterable: true,
31887
+ prefix: /* @__PURE__ */ jsxRuntimeExports.jsx(GitBranch, { size: 13, className: "text-[#8888a0]" }),
31888
+ children: branches.map((b2) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectOption_default, { value: b2, label: b2 }, b2))
31889
+ }
31890
+ );
31891
+ }
31159
31892
  function ConfigureStep({
31160
31893
  repoPath,
31894
+ branches,
31161
31895
  adding,
31162
31896
  onBack,
31163
31897
  onAdd
31164
31898
  }) {
31899
+ const { setValue } = useFormContext();
31165
31900
  const folderName = repoPath.split("/").filter(Boolean).at(-1) ?? repoPath;
31166
31901
  const deliveryMode = useWatch({ name: "deliveryMode" });
31902
+ const defaultBaseBranch = useWatch({ name: "defaultBaseBranch" }) ?? "";
31903
+ const assistantModel = useWatch({ name: "assistantModel" }) ?? DEFAULT_AGENT_MODEL_CHOICE;
31167
31904
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col min-h-0", children: [
31168
31905
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto p-6 flex flex-col gap-5", children: [
31169
31906
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
@@ -31194,12 +31931,36 @@ function ConfigureStep({
31194
31931
  ] })
31195
31932
  ] })
31196
31933
  ] }),
31934
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3.5", children: [
31935
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-medium uppercase text-[#4a4a5a] tracking-[1px]", children: "Git defaults" }),
31936
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-3", children: [
31937
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
31938
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[13px] text-[#c0c0d0]", children: "Default base branch" }),
31939
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] mt-0.5 text-[#4a4a5a]", children: "Used when creating new tasks and stories." })
31940
+ ] }),
31941
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-40", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31942
+ BranchSelect,
31943
+ {
31944
+ branches,
31945
+ value: defaultBaseBranch,
31946
+ onChange: (v2) => setValue("defaultBaseBranch", v2 || void 0, { shouldDirty: true }),
31947
+ placeholder: "main"
31948
+ }
31949
+ ) })
31950
+ ] })
31951
+ ] }),
31197
31952
  /* @__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." })
31953
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-medium uppercase text-[#4a4a5a] tracking-[1px]", children: "Assistant" }),
31954
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5", children: [
31955
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[13px] text-[#c0c0d0]", children: "Agent & model" }),
31956
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-[#4a4a5a]", children: "Which agent runs the in-app assistant." }),
31957
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
31958
+ AgentModelPicker,
31959
+ {
31960
+ value: assistantModel,
31961
+ onChange: (next2) => setValue("assistantModel", next2, { shouldDirty: true })
31962
+ }
31963
+ ) })
31203
31964
  ] })
31204
31965
  ] })
31205
31966
  ] }),
@@ -31341,7 +32102,8 @@ function AddProjectDialog({ onClose, onAdded }) {
31341
32102
  values: {
31342
32103
  repoPath: "",
31343
32104
  deliveryMode: "off",
31344
- installCommand: ""
32105
+ defaultBaseBranch: void 0,
32106
+ assistantModel: DEFAULT_AGENT_MODEL_CHOICE
31345
32107
  }
31346
32108
  });
31347
32109
  const { control, getValues, setValue } = methods;
@@ -31364,12 +32126,19 @@ function AddProjectDialog({ onClose, onAdded }) {
31364
32126
  const pathStatus = !trimmed ? "idle" : trimmed !== debouncedPath || checking ? "checking" : checkError ? "invalid" : pathData ? pathData.valid ? "valid" : "invalid" : "checking";
31365
32127
  const pathError = checkError ? "Failed to check path" : pathStatus === "invalid" ? (pathData == null ? void 0 : pathData.error) ?? null : null;
31366
32128
  const repoInfo = pathStatus === "valid" && pathData ? { name: pathData.name, branch: pathData.branch, remote: pathData.remote } : { name: null, branch: null, remote: null };
32129
+ const currentBranch = repoInfo.branch;
32130
+ reactExports.useEffect(() => {
32131
+ if (currentBranch && !getValues("defaultBaseBranch")) {
32132
+ setValue("defaultBaseBranch", currentBranch);
32133
+ }
32134
+ }, [currentBranch, getValues, setValue]);
31367
32135
  const addProjectWrite = useWrite((api) => api("projects").POST());
31368
32136
  const handleAdd = methods.handleSubmit(async (values) => {
31369
32137
  var _a3, _b2;
31370
32138
  const initialConfig = {
31371
32139
  deliveryMode: values.deliveryMode,
31372
- worktreeSetup: ((_a3 = values.installCommand) == null ? void 0 : _a3.trim()) ? { filesToCopy: [], installCommand: values.installCommand.trim() } : void 0
32140
+ defaultBaseBranch: ((_a3 = values.defaultBaseBranch) == null ? void 0 : _a3.trim()) || void 0,
32141
+ assistantModel: values.assistantModel
31373
32142
  };
31374
32143
  const res = await addProjectWrite.trigger({
31375
32144
  body: { repoPath: values.repoPath.trim(), initialConfig }
@@ -31471,6 +32240,7 @@ function AddProjectDialog({ onClose, onAdded }) {
31471
32240
  ConfigureStep,
31472
32241
  {
31473
32242
  repoPath: getValues("repoPath"),
32243
+ branches: (pathData == null ? void 0 : pathData.branches) ?? [],
31474
32244
  adding: addProjectWrite.loading,
31475
32245
  onBack: () => setStep("select"),
31476
32246
  onAdd: () => void handleAdd()
@@ -48654,195 +49424,34 @@ const ConnectedDroppable = connect_default(makeMapStateToProps, mapDispatchToPro
48654
49424
  return {
48655
49425
  ...attachDefaultPropsToOwnProps(ownProps),
48656
49426
  ...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 };
49427
+ ...dispatchProps
49428
+ };
49429
+ }, {
49430
+ context: StoreContext,
49431
+ areStatePropsEqual: isStrictEqual
49432
+ })(Droppable);
49433
+ function EmptyFolderSlot({ dp }) {
49434
+ 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: [
49435
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1 h-1 rounded-full bg-[#2a2a35]" }),
49436
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-[#3a3a45]", children: "Drop project here" })
49437
+ ] }) });
48836
49438
  }
48837
- function ProjectItem({
48838
- project,
48839
- workspaceId,
49439
+ function FolderHeader({
49440
+ folderId,
49441
+ folder,
48840
49442
  dp,
48841
49443
  snap,
48842
- isActive: isActive2,
48843
- indent: indent2,
48844
- onSwitch,
48845
- onRemove
49444
+ expanded,
49445
+ hovered,
49446
+ editing,
49447
+ editName,
49448
+ editRef,
49449
+ onToggleCollapse,
49450
+ onStartRename,
49451
+ onDeleteFolder,
49452
+ onEditNameChange,
49453
+ onCommitRename,
49454
+ onCancelRename
48846
49455
  }) {
48847
49456
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
48848
49457
  "div",
@@ -48850,916 +49459,514 @@ function ProjectItem({
48850
49459
  ref: dp.innerRef,
48851
49460
  ...dp.draggableProps,
48852
49461
  ...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
- ),
49462
+ onClick: () => onToggleCollapse(folderId),
49463
+ 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
49464
  style: {
48860
49465
  ...dp.draggableProps.style,
48861
- background: isActive2 && !snap.isDragging ? "#7c6aff18" : "transparent",
48862
- borderLeft: isActive2 && !snap.isDragging ? "2px solid #7c6aff" : "2px solid transparent"
49466
+ background: snap.isDragging ? "#1f1f28" : hovered ? "#7c6aff15" : "transparent"
48863
49467
  },
48864
49468
  children: [
48865
- /* @__PURE__ */ jsxRuntimeExports.jsx(
48866
- "div",
49469
+ /* @__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 }) }),
49470
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Folder, { size: 13, className: classNames("shrink-0", hovered ? "text-[#7c6aff]" : "text-[#60607a]") }),
49471
+ editing ? /* @__PURE__ */ jsxRuntimeExports.jsx(
49472
+ "input",
48867
49473
  {
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
49474
+ ref: editRef,
49475
+ value: editName,
49476
+ onChange: (e) => onEditNameChange(e.target.value),
49477
+ onBlur: onCommitRename,
49478
+ onClick: (e) => e.stopPropagation(),
49479
+ onKeyDown: (e) => {
49480
+ if (e.key === "Enter") onCommitRename();
49481
+ if (e.key === "Escape") onCancelRename();
49482
+ },
49483
+ className: "flex-1 min-w-0 outline-none text-[11px] rounded px-1 bg-[#0c0c0f] border border-[#3a3a55] text-[#f0f0f5]"
48870
49484
  }
48871
- ),
48872
- /* @__PURE__ */ jsxRuntimeExports.jsx(
49485
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
48873
49486
  "span",
48874
49487
  {
48875
49488
  className: classNames(
48876
- "flex-1 min-w-0 truncate text-[12px]",
48877
- isActive2 ? "text-[#f0f0f5] font-medium" : "text-[#8888a0] font-normal"
49489
+ "flex-1 min-w-0 truncate text-[11px] font-medium tracking-[0.2px]",
49490
+ hovered ? "text-[#c0c0d0]" : "text-[#8888a0]"
48878
49491
  ),
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) => {
49492
+ onDoubleClick: (e) => {
48886
49493
  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
- });
49494
+ onStartRename(folderId);
48897
49495
  },
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 })
49496
+ children: folder.name
49145
49497
  }
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(
49498
+ ),
49499
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "shrink-0 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity", children: [
49500
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
49158
49501
  "button",
49159
49502
  {
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 })
49503
+ onClick: (e) => {
49504
+ e.stopPropagation();
49505
+ onStartRename(folderId);
49506
+ },
49507
+ className: "flex items-center justify-center w-5 h-5 rounded hover:bg-[#2a2a35] transition-colors text-[#60607a]",
49508
+ title: "Rename",
49509
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Pencil, { size: 10 })
49510
+ }
49511
+ ),
49512
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
49513
+ "button",
49514
+ {
49515
+ onClick: (e) => {
49516
+ e.stopPropagation();
49517
+ onDeleteFolder(folderId);
49518
+ },
49519
+ className: "flex items-center justify-center w-5 h-5 rounded hover:bg-[#ef444420] transition-colors text-[#60607a]",
49520
+ title: "Delete",
49521
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 10 })
49164
49522
  }
49165
49523
  )
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
- ] });
49524
+ ] })
49525
+ ]
49526
+ }
49527
+ );
49199
49528
  }
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];
49529
+ function genId() {
49530
+ return Math.random().toString(36).slice(2, 10);
49531
+ }
49532
+ function buildFlat(layout, expandAll, isDragging2 = false) {
49533
+ var _a3, _b2;
49534
+ const flat = [];
49535
+ for (const item of layout.topLevel) {
49536
+ if (item.type === "folder") {
49537
+ flat.push({ kind: "folder-header", folderId: item.id });
49538
+ const expanded = expandAll || !((_a3 = layout.folders[item.id]) == null ? void 0 : _a3.collapsed);
49539
+ if (expanded) {
49540
+ const projectIds = ((_b2 = layout.folders[item.id]) == null ? void 0 : _b2.projectIds) ?? [];
49541
+ for (const wsId of projectIds) {
49542
+ flat.push({ kind: "project", workspaceId: wsId, folderId: item.id });
49543
+ }
49544
+ if (isDragging2 && projectIds.length === 0) {
49545
+ flat.push({ kind: "empty-folder-slot", folderId: item.id });
49546
+ }
49547
+ }
49548
+ } else {
49549
+ flat.push({ kind: "project", workspaceId: item.workspaceId, folderId: null });
49550
+ }
49287
49551
  }
49552
+ return flat;
49288
49553
  }
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;
49554
+ function flatToLayout(flat, existing) {
49555
+ var _a3;
49556
+ const topLevel = [];
49557
+ const folders = Object.fromEntries(
49558
+ Object.entries(existing.folders).map(([k, v2]) => [k, { ...v2, projectIds: [] }])
49559
+ );
49560
+ const seen2 = /* @__PURE__ */ new Set();
49561
+ for (const item of flat) {
49562
+ if (item.kind === "empty-folder-slot") continue;
49563
+ if (item.kind === "folder-header") {
49564
+ if (!seen2.has(item.folderId)) {
49565
+ seen2.add(item.folderId);
49566
+ topLevel.push({ type: "folder", id: item.folderId });
49567
+ }
49568
+ } else if (item.folderId !== null) {
49569
+ (_a3 = folders[item.folderId]) == null ? void 0 : _a3.projectIds.push(item.workspaceId);
49570
+ } else {
49571
+ topLevel.push({ type: "project", workspaceId: item.workspaceId });
49572
+ }
49303
49573
  }
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];
49574
+ return { ...existing, topLevel, folders };
49575
+ }
49576
+ function folderAtDest(flat, destIndex) {
49577
+ const after = flat[destIndex];
49578
+ if (!after) {
49579
+ const prev = flat[destIndex - 1];
49580
+ if (!prev || prev.kind === "folder-header") return null;
49581
+ if (prev.kind === "empty-folder-slot") return prev.folderId;
49582
+ return prev.folderId;
49307
49583
  }
49308
- const fallback = cfg.pairs[0];
49309
- if (!fallback) throw new Error("resolvePair: slot has no model pairs");
49310
- return fallback;
49584
+ if (after.kind === "folder-header") return after.folderId;
49585
+ if (after.kind === "empty-folder-slot") return after.folderId;
49586
+ return after.folderId;
49311
49587
  }
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));
49588
+ function syncLayout(layout, projects) {
49589
+ const known = new Set(projects.map((p) => p.workspaceId));
49590
+ const topLevel = layout.topLevel.filter((i2) => i2.type === "folder" || known.has(i2.workspaceId));
49591
+ const folders = Object.fromEntries(
49592
+ Object.entries(layout.folders).map(([id, f]) => [
49593
+ id,
49594
+ { ...f, projectIds: f.projectIds.filter((id2) => known.has(id2)) }
49595
+ ])
49596
+ );
49597
+ const inLayout = /* @__PURE__ */ new Set();
49598
+ for (const i2 of topLevel) {
49599
+ if (i2.type === "project") inLayout.add(i2.workspaceId);
49352
49600
  }
49353
- return LEVEL_ORDER[bestIdx] ?? "medium";
49601
+ for (const f of Object.values(folders)) {
49602
+ for (const id of f.projectIds) inLayout.add(id);
49603
+ }
49604
+ const newItems = projects.filter((p) => !inLayout.has(p.workspaceId)).map((p) => ({ type: "project", workspaceId: p.workspaceId }));
49605
+ return { ...layout, topLevel: [...topLevel, ...newItems], folders };
49354
49606
  }
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, "");
49607
+ function ProjectItem({
49608
+ project,
49609
+ workspaceId,
49610
+ dp,
49611
+ snap,
49612
+ isActive: isActive2,
49613
+ indent: indent2,
49614
+ onSwitch,
49615
+ onRemove
49616
+ }) {
49617
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
49618
+ "div",
49619
+ {
49620
+ ref: dp.innerRef,
49621
+ ...dp.draggableProps,
49622
+ ...dp.dragHandleProps,
49623
+ onClick: () => onSwitch(workspaceId),
49624
+ className: classNames(
49625
+ "group flex items-center gap-2 h-8 pr-2 my-px mx-1 rounded-md cursor-pointer select-none transition-colors",
49626
+ indent2 ? "pl-10" : "pl-3",
49627
+ snap.isDragging ? "opacity-70" : isActive2 ? "" : "hover:bg-[#1a1a1f]"
49628
+ ),
49629
+ style: {
49630
+ ...dp.draggableProps.style,
49631
+ background: isActive2 && !snap.isDragging ? "#7c6aff18" : "transparent",
49632
+ borderLeft: isActive2 && !snap.isDragging ? "2px solid #7c6aff" : "2px solid transparent"
49633
+ },
49634
+ children: [
49635
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
49636
+ "div",
49637
+ {
49638
+ className: classNames("w-1.5 h-1.5 rounded-full shrink-0", isActive2 ? "bg-[#7c6aff]" : "bg-[#2a2a35]"),
49639
+ style: isActive2 ? { boxShadow: "0 0 6px #7c6aff80" } : void 0
49640
+ }
49641
+ ),
49642
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
49643
+ "span",
49644
+ {
49645
+ className: classNames(
49646
+ "flex-1 min-w-0 truncate text-[12px]",
49647
+ isActive2 ? "text-[#f0f0f5] font-medium" : "text-[#8888a0] font-normal"
49648
+ ),
49649
+ children: project.name
49650
+ }
49651
+ ),
49652
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "shrink-0 flex items-center opacity-0 group-hover:opacity-100 transition-opacity", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49653
+ "button",
49654
+ {
49655
+ onClick: (e) => {
49656
+ e.stopPropagation();
49657
+ ConfirmDialog_default.show({
49658
+ title: "Remove project",
49659
+ content: `Remove "${project.name}" from Whipped? This will stop all running agents and delete all associated worktrees and data.`,
49660
+ confirmButtonLabel: "Remove",
49661
+ cancelButtonLabel: "Cancel",
49662
+ onConfirm: async ({ dismiss: dismiss2 }) => {
49663
+ await onRemove(workspaceId);
49664
+ dismiss2();
49665
+ }
49666
+ });
49667
+ },
49668
+ className: "flex items-center justify-center w-5 h-5 rounded hover:bg-[#ef444420] transition-colors text-[#60607a] hover:text-[#ef4444]",
49669
+ title: "Remove project",
49670
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 10 })
49671
+ }
49672
+ ) })
49673
+ ]
49674
+ }
49675
+ );
49694
49676
  }
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()
49677
+ const ProjectsSidebar = React.forwardRef(function ProjectsSidebar2({ projects, activeWorkspaceId, onSwitch, onRemove }, ref) {
49678
+ const [layout, setLayout] = reactExports.useState({ version: 1, topLevel: [], folders: {} });
49679
+ const [isDragging2, setIsDragging] = reactExports.useState(false);
49680
+ const [hoveredFolderId, setHoveredFolderId] = reactExports.useState(null);
49681
+ const [editingId, setEditingId] = reactExports.useState(null);
49682
+ const [editName, setEditName] = reactExports.useState("");
49683
+ const editRef = reactExports.useRef(null);
49684
+ const saveTimer = reactExports.useRef(null);
49685
+ const { trigger: fetchLayout } = useRead((api) => api("projects/layout").GET(), { enabled: false });
49686
+ const { trigger: saveLayout } = useWrite((api) => api("projects/layout").PUT());
49687
+ reactExports.useEffect(() => {
49688
+ fetchLayout().then((res) => setLayout((prev) => syncLayout(res.data ?? prev, projects))).catch(() => {
49689
+ });
49690
+ }, []);
49691
+ reactExports.useEffect(() => {
49692
+ setLayout((prev) => syncLayout(prev, projects));
49693
+ }, [projects]);
49694
+ const persist = (next2) => {
49695
+ if (saveTimer.current) clearTimeout(saveTimer.current);
49696
+ saveTimer.current = setTimeout(() => {
49697
+ saveLayout({ body: next2 }).catch(() => {
49698
+ });
49699
+ }, 300);
49700
+ };
49701
+ const update2 = (next2) => {
49702
+ setLayout(next2);
49703
+ persist(next2);
49704
+ };
49705
+ const addFolder = () => {
49706
+ const id = genId();
49707
+ const next2 = {
49708
+ ...layout,
49709
+ topLevel: [...layout.topLevel, { type: "folder", id }],
49710
+ folders: { ...layout.folders, [id]: { id, name: "New Folder", collapsed: false, projectIds: [] } }
49711
+ };
49712
+ update2(next2);
49713
+ setEditingId(id);
49714
+ setEditName("New Folder");
49715
+ setTimeout(() => {
49716
+ var _a3, _b2;
49717
+ (_a3 = editRef.current) == null ? void 0 : _a3.focus();
49718
+ (_b2 = editRef.current) == null ? void 0 : _b2.select();
49719
+ }, 50);
49720
+ };
49721
+ reactExports.useImperativeHandle(ref, () => ({ addFolder }));
49722
+ const startRename = (id) => {
49723
+ var _a3;
49724
+ setEditingId(id);
49725
+ setEditName(((_a3 = layout.folders[id]) == null ? void 0 : _a3.name) ?? "");
49726
+ setTimeout(() => {
49727
+ var _a4, _b2;
49728
+ (_a4 = editRef.current) == null ? void 0 : _a4.focus();
49729
+ (_b2 = editRef.current) == null ? void 0 : _b2.select();
49730
+ }, 50);
49731
+ };
49732
+ const commitRename = () => {
49733
+ if (!editingId) return;
49734
+ update2({
49735
+ ...layout,
49736
+ folders: {
49737
+ ...layout.folders,
49738
+ [editingId]: { ...layout.folders[editingId], name: editName.trim() || "Untitled" }
49739
+ }
49740
+ });
49741
+ setEditingId(null);
49742
+ };
49743
+ const deleteFolder = (id) => {
49744
+ const folder = layout.folders[id];
49745
+ if (!folder) return;
49746
+ const idx = layout.topLevel.findIndex((i2) => i2.type === "folder" && i2.id === id);
49747
+ const returned = folder.projectIds.map((ws2) => ({ type: "project", workspaceId: ws2 }));
49748
+ const topLevel = [...layout.topLevel];
49749
+ topLevel.splice(idx, 1, ...returned);
49750
+ const folders = { ...layout.folders };
49751
+ delete folders[id];
49752
+ update2({ ...layout, topLevel, folders });
49753
+ };
49754
+ const toggleCollapse = (id) => {
49755
+ const f = layout.folders[id];
49756
+ if (!f) return;
49757
+ update2({ ...layout, folders: { ...layout.folders, [id]: { ...f, collapsed: !f.collapsed } } });
49758
+ };
49759
+ const onDragUpdate2 = reactExports.useCallback(
49760
+ (update22) => {
49761
+ if (!update22.destination) {
49762
+ setHoveredFolderId(null);
49763
+ return;
49764
+ }
49765
+ const flat2 = buildFlat(layout, true, true);
49766
+ flat2.splice(update22.source.index, 1);
49767
+ const folderId = folderAtDest(flat2, update22.destination.index);
49768
+ setHoveredFolderId(folderId);
49769
+ },
49770
+ [layout]
49771
+ );
49772
+ const onDragEnd2 = (result) => {
49773
+ setIsDragging(false);
49774
+ setHoveredFolderId(null);
49775
+ if (!result.destination) return;
49776
+ const { draggableId, source, destination } = result;
49777
+ if (source.index === destination.index) return;
49778
+ const flat2 = buildFlat(layout, true, true);
49779
+ if (draggableId.startsWith("fh:")) {
49780
+ const folderId = draggableId.slice(3);
49781
+ flat2.splice(source.index, 1);
49782
+ const group = [];
49783
+ 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)) {
49784
+ group.push(flat2.splice(source.index, 1)[0]);
49785
+ }
49786
+ const at2 = Math.min(destination.index, flat2.length);
49787
+ flat2.splice(at2, 0, { kind: "folder-header", folderId }, ...group);
49788
+ } else {
49789
+ const [moved] = flat2.splice(source.index, 1);
49790
+ const newFolderId = folderAtDest(flat2, destination.index);
49791
+ flat2.splice(destination.index, 0, { ...moved, folderId: newFolderId });
49792
+ }
49793
+ update2(flatToLayout(flat2, layout));
49794
+ };
49795
+ const projectMap = Object.fromEntries(projects.map((p) => [p.workspaceId, p]));
49796
+ const flat = buildFlat(layout, isDragging2, isDragging2);
49797
+ 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: [
49798
+ flat.map((item, index2) => {
49799
+ if (item.kind === "folder-header") {
49800
+ const folder = layout.folders[item.folderId];
49801
+ if (!folder) return null;
49802
+ const expanded = !folder.collapsed || isDragging2;
49803
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(PublicDraggable, { draggableId: `fh:${item.folderId}`, index: index2, children: (dp, snap) => /* @__PURE__ */ jsxRuntimeExports.jsx(
49804
+ FolderHeader,
49805
+ {
49806
+ folderId: item.folderId,
49807
+ folder,
49808
+ dp,
49809
+ snap,
49810
+ expanded,
49811
+ hovered: hoveredFolderId === item.folderId,
49812
+ editing: editingId === item.folderId,
49813
+ editName,
49814
+ editRef,
49815
+ onToggleCollapse: toggleCollapse,
49816
+ onStartRename: startRename,
49817
+ onDeleteFolder: deleteFolder,
49818
+ onEditNameChange: setEditName,
49819
+ onCommitRename: commitRename,
49820
+ onCancelRename: () => setEditingId(null)
49821
+ }
49822
+ ) }, `fh:${item.folderId}`);
49823
+ }
49824
+ if (item.kind === "empty-folder-slot") {
49825
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
49826
+ PublicDraggable,
49827
+ {
49828
+ draggableId: `slot:${item.folderId}`,
49829
+ index: index2,
49830
+ isDragDisabled: true,
49831
+ children: (dp) => /* @__PURE__ */ jsxRuntimeExports.jsx(EmptyFolderSlot, { dp })
49832
+ },
49833
+ `slot:${item.folderId}`
49834
+ );
49835
+ }
49836
+ const project = projectMap[item.workspaceId];
49837
+ if (!project) return null;
49838
+ const isActive2 = item.workspaceId === activeWorkspaceId;
49839
+ const indent2 = item.folderId !== null;
49840
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(PublicDraggable, { draggableId: `p:${item.workspaceId}`, index: index2, children: (dp, snap) => /* @__PURE__ */ jsxRuntimeExports.jsx(
49841
+ ProjectItem,
49842
+ {
49843
+ project,
49844
+ workspaceId: item.workspaceId,
49845
+ dp,
49846
+ snap,
49847
+ isActive: isActive2,
49848
+ indent: indent2,
49849
+ onSwitch,
49850
+ onRemove
49851
+ }
49852
+ ) }, `p:${item.workspaceId}`);
49853
+ }),
49854
+ provided.placeholder
49855
+ ] }) }) });
49762
49856
  });
49857
+ function CardDetailHeader({
49858
+ card,
49859
+ workspaceId,
49860
+ projectName,
49861
+ externalUrl,
49862
+ isStory,
49863
+ isReadyForReview,
49864
+ hasStartCommand = false,
49865
+ merging,
49866
+ onMerge,
49867
+ onPR,
49868
+ onDelete,
49869
+ onClose
49870
+ }) {
49871
+ var _a3, _b2, _c3;
49872
+ const { session: runSession, start: startRun, stop: stopRun } = useRunSession(workspaceId);
49873
+ 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: [
49874
+ /* @__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 }) }),
49875
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-[18px] bg-[#2a2a35] shrink-0" }),
49876
+ projectName && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
49877
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-[#60607a]", children: projectName }),
49878
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-[#2a2a35]", children: "/" })
49879
+ ] }),
49880
+ /* @__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 }),
49881
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1" }),
49882
+ externalUrl && !((_b2 = card.pr) == null ? void 0 : _b2.url) && /* @__PURE__ */ jsxRuntimeExports.jsx(
49883
+ "a",
49884
+ {
49885
+ href: externalUrl,
49886
+ target: "_blank",
49887
+ rel: "noreferrer",
49888
+ className: "text-[#60607a] hover:text-gray-300 transition-colors",
49889
+ title: "Open external link",
49890
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { size: 15 })
49891
+ }
49892
+ ),
49893
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-[18px] bg-[#2a2a35] shrink-0" }),
49894
+ 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(
49895
+ "button",
49896
+ {
49897
+ onClick: () => void stopRun(),
49898
+ className: "cursor-pointer text-[#60607a] hover:text-red-400 transition-colors",
49899
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Square, { size: 15, className: "fill-current" })
49900
+ }
49901
+ ) }) : /* @__PURE__ */ jsxRuntimeExports.jsx(
49902
+ Tooltip_default,
49903
+ {
49904
+ delayDuration: 0,
49905
+ content: runSession.status === "running" ? "Another task is running" : "Run",
49906
+ side: "bottom",
49907
+ triggerAsChild: true,
49908
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49909
+ "button",
49910
+ {
49911
+ onClick: () => void startRun(card.id),
49912
+ disabled: runSession.status === "running",
49913
+ className: "cursor-pointer text-[#60607a] hover:text-emerald-400 transition-colors disabled:opacity-40 disabled:cursor-not-allowed",
49914
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 15 })
49915
+ }
49916
+ )
49917
+ }
49918
+ ) }),
49919
+ !isStory && isReadyForReview && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
49920
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
49921
+ Tooltip_default,
49922
+ {
49923
+ delayDuration: 0,
49924
+ content: merging ? "Merging..." : `Merge into ${card.baseRef}`,
49925
+ side: "bottom",
49926
+ triggerAsChild: true,
49927
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49928
+ "button",
49929
+ {
49930
+ onClick: onMerge,
49931
+ disabled: merging,
49932
+ className: "cursor-pointer text-[#60607a] hover:text-emerald-400 transition-colors disabled:opacity-40",
49933
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(GitMerge, { size: 15 })
49934
+ }
49935
+ )
49936
+ }
49937
+ ),
49938
+ ((_c3 = card.pr) == null ? void 0 : _c3.url) ? /* @__PURE__ */ jsxRuntimeExports.jsx(
49939
+ "a",
49940
+ {
49941
+ href: card.pr.url,
49942
+ target: "_blank",
49943
+ rel: "noreferrer",
49944
+ title: "Open Pull Request",
49945
+ className: "cursor-pointer text-green-400 hover:text-green-300 transition-colors",
49946
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(GitPullRequest, { size: 15 })
49947
+ }
49948
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip_default, { delayDuration: 0, content: `Create PR against ${card.baseRef}`, side: "bottom", triggerAsChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49949
+ "button",
49950
+ {
49951
+ onClick: onPR,
49952
+ disabled: merging,
49953
+ className: "cursor-pointer text-[#60607a] hover:text-green-400 transition-colors disabled:opacity-40",
49954
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(GitPullRequest, { size: 15 })
49955
+ }
49956
+ ) })
49957
+ ] }),
49958
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-[18px] bg-[#2a2a35] shrink-0" }),
49959
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip_default, { delayDuration: 0, content: "Delete task", side: "bottom", triggerAsChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
49960
+ "button",
49961
+ {
49962
+ onClick: onDelete,
49963
+ className: "cursor-pointer text-[#60607a] hover:text-red-400 transition-colors",
49964
+ title: "Delete task",
49965
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 15 })
49966
+ }
49967
+ ) })
49968
+ ] });
49969
+ }
49763
49970
  const COLUMN_LABELS = {
49764
49971
  todo: "Todo",
49765
49972
  in_progress: "In Progress",
@@ -73705,19 +73912,6 @@ function CommitMsgDialog({
73705
73912
  ] })
73706
73913
  ] });
73707
73914
  }
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
73915
  function CreatePRDialog({
73722
73916
  dismiss: dismiss2,
73723
73917
  workspaceId,
@@ -74358,90 +74552,6 @@ function cardToFormModelConfig(cfg) {
74358
74552
  }
74359
74553
  return out;
74360
74554
  }
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
74555
  function ModelTiersDialog({
74446
74556
  pairs,
74447
74557
  defaultBinary,
@@ -74564,13 +74674,12 @@ function TierRow({
74564
74674
  }
74565
74675
  ),
74566
74676
  /* @__PURE__ */ jsxRuntimeExports.jsx(
74567
- Select_default,
74677
+ AgentBinarySelect,
74568
74678
  {
74569
74679
  floatingStrategy: "fixed",
74570
74680
  value: pair.binary,
74571
74681
  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))
74682
+ menuClassName: "w-fit"
74574
74683
  }
74575
74684
  ),
74576
74685
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -79344,7 +79453,7 @@ function TagInput({
79344
79453
  }) {
79345
79454
  const [draft, setDraft] = reactExports.useState("");
79346
79455
  const add2 = (raw2) => {
79347
- const tag = normalizeTag(raw2);
79456
+ const tag = normalizeTag$1(raw2);
79348
79457
  if (!tag || value.includes(tag)) return;
79349
79458
  onChange([...value, tag]);
79350
79459
  setDraft("");
@@ -79764,49 +79873,6 @@ function MemorySection({ workspaceId }) {
79764
79873
  ] })
79765
79874
  ] });
79766
79875
  }
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
79876
  function SaveRow({ saving, onSave }) {
79811
79877
  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
79878
  }