@galda/cli 0.10.49 → 0.10.50
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 +75 -19
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -472,7 +472,17 @@
|
|
|
472
472
|
scoped here, since every other .qsub stays a one-liner. */
|
|
473
473
|
.replynote{white-space:normal;overflow:visible;margin-top:14px;padding-top:12px;border-top:1px solid var(--hair)}
|
|
474
474
|
.replynote .qchips{margin-top:6px}
|
|
475
|
-
.
|
|
475
|
+
/* The .txr You/Claude rows are styled by `.redesign .txr` — which the See-all card
|
|
476
|
+
(.sa-item.redesign) provides, but the goal-detail overlay and review dialog do NOT.
|
|
477
|
+
There the same note rendered as unstyled run-in black text ("Youこんにちは" with no
|
|
478
|
+
gap/weight — Masa 2026-07-24 "弾"). Scope the row layout to .replynote too so the
|
|
479
|
+
reply thread reads the same wherever its box lives. */
|
|
480
|
+
.replynote .txr{display:flex;gap:12px;padding:5px 0;font-size:13px;line-height:1.55;align-items:baseline}
|
|
481
|
+
.replynote .txr .w{flex:0 0 46px;font-size:11px;color:var(--ink3)}
|
|
482
|
+
.replynote .txr.you .w,.replynote .txr.cl .w{color:var(--ink);font-weight:600}
|
|
483
|
+
.replynote .txr .m{flex:1;min-width:0;color:var(--ink2)}
|
|
484
|
+
.replynote .txr.cl .m{color:var(--ink)}
|
|
485
|
+
.replynote .txr.note .m{font-family:var(--mono);font-size:12px;color:var(--ink3)}
|
|
476
486
|
.ob-unsent{color:rgb(var(--danger-rgb));cursor:pointer}
|
|
477
487
|
[data-oresend]{cursor:pointer}
|
|
478
488
|
.chip2{font-family:var(--mono);font-size:9.5px;letter-spacing:.05em;padding:2px 7px;border-radius:6px;border:1px solid var(--hair2);color:var(--ink3);flex:0 0 auto}
|
|
@@ -2283,7 +2293,7 @@
|
|
|
2283
2293
|
#fsRoot .trow .st{width:13px;height:13px;border-radius:50%;border:1.5px solid var(--hair2);flex:0 0 13px;position:relative;top:1px} /* top:1px = optical nudge so the whole status column (To Do/Doing/Failed/Done) centres on the row text, not the line-box (Masa 2026-07-24, board=C) */
|
|
2284
2294
|
/* Doing marker = a broken green ring that spins (pattern B · flagship #316). Running only —
|
|
2285
2295
|
.bad (failed) / .int (paused) stay static: no dead motion (DESIGN-RULES §5.5). Reuses @keyframes spin. */
|
|
2286
|
-
#fsRoot .trow .st.run{border-color:var(--green);border-top-color:transparent;background:none;box-shadow:0 0 6px color-mix(in srgb,var(--green) 40%,transparent);animation:spin 1.8s linear infinite}
|
|
2296
|
+
#fsRoot .trow .st.run{border-color:var(--green);border-top-color:transparent;background:none;box-shadow:0 0 6px color-mix(in srgb,var(--green) 40%,transparent);animation:spin 1.8s linear infinite;animation-delay:var(--spd,0ms)} /* --spd = wall-clock phase (set in render()) so a re-created board marker doesn't restart the spin at 0deg */
|
|
2287
2297
|
#fsRoot .trow .st.run.bad{border-color:var(--danger);background:radial-gradient(circle,rgba(var(--danger-rgb),.42),transparent 70%);animation:none;box-shadow:none}
|
|
2288
2298
|
#fsRoot .trow .st.run.int{border-color:var(--amber);background:radial-gradient(circle,rgba(var(--amber-rgb),.4),transparent 70%);animation:none;box-shadow:none}
|
|
2289
2299
|
@media (prefers-reduced-motion:reduce){#fsRoot .trow .st.run{animation:none}}
|
|
@@ -6195,6 +6205,11 @@ function renderFlagship(){
|
|
|
6195
6205
|
if (state.layout !== 'flagship') return;
|
|
6196
6206
|
const root = $('fsRoot'); if (!root) return;
|
|
6197
6207
|
root.dataset.rv = String(fsUI.rv);
|
|
6208
|
+
// Wall-clock phase for the spinning markers: any ring re-created this render (the board
|
|
6209
|
+
// re-renders often on progress %) starts at the angle a continuous one would be at now,
|
|
6210
|
+
// instead of snapping back to 0deg — no "jump". The rail ring is guarded separately and
|
|
6211
|
+
// doesn't read --spd, so changing it here can't disturb the persistent rail ring. (Masa 2026-07-24)
|
|
6212
|
+
root.style.setProperty('--spd', (-(Date.now() % 1800)) + 'ms');
|
|
6198
6213
|
renderFsRail(); renderFsFeed(); renderFsLane();
|
|
6199
6214
|
}
|
|
6200
6215
|
function renderFsRail(){
|
|
@@ -6269,6 +6284,14 @@ function fsActiveContext(){
|
|
|
6269
6284
|
// Per-project last non-empty center feed — the whiteout guard (see below): keeps
|
|
6270
6285
|
// what the user was looking at when a render pass momentarily produces no lines.
|
|
6271
6286
|
const _fsLastFeed = {};
|
|
6287
|
+
// H-CENTERLANE decision 1: pure merge-sort of timed HTML blocks (the focused task's
|
|
6288
|
+
// activity block + each chat turn) so the center lane renders in the order things
|
|
6289
|
+
// actually happened. Stable sort (spec-guaranteed since ES2019) keeps same-ts blocks
|
|
6290
|
+
// in their original relative order. Pure/no DOM — covered directly in
|
|
6291
|
+
// engine/test/center-lane-timeline.test.mjs.
|
|
6292
|
+
function fsMergeTimelineBlocks(blocks){
|
|
6293
|
+
return [...blocks].sort((a, b) => a.ts - b.ts).map((b) => b.html).join('');
|
|
6294
|
+
}
|
|
6272
6295
|
function renderFsFeed(){
|
|
6273
6296
|
if (state.layout !== 'flagship') return;
|
|
6274
6297
|
const feed = $('fsFeed'); if (!feed) return;
|
|
@@ -6313,7 +6336,12 @@ function renderFsFeed(){
|
|
|
6313
6336
|
} else { nd.hidden = true; nd.innerHTML = ''; }
|
|
6314
6337
|
}
|
|
6315
6338
|
let html = '';
|
|
6339
|
+
// H-CENTERLANE decision 1: the focused task's activity block is anchored at its
|
|
6340
|
+
// startedAt so it can be merge-sorted against chat turns below (state.act carries
|
|
6341
|
+
// no per-line timestamp, so the whole block — not each line — is the sort unit).
|
|
6342
|
+
let focusTs = 0;
|
|
6316
6343
|
if (focus) {
|
|
6344
|
+
focusTs = focus.startedAt ? Date.parse(focus.startedAt) : 0;
|
|
6317
6345
|
const lines = state.act[focus.id] ?? [];
|
|
6318
6346
|
const lineLimit = fsUI.logOpen.has(focus.id) || focus.status === 'running' || ['running', 'partial'].includes(goal?.status) ? 1000 : 80;
|
|
6319
6347
|
html += lines.slice(-lineLimit).map(fsFeedRow).join('');
|
|
@@ -6380,29 +6408,39 @@ function renderFsFeed(){
|
|
|
6380
6408
|
// whole-feed re-render (SSE act events) can't drop them. Order per turn:
|
|
6381
6409
|
// You → (mark, if it became a task) and, for a reply, speaker line → reply.
|
|
6382
6410
|
const turns = (state.chatTurns[state.active] || []).slice(-40);
|
|
6383
|
-
|
|
6411
|
+
// H-CENTERLANE decision 1: each turn becomes its own timed block (ts, html) so it
|
|
6412
|
+
// can be merge-sorted against the focused task's activity block below, instead of
|
|
6413
|
+
// always rendering as one chat block stapled after the task feed regardless of
|
|
6414
|
+
// when either actually happened.
|
|
6415
|
+
const chatBlocks = [];
|
|
6384
6416
|
for (const turn of turns) {
|
|
6417
|
+
let t = '';
|
|
6385
6418
|
if (turn.role === 'you') {
|
|
6386
|
-
|
|
6419
|
+
t += `<div class="cc-you"><span class="cy-n">You</span><div class="cy-t">${esc(turn.text)}</div></div>`;
|
|
6387
6420
|
if (turn.markGoalId != null && !turn.undone) {
|
|
6388
6421
|
const mg = state.goals.find((x) => x.id === turn.markGoalId);
|
|
6389
6422
|
const n = mg ? goalReviewNumber({ goal: mg, tasks: state.tasks }) : turn.markGoalId;
|
|
6390
|
-
|
|
6423
|
+
t += `<div class="cc-mark"><span class="cm-ar">→</span><span class="cm-t">Added to To Do</span><span class="cm-no">#${n}</span><button class="cm-undo" type="button" data-ccundo="${turn.markGoalId}">Undo</button></div>`;
|
|
6391
6424
|
}
|
|
6392
6425
|
} else { // Claude Code reply — speaker line first (You → speaker → reply), then the text
|
|
6393
6426
|
const app = connectedAppMeta(turn.agent);
|
|
6394
|
-
|
|
6427
|
+
t += `<div class="cc-who">${app.icon}<span class="cs-n">${esc(app.label)}</span></div>`;
|
|
6395
6428
|
const shown = esc((turn.text || '').slice(0, turn.revealed || 0));
|
|
6396
|
-
|
|
6429
|
+
t += turn.done
|
|
6397
6430
|
? `<div class="cc-say">${shown}</div>`
|
|
6398
6431
|
: `<div class="cc-say"><span id="ccTyping">${shown}</span><span class="cc-caret"></span></div>`;
|
|
6399
6432
|
}
|
|
6433
|
+
chatBlocks.push({ ts: turn.ts ?? 0, html: t });
|
|
6400
6434
|
}
|
|
6401
6435
|
const pgoals = state.goals.filter((g) => g.projectId === state.active);
|
|
6402
6436
|
const ptasks = state.tasks.filter((t) => t.projectId === state.active);
|
|
6403
6437
|
const hasWork = pgoals.length > 0 || ptasks.length > 0 || turns.length > 0;
|
|
6404
|
-
//
|
|
6405
|
-
|
|
6438
|
+
// H-CENTERLANE decision 1: interleave the focused task's activity block with the
|
|
6439
|
+
// chat turns in the order they actually happened (stable sort keeps same-ts items
|
|
6440
|
+
// in their original relative order); the question card(s) stay pinned at the very
|
|
6441
|
+
// end (H18/H19 — a pending question must never be lost mid-scroll).
|
|
6442
|
+
const timelineBlocks = html ? [{ ts: focusTs, html }, ...chatBlocks] : chatBlocks;
|
|
6443
|
+
let out = fsMergeTimelineBlocks(timelineBlocks) + askHtml;
|
|
6406
6444
|
if (out) _fsLastFeed[state.active] = out;
|
|
6407
6445
|
else if (hasWork) out = _fsLastFeed[state.active] || `<div class="cc-c">⎿ ${esc(running.length ? 'working…' : 'no active task')}</div>`;
|
|
6408
6446
|
$('fsRoot').classList.toggle('clawdhero', !hasWork);
|
|
@@ -6819,7 +6857,10 @@ function fsBucketTodo(){
|
|
|
6819
6857
|
needsInput: pgoals.filter((g) => g.status === 'needsInput'), // a clarifying question is waiting on the user (board's "Needs you")
|
|
6820
6858
|
reviewGoals: pgoals.filter((g) => g.status === 'review'),
|
|
6821
6859
|
queued: list.filter((t) => t.status === 'queued' && !t.reply).sort(byQueue),
|
|
6822
|
-
|
|
6860
|
+
// A reply task's completion is already visible inline in the center-lane timeline
|
|
6861
|
+
// (H-CENTERLANE decision 3) — showing it again as an independent Done row is the
|
|
6862
|
+
// same event twice. `queued` already guards `!t.reply` above; mirror it here.
|
|
6863
|
+
doneAll: list.filter((t) => ['done', 'skipped'].includes(t.status) && !closedGoalIds.has(t.goalId) && !t.reply).sort((a, b) => b.num - a.num),
|
|
6823
6864
|
later: pgoals.filter((g) => g.status === 'pending'),
|
|
6824
6865
|
rejected: pgoals.filter((g) => g.status === 'rejected'), // 07-20c: Reject parks goals here (own fold, like Done)
|
|
6825
6866
|
};
|
|
@@ -6946,7 +6987,7 @@ function renderFsTodo(){
|
|
|
6946
6987
|
<button class="qadd" data-fsnew="1" title="New goal"><span class="paplus"></span>New</button>
|
|
6947
6988
|
</div>
|
|
6948
6989
|
${doneAll.length ? `<div class="doneline${fsUI.doneOpen ? ' open' : ''}" id="fsDoneLine">✓ Done<span class="num">${doneAll.length}</span><span class="fold">▾</span></div>
|
|
6949
|
-
<div id="fsDonelist"${fsUI.doneOpen ? '' : ' hidden'}>${doneAll.
|
|
6990
|
+
<div id="fsDonelist"${fsUI.doneOpen ? '' : ' hidden'}>${doneAll.map((t) => `<div class="trow up dim done"><span class="st ok"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span></div>`).join('')}</div>` : ''}`;
|
|
6950
6991
|
el.classList.toggle('folded', !!fsUI.todoFold);
|
|
6951
6992
|
$('fsTodoHd').onclick = () => { fsUI.todoFold = !fsUI.todoFold; el.classList.toggle('folded', fsUI.todoFold); };
|
|
6952
6993
|
const qadd = el.querySelector('.qadd[data-fsnew]'); // + New → focus the composer (the real create path)
|
|
@@ -8218,7 +8259,7 @@ async function postOutboxItem(item){
|
|
|
8218
8259
|
// .peraanswer bar. Never land()/upsert(): a kind:'chat' response has no id.
|
|
8219
8260
|
outbox.remove(item.oid);
|
|
8220
8261
|
if (state.layout === 'flagship') {
|
|
8221
|
-
const turn = { role: 'clawd', text: String(goal.reply || ''), revealed: 0, done: false, agent: item.agent };
|
|
8262
|
+
const turn = { role: 'clawd', text: String(goal.reply || ''), revealed: 0, done: false, agent: item.agent, ts: Date.now() };
|
|
8222
8263
|
(state.chatTurns[item.projectId] ||= []).push(turn);
|
|
8223
8264
|
renderFsFeed();
|
|
8224
8265
|
typeChatTurn(turn);
|
|
@@ -9995,7 +10036,7 @@ async function argItems(cmd){
|
|
|
9995
10036
|
// Read the raw input → decide what the palette shows (null = closed).
|
|
9996
10037
|
function paletteState(val){
|
|
9997
10038
|
if (val[0] !== '/') return null;
|
|
9998
|
-
const m = val.match(/^\/(\w*)(\s)?([\s\S]*)$/);
|
|
10039
|
+
const m = val.match(/^\/([\w-]*)(\s)?([\s\S]*)$/); // allow '-' so hyphenated skill names filter (english-practice)
|
|
9999
10040
|
if (!m) return null;
|
|
10000
10041
|
const word = m[1], hadSpace = m[2] != null, rest = m[3];
|
|
10001
10042
|
if (!hadSpace) return { level: 'top', query: word }; // still typing the command name
|
|
@@ -10018,11 +10059,18 @@ function renderPalette(){
|
|
|
10018
10059
|
if (!p || !el) return;
|
|
10019
10060
|
const head = p.level === 'top' ? 'Commands' : '/' + p.cmd;
|
|
10020
10061
|
if (!p.items.length) { el.innerHTML = `<div class="sphead">${esc(head)}</div><div class="empty">No match</div>`; el.classList.add('show'); return; }
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10062
|
+
let html = '', lastGroup = null;
|
|
10063
|
+
p.items.forEach((it, i) => {
|
|
10064
|
+
// group headers: top-level splits Commands vs Skills; arg-level shows the "/cmd" head once.
|
|
10065
|
+
if (p.level === 'top') {
|
|
10066
|
+
const group = it.skill ? 'Skills' : 'Commands';
|
|
10067
|
+
if (group !== lastGroup) { html += `<div class="sphead">${group}</div>`; lastGroup = group; }
|
|
10068
|
+
} else if (i === 0) { html += `<div class="sphead">${esc('/' + p.cmd)}</div>`; }
|
|
10069
|
+
const name = p.level === 'top' ? (it.skill ? '/' + it.skill : '/' + it.cmd + (it.arg ? ' …' : '')) : it.v;
|
|
10070
|
+
const desc = p.level === 'top' ? (it.desc || '') : (it.d || '');
|
|
10071
|
+
html += `<button type="button" class="spopt${i === p.sel ? ' on' : ''}" data-i="${i}" role="option"><span class="spn">${esc(name)}</span><span class="spd">${esc(desc)}</span></button>`;
|
|
10072
|
+
});
|
|
10073
|
+
el.innerHTML = html;
|
|
10026
10074
|
el.classList.add('show');
|
|
10027
10075
|
for (const b of el.querySelectorAll('[data-i]')) b.onmousedown = (e) => { e.preventDefault(); state.palette.sel = Number(b.dataset.i); acceptPalette(); };
|
|
10028
10076
|
}
|
|
@@ -10034,6 +10082,13 @@ async function refreshPalette(){
|
|
|
10034
10082
|
if (info.level === 'top') {
|
|
10035
10083
|
const q = info.query.toLowerCase();
|
|
10036
10084
|
items = SLASH_CMDS.filter((s) => !q || s.cmd.startsWith(q));
|
|
10085
|
+
// Once the user types after "/", also offer their registered skills (type-to-filter, so a
|
|
10086
|
+
// bare "/" stays a short command list). Selecting one runs the goal with that skill first.
|
|
10087
|
+
if (q) {
|
|
10088
|
+
const skills = (await loadSkills()).filter((s) => s.name.toLowerCase().includes(q));
|
|
10089
|
+
if ($('input').value !== val) return; // input changed while awaiting skills
|
|
10090
|
+
items = items.concat(skills.map((s) => ({ skill: s.name, desc: `${s.kind || 'skill'}${s.description ? ' · ' + s.description : ''}`.trim() })));
|
|
10091
|
+
}
|
|
10037
10092
|
} else {
|
|
10038
10093
|
const all = await argItems(info.cmd);
|
|
10039
10094
|
if ($('input').value !== val) return; // input changed while awaiting skills
|
|
@@ -10054,7 +10109,8 @@ function acceptPalette(){
|
|
|
10054
10109
|
const p = state.palette; if (!p) return false;
|
|
10055
10110
|
const item = p.items[p.sel]; if (!item) return true;
|
|
10056
10111
|
if (p.level === 'top') {
|
|
10057
|
-
if (item.
|
|
10112
|
+
if (item.skill) { applySlash('skill', item.skill); $('input').value = $('input').value.replace(/^\s*\/\S*\s*/, ''); closePalette(); } // hyphen-safe strip of the "/skillname" token
|
|
10113
|
+
else if (item.arg) { $('input').value = '/' + item.cmd + ' '; refreshPalette(); }
|
|
10058
10114
|
else { applySlash(item.cmd, null); $('input').value = stripLeadingCmd($('input').value); closePalette(); }
|
|
10059
10115
|
} else {
|
|
10060
10116
|
applySlash(p.cmd, item.v);
|
package/package.json
CHANGED