@galda/cli 0.10.55 → 0.10.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/index.html CHANGED
@@ -6890,14 +6890,12 @@ function renderFsTodo(){
6890
6890
  // 一時停止したゴール(blocked kind:'paused')は Doing に「Paused」1行で留める(Masa 2026-07-19)。
6891
6891
  // Needs you 側(failed/blocked)へは出さず、その走行/中断タスク行も抑止して二重行を作らない。
6892
6892
  const pausedGoalIds = new Set(pgoals.filter((g) => g.status === 'blocked' && g.blocked?.kind === 'paused').map((g) => g.id));
6893
- // A goal picked back up by a "Needs you" reply is `running` but may have no running WORK task
6894
- // (only a reply task, which Doing excludes) it used to vanish from the lane (Masa 2026-07-24).
6895
- // Show it in Doing as a goal row, and stop its old stopped task from lingering under "Needs you".
6893
+ // A reply is a real running task and already renders in Doing. Keep its goal id
6894
+ // only to hide the old failed row; rendering a second goal row duplicated the
6895
+ // same card whenever feedback was being applied.
6896
6896
  const runningTaskGoalIds = new Set(running.filter((t) => !t.reply).map((t) => t.goalId));
6897
6897
  const runningReplyGoalIds = new Set(list.filter((t) => t.reply && t.status === 'running').map((t) => t.goalId));
6898
6898
  const resumedGoalIds = new Set(runningReplyGoalIds);
6899
- const doingGoals = pgoals.filter((g) => g.status === 'running' && g.startedAt
6900
- && runningReplyGoalIds.has(g.id) && !runningTaskGoalIds.has(g.id) && !pausedGoalIds.has(g.id));
6901
6899
  // A reply that was OVER the parallel cap (engine PR #342): the goal is 'running'
6902
6900
  // but startedAt is cleared, and its only pending work is a QUEUED reply task —
6903
6901
  // which Doing needs startedAt for, and Next-up excludes (!t.reply). With no
@@ -6926,13 +6924,14 @@ function renderFsTodo(){
6926
6924
  // out to its own "Needs you" caption below (Masa 2026-07-19) — folding it into Doing read
6927
6925
  // as "still in flight".
6928
6926
  const doingRows =
6929
- running.filter((t) => !pausedGoalIds.has(t.goalId)).map((t) => `<div class="drow"><div class="trow now"><span class="st run"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span><span class="pct num">${progressFor(t)}%</span>${fsActs(t.goalId)}</div>
6930
- <div class="tbar"><i style="width:${progressFor(t)}%"></i></div></div>`).join('')
6927
+ running.filter((t) => !pausedGoalIds.has(t.goalId)).map((t) => {
6928
+ const goal = pgoals.find((g) => g.id === t.goalId);
6929
+ const title = t.reply ? (goal?.reviewSummary?.check || goal?.plan?.[0] || goal?.text || t.title) : t.title;
6930
+ return `<div class="drow"><div class="trow now"><span class="st run"></span>${noSpan(t.goalId)}<span class="tt">${tt(title)}</span><span class="pct num">${progressFor(t)}%</span>${fsActs(t.goalId)}</div>
6931
+ <div class="tbar"><i style="width:${progressFor(t)}%"></i></div></div>`;
6932
+ }).join('')
6931
6933
  // 一時停止したゴールは Doing に「Paused」1行(アンバー)で留める+Resume。走行/中断行は上と needsRows で抑止済み。
6932
- + pgoals.filter((g) => pausedGoalIds.has(g.id)).map((g) => `<div class="drow"><div class="trow now"><span class="st run int"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword int">Paused</span><button class="ab txt" data-fspause="1" title="Resume — 再開">Resume</button></div></div>`).join('')
6933
- // A goal picked back up by a reply (running, no running work task yet): show it here so it
6934
- // lands in Doing instead of vanishing. No % bar — nothing measurable is running yet.
6935
- + doingGoals.map((g) => `<div class="drow"><div class="trow now"><span class="st run"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword" style="color:var(--green)">Working on it</span>${fsActs(g.id)}</div></div>`).join('');
6934
+ + pgoals.filter((g) => pausedGoalIds.has(g.id)).map((g) => `<div class="drow"><div class="trow now"><span class="st run int"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword int">Paused</span><button class="ab txt" data-fspause="1" title="Resume — 再開">Resume</button></div></div>`).join('');
6936
6935
  // NEEDS YOU: stopped/errored work that used to fold into Doing, now under its own caption so
6937
6936
  // it no longer reads as "still in flight" (Masa 2026-07-19). Three kinds share it, each
6938
6937
  // keeping the affordances it already had — nothing about the failed-task and blocked-goal
@@ -6998,7 +6997,7 @@ function renderFsTodo(){
6998
6997
  // queued.length specifically, since that suffix describes the Next-up sublist's own sort,
6999
6998
  // not the lane total.
7000
6999
  const needsCount = failed.filter((t) => !resumedGoalIds.has(t.goalId)).length + blocked.length + needsInput.length;
7001
- const todoTotal = running.length + doingGoals.length + parkedReplyGoals.length + needsCount + queued.length + later.length;
7000
+ const todoTotal = running.length + parkedReplyGoals.length + needsCount + queued.length + later.length;
7002
7001
  // Free-tier pending counter (H22): on the free plan the header shows the wall
7003
7002
  // — pending goals out of the 5-slot cap — reddening at the cap and gone once
7004
7003
  // paid. Reads the SAME numbers the gate enforces (state.billing, never a local
@@ -10055,6 +10054,12 @@ $('appmenu').addEventListener('click', (e) => {
10055
10054
  renderEffortSelect();
10056
10055
  renderMsgHint();
10057
10056
  $('appmenu').classList.remove('show');
10057
+ const projectId = state.active;
10058
+ if (projectId) fetch(withKey(`/api/projects/${encodeURIComponent(projectId)}/queue-agent`), {
10059
+ method: 'POST', headers: { 'content-type': 'application/json' },
10060
+ body: JSON.stringify({ agent: state.connectedApp, model: state.stickyModel, effort: state.stickyEffort }),
10061
+ }).then(async (r) => { if (!r.ok) showErr((await r.json().catch(() => ({}))).error || 'Could not update Next up.'); else await refresh(); })
10062
+ .catch(() => showErr('Could not update Next up.'));
10058
10063
  });
10059
10064
  document.addEventListener('click', (e) => {
10060
10065
  if (!e.target.closest('.appsel')) $('appmenu').classList.remove('show');
package/engine/server.mjs CHANGED
@@ -4008,6 +4008,40 @@ const server = createServer(async (req, res) => {
4008
4008
  return json(res, 200, task);
4009
4009
  }
4010
4010
 
4011
+ // The composer agent picker also owns Next up for this project. Only work
4012
+ // that has not started moves; running/review/done work keeps its original
4013
+ // agent and conversation.
4014
+ const queueAgentMatch = url.pathname.match(/^\/api\/projects\/([^/]+)\/queue-agent$/);
4015
+ if (queueAgentMatch && req.method === 'POST') {
4016
+ const projectId = decodeURIComponent(queueAgentMatch[1]);
4017
+ if (!projects.some((project) => project.id === projectId)) return json(res, 404, { error: 'project not found' });
4018
+ let body = '';
4019
+ req.on('data', (chunk) => { body += chunk; });
4020
+ req.on('end', () => {
4021
+ let requested;
4022
+ try { requested = JSON.parse(body || '{}'); } catch { return json(res, 400, { error: 'bad json' }); }
4023
+ if (!['claude-code', 'codex'].includes(requested.agent)) return json(res, 400, { error: 'unsupported agent' });
4024
+ const agent = workerAgent(requested.agent);
4025
+ if (!AVAILABLE_AGENTS.includes(agent)) return json(res, 409, { error: `${agent} is not connected on this device` });
4026
+ const model = workerModel(agent, requested.model);
4027
+ const effort = workerEffort(agent, requested.effort);
4028
+ const runningGoalIds = new Set(tasks.filter((task) => task.projectId === projectId && task.status === 'running').map((task) => task.goalId));
4029
+ const queued = tasks.filter((task) => task.projectId === projectId && task.status === 'queued' && !runningGoalIds.has(task.goalId));
4030
+ const goalIds = new Set(queued.map((task) => task.goalId));
4031
+ for (const task of queued) {
4032
+ task.agent = agent; task.model = model; task.effort = effort; task.sessionId = null;
4033
+ saveTask(task);
4034
+ }
4035
+ for (const goal of goals.filter((item) => goalIds.has(item.id))) {
4036
+ goal.agent = agent; goal.model = model; goal.effort = effort; goal.sessionId = null; goal.startedAt = null;
4037
+ saveGoal(goal);
4038
+ }
4039
+ pump(projectId);
4040
+ return json(res, 200, { changed: queued.length, agent, model, effort });
4041
+ });
4042
+ return;
4043
+ }
4044
+
4011
4045
  // A Claude usage hold should be actionable without asking people to edit
4012
4046
  // models or sessions. Move every still-queued step for this goal together so
4013
4047
  // one tap cannot leave a mixed Claude/Codex chain behind.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.55",
3
+ "version": "0.10.57",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {