@luckydraw/cumulus 1.0.38 → 1.0.40

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/gateway/adapters/webchat.d.ts +3 -0
  3. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  4. package/dist/gateway/adapters/webchat.js +84 -1
  5. package/dist/gateway/adapters/webchat.js.map +1 -1
  6. package/dist/gateway/config.d.ts +16 -2
  7. package/dist/gateway/config.d.ts.map +1 -1
  8. package/dist/gateway/config.js +12 -6
  9. package/dist/gateway/config.js.map +1 -1
  10. package/dist/gateway/daemon.d.ts +1 -0
  11. package/dist/gateway/daemon.d.ts.map +1 -1
  12. package/dist/gateway/daemon.js +4 -0
  13. package/dist/gateway/daemon.js.map +1 -1
  14. package/dist/gateway/one-shot-model.d.ts +92 -0
  15. package/dist/gateway/one-shot-model.d.ts.map +1 -0
  16. package/dist/gateway/one-shot-model.js +191 -0
  17. package/dist/gateway/one-shot-model.js.map +1 -0
  18. package/dist/gateway/server.d.ts +3 -0
  19. package/dist/gateway/server.d.ts.map +1 -1
  20. package/dist/gateway/server.js +4 -0
  21. package/dist/gateway/server.js.map +1 -1
  22. package/dist/gateway/stall-detection.d.ts +19 -50
  23. package/dist/gateway/stall-detection.d.ts.map +1 -1
  24. package/dist/gateway/stall-detection.js +27 -190
  25. package/dist/gateway/stall-detection.js.map +1 -1
  26. package/dist/gateway/static/widget.js +342 -35
  27. package/dist/gateway/tasks-refresh.d.ts +89 -0
  28. package/dist/gateway/tasks-refresh.d.ts.map +1 -0
  29. package/dist/gateway/tasks-refresh.js +224 -0
  30. package/dist/gateway/tasks-refresh.js.map +1 -0
  31. package/dist/gateway/tasks.d.ts +21 -1
  32. package/dist/gateway/tasks.d.ts.map +1 -1
  33. package/dist/gateway/tasks.js +62 -10
  34. package/dist/gateway/tasks.js.map +1 -1
  35. package/dist/gateway/worker.d.ts +86 -0
  36. package/dist/gateway/worker.d.ts.map +1 -0
  37. package/dist/gateway/worker.js +0 -0
  38. package/dist/gateway/worker.js.map +1 -0
  39. package/dist/lib/tasks-file.d.ts +23 -0
  40. package/dist/lib/tasks-file.d.ts.map +1 -1
  41. package/dist/lib/tasks-file.js +49 -0
  42. package/dist/lib/tasks-file.js.map +1 -1
  43. package/dist/lib/tasks-format.d.ts.map +1 -1
  44. package/dist/lib/tasks-format.js +12 -5
  45. package/dist/lib/tasks-format.js.map +1 -1
  46. package/package.json +1 -1
@@ -144,6 +144,9 @@
144
144
  // "haiku" for an unmatched id anyway.
145
145
  stallClassifierModel:
146
146
  typeof config.stallClassifierModel === 'string' ? config.stallClassifierModel : '',
147
+ // Task 179. Same verbatim rule again; the gateway's resolveGatewayModel falls
148
+ // back to "haiku" for an unmatched id.
149
+ workerModel: typeof config.workerModel === 'string' ? config.workerModel : '',
147
150
  };
148
151
  }
149
152
 
@@ -208,6 +211,9 @@
208
211
  // Task 164: '' means "haiku" and must reach the server as an explicit null (clear),
209
212
  // not as absent — absent would leave a previously-set classifier model untouched.
210
213
  stallClassifierModel: state.stallClassifierModel ? state.stallClassifierModel : null,
214
+ // Task 179: same explicit-null rule — '' means "haiku", and absent would
215
+ // leave a previously-set worker model untouched.
216
+ workerModel: state.workerModel ? state.workerModel : null,
211
217
  // Registry travels without credentials — addCredentialPatch fills those
212
218
  // in, so an untouched key round-trips as absent and the server preserves it.
213
219
  customProviders: (Array.isArray(state.customProviders) ? state.customProviders : []).map(
@@ -1975,17 +1981,55 @@
1975
1981
  '}',
1976
1982
  '.cumulus-task-dep button:hover { border-color: #666; color: #eee; }',
1977
1983
 
1978
- /* Selection bar same shape as the message selection bar */
1984
+ /* Toolbartask 178 put the selection actions above the list; task 179 gave
1985
+ the bar something to show at zero selected, so it is now always present. */
1979
1986
  '.cumulus-tasks-selbar {',
1980
- ' display: flex; align-items: center; gap: 8px; flex-shrink: 0;',
1981
- ' padding: 0.5em 0.85em; background: #2d2d2d; border-top: 1px solid #3a3a3a;',
1987
+ ' display: flex; align-items: center; gap: 6px; flex-shrink: 0;',
1988
+ ' padding: 0.45em 0.6em 0.45em 0.85em; background: #2d2d2d;',
1989
+ ' border-bottom: 1px solid #3a3a3a;',
1990
+ '}',
1991
+ '.cumulus-tasks-selcount { font-size: 11.5px; color: #aaa; margin-right: auto; }',
1992
+ '.cumulus-tasks-refresh-note { font-size: 11.5px; color: #888; margin-right: auto; }',
1993
+ '.cumulus-tasks-action--refresh:disabled { opacity: 0.55; cursor: default; }',
1994
+ '.cumulus-tasks-action {',
1995
+ ' background: #2a2a2a; border: 1px solid #3f3f3f; color: #ddd;',
1996
+ ' border-radius: 4px; padding: 0.28em 0.6em; cursor: pointer; font-size: 11px;',
1997
+ ' white-space: nowrap;',
1998
+ '}',
1999
+ '.cumulus-tasks-action:hover { border-color: #666; color: #fff; }',
2000
+ '.cumulus-tasks-action--closed { background: #2f4a35; border-color: #3f5f45; }',
2001
+ '.cumulus-tasks-action--archive { background: #3a3320; border-color: #55492c; }',
2002
+ '.cumulus-tasks-selclear {',
2003
+ ' background: none; border: none; color: #777; cursor: pointer;',
2004
+ ' font-size: 15px; line-height: 1; padding: 2px 3px;',
2005
+ '}',
2006
+ '.cumulus-tasks-selclear:hover { color: #e0e0e0; }',
2007
+
2008
+ /* Inline comment editor — the drawer never opens the floating popover */
2009
+ '.cumulus-tasks-comment {',
2010
+ ' background: #232323; border: 1px solid #3a3a3a; border-radius: 5px;',
2011
+ ' margin: 4px 6px; padding: 0.5em 0.6em; flex-shrink: 0;',
1982
2012
  '}',
1983
- '.cumulus-tasks-selcount { font-size: 11.5px; color: #aaa; }',
1984
- '.cumulus-tasks-comment-btn {',
1985
- ' margin-left: auto; background: #2f4a35; border: 1px solid #3f5f45; color: #ddd;',
1986
- ' border-radius: 4px; padding: 0.3em 0.8em; cursor: pointer; font-size: 11.5px;',
2013
+ '.cumulus-tasks-comment-quote {',
2014
+ ' font-size: 11px; color: #9a9a9a; white-space: pre-wrap; line-height: 1.4;',
2015
+ ' border-left: 2px solid #4a4a4a; padding-left: 0.5em; margin-bottom: 0.45em;',
2016
+ ' max-height: 5.6em; overflow-y: auto;',
1987
2017
  '}',
1988
- '.cumulus-tasks-comment-btn:disabled { background: #2a2a2a; border-color: #333; color: #666; cursor: not-allowed; }',
2018
+ '.cumulus-tasks-comment-input {',
2019
+ ' width: 100%; box-sizing: border-box; resize: vertical;',
2020
+ ' background: #1a1a1a; border: 1px solid #3a3a3a; border-radius: 4px;',
2021
+ ' color: #e0e0e0; font-size: 12px; font-family: inherit; padding: 0.4em 0.5em;',
2022
+ '}',
2023
+ '.cumulus-tasks-comment-actions {',
2024
+ ' display: flex; justify-content: flex-end; gap: 6px; margin-top: 0.4em;',
2025
+ '}',
2026
+ '.cumulus-tasks-comment-actions button {',
2027
+ ' border-radius: 4px; padding: 0.25em 0.7em; cursor: pointer; font-size: 11px;',
2028
+ ' background: #2a2a2a; border: 1px solid #3f3f3f; color: #ccc;',
2029
+ '}',
2030
+ '.cumulus-tasks-comment-add { background: #2f4a35 !important; border-color: #3f5f45 !important; color: #ddd !important; }',
2031
+ '.cumulus-task-title--clickable { cursor: pointer; }',
2032
+ '.cumulus-task-title--clickable:hover { text-decoration: underline; text-decoration-color: #555; }',
1989
2033
  '.cumulus-include-add-btn {',
1990
2034
  ' background: #0066cc; border: none; color: white;',
1991
2035
  ' padding: 0.4em 0.8em; border-radius: 4px; cursor: pointer;',
@@ -6653,6 +6697,25 @@
6653
6697
  stallCard.appendChild(stallSelect);
6654
6698
  body.appendChild(stallCard);
6655
6699
 
6700
+ // Task 179: background workers get their own dial rather than sharing the
6701
+ // classifier's. The jobs differ in difficulty — that one is a yes/no read of
6702
+ // three sentences, this one rewrites a whole task file and judges every
6703
+ // task's state — so the cheap model that is plenty for the first may not be
6704
+ // for the second.
6705
+ var workerCard = document.createElement('div');
6706
+ workerCard.className = 'cumulus-settings-default-card';
6707
+ var workerCardLabel = document.createElement('label');
6708
+ workerCardLabel.textContent = 'Background workers use';
6709
+ var workerSelect = document.createElement('select');
6710
+ workerSelect.className = 'cumulus-settings-select';
6711
+ workerSelect.setAttribute('data-testid', 'webchat-gw-worker-select');
6712
+ workerSelect.addEventListener('change', function () {
6713
+ gwState.workerModel = workerSelect.value;
6714
+ });
6715
+ workerCard.appendChild(workerCardLabel);
6716
+ workerCard.appendChild(workerSelect);
6717
+ body.appendChild(workerCard);
6718
+
6656
6719
  var modelsLabel = document.createElement('div');
6657
6720
  modelsLabel.className = 'cumulus-settings-section-label';
6658
6721
  modelsLabel.textContent = 'Model catalog';
@@ -7292,6 +7355,18 @@
7292
7355
  if (stallSelect.value !== (gwState.stallClassifierModel || '')) stallSelect.value = '';
7293
7356
  }
7294
7357
 
7358
+ // Task 179 — same rule, same fallback, its own setting.
7359
+ function renderWorkerSelect() {
7360
+ workerSelect.innerHTML = '';
7361
+ var workerHaiku = document.createElement('option');
7362
+ workerHaiku.value = '';
7363
+ workerHaiku.textContent = 'Haiku (default)';
7364
+ workerSelect.appendChild(workerHaiku);
7365
+ fillModelOptions(workerSelect);
7366
+ workerSelect.value = gwState.workerModel || '';
7367
+ if (workerSelect.value !== (gwState.workerModel || '')) workerSelect.value = '';
7368
+ }
7369
+
7295
7370
  // Rebuild the provider list from state before anything reads it, so a
7296
7371
  // provider added or removed this session reaches the cards, the default
7297
7372
  // dropdown and the add-model select in the same pass (task 147).
@@ -7316,6 +7391,7 @@
7316
7391
  renderDefaultSelect();
7317
7392
  renderVoiceSelect();
7318
7393
  renderStallSelect();
7394
+ renderWorkerSelect();
7319
7395
  renderGwModels();
7320
7396
  }
7321
7397
 
@@ -9221,45 +9297,86 @@
9221
9297
  errorEl.style.display = 'none';
9222
9298
  drawer.appendChild(errorEl);
9223
9299
 
9224
- var body = document.createElement('div');
9225
- body.className = 'cumulus-tasks-body';
9226
- body.innerHTML = '<div class="cumulus-tasks-empty">Loading…</div>';
9227
- drawer.appendChild(body);
9228
-
9300
+ // Task 178: the selection bar sits ABOVE the list and exists only while
9301
+ // something is ticked. At the bottom it anchored the comment editor below
9302
+ // the panel's own floor, where the panel clipped it — the button worked and
9303
+ // nothing appeared. Nothing here is positioned relative to the viewport now.
9229
9304
  var selbar = document.createElement('div');
9230
9305
  selbar.className = 'cumulus-tasks-selbar';
9306
+ selbar.setAttribute('data-testid', 'webchat-tasks-selbar');
9307
+
9308
+ // Task 179 — Refresh occupies the toolbar whenever nothing is ticked. It is
9309
+ // the only control here that acts on the WHOLE file rather than a selection,
9310
+ // which is why it swaps out entirely the moment a row is ticked rather than
9311
+ // sitting alongside actions that mean something narrower.
9312
+ var refreshNote = document.createElement('span');
9313
+ refreshNote.className = 'cumulus-tasks-refresh-note';
9314
+ refreshNote.setAttribute('data-testid', 'webchat-tasks-refresh-note');
9315
+ refreshNote.textContent = '';
9316
+ selbar.appendChild(refreshNote);
9317
+
9318
+ var refreshBtn = taskActionButton('Refresh', 'refresh', function () {
9319
+ requestTasksRefresh(drawer, threadName);
9320
+ });
9321
+ refreshBtn.setAttribute(
9322
+ 'title',
9323
+ 'Convert this file to the current format and re-check every task’s state. ' +
9324
+ 'Commits a snapshot to git first.'
9325
+ );
9326
+ selbar.appendChild(refreshBtn);
9327
+
9231
9328
  var selcount = document.createElement('span');
9232
9329
  selcount.className = 'cumulus-tasks-selcount';
9233
9330
  selcount.setAttribute('data-testid', 'webchat-tasks-selcount');
9234
9331
  selcount.textContent = '0 selected';
9235
9332
  selbar.appendChild(selcount);
9236
- var commentBtn = document.createElement('button');
9237
- commentBtn.className = 'cumulus-tasks-comment-btn';
9238
- commentBtn.setAttribute('data-testid', 'webchat-tasks-comment-btn');
9239
- commentBtn.textContent = 'Comment';
9240
- commentBtn.disabled = true;
9241
- commentBtn.addEventListener('click', function () {
9242
- var labels = drawer._selectedLabels ? drawer._selectedLabels() : [];
9243
- if (labels.length === 0) return;
9244
- // The same popover the chat uses for a highlighted quote — the selected
9245
- // rows ARE the quote, so the chip that lands in the composer is
9246
- // indistinguishable from one made by highlighting text.
9247
- showAnnotationPopover(
9333
+
9334
+ var commentBtn = taskActionButton('Comment', 'comment', function () {
9335
+ openTaskComment(
9248
9336
  panel,
9249
- labels.join('\n'),
9250
- commentBtn.getBoundingClientRect(),
9251
- false,
9252
- ''
9337
+ drawer,
9338
+ drawer._selectedLabels ? drawer._selectedLabels() : [],
9339
+ selbar,
9340
+ 'selection'
9253
9341
  );
9254
9342
  });
9255
9343
  selbar.appendChild(commentBtn);
9344
+
9345
+ var closedBtn = taskActionButton('Closed ✓', 'closed', function () {
9346
+ dispositionSelectedTasks(drawer, threadName, 'closed');
9347
+ });
9348
+ selbar.appendChild(closedBtn);
9349
+ var archiveBtn = taskActionButton('Archive', 'archive', function () {
9350
+ dispositionSelectedTasks(drawer, threadName, 'archived');
9351
+ });
9352
+ selbar.appendChild(archiveBtn);
9353
+
9354
+ var clearBtn = document.createElement('button');
9355
+ clearBtn.className = 'cumulus-tasks-selclear';
9356
+ clearBtn.setAttribute('data-testid', 'webchat-tasks-selclear');
9357
+ clearBtn.setAttribute('title', 'Clear selection');
9358
+ clearBtn.textContent = '\xD7';
9359
+ clearBtn.addEventListener('click', function () {
9360
+ clearTaskSelection(drawer);
9361
+ });
9362
+ selbar.appendChild(clearBtn);
9256
9363
  drawer.appendChild(selbar);
9257
9364
 
9365
+ var body = document.createElement('div');
9366
+ body.className = 'cumulus-tasks-body';
9367
+ body.innerHTML = '<div class="cumulus-tasks-empty">Loading…</div>';
9368
+ drawer.appendChild(body);
9369
+
9258
9370
  drawer._body = body;
9259
9371
  drawer._errorEl = errorEl;
9260
9372
  drawer._countEl = count;
9373
+ drawer._selbar = selbar;
9261
9374
  drawer._selcount = selcount;
9262
- drawer._commentBtn = commentBtn;
9375
+ drawer._panel = panel;
9376
+ drawer._refreshBtn = refreshBtn;
9377
+ drawer._refreshNote = refreshNote;
9378
+ // The selection half of the toolbar, hidden together at zero selected.
9379
+ drawer._selGroup = [selcount, commentBtn, closedBtn, archiveBtn, clearBtn];
9263
9380
 
9264
9381
  panel.appendChild(drawer);
9265
9382
  if (connection) connection.send({ type: 'tasks_list', threadName: threadName });
@@ -9385,7 +9502,7 @@
9385
9502
  row.appendChild(now);
9386
9503
  }
9387
9504
 
9388
- return { row: row, check: check };
9505
+ return { row: row, check: check, title: title };
9389
9506
  }
9390
9507
 
9391
9508
  function renderTasks(panel, threadName, data) {
@@ -9407,13 +9524,19 @@
9407
9524
  var done = data.done || [];
9408
9525
  drawer._countEl.textContent = data.missing ? '' : queue.length + ' open';
9409
9526
 
9527
+ // A row-anchored editor lives inside the list being rebuilt; dropping it here
9528
+ // is what stops it surviving as a detached node its Add button still writes to.
9529
+ closeTaskComment(drawer, 'row');
9410
9530
  body.innerHTML = '';
9411
9531
 
9412
9532
  if (data.missing) {
9533
+ // The un-migrated case is exactly what Refresh is for, so say so here
9534
+ // rather than leaving the button above looking decorative (task 179).
9413
9535
  body.innerHTML =
9414
- '<div class="cumulus-tasks-empty">This thread has no tasks file.<br><br>' +
9415
- 'Add a <code>Tasks.md</code> carrying the <code>cumulus:tasks</code> marker to the ' +
9416
- 'thread’s always-include list, and it will appear here.</div>';
9536
+ '<div class="cumulus-tasks-empty">This thread has no tasks file in the current ' +
9537
+ 'format.<br><br>If it already has a <code>Tasks.md</code> in its always-include ' +
9538
+ 'list, press <b>Refresh</b> to convert it the file is committed to git first. ' +
9539
+ 'Otherwise add one, and it will appear here.</div>';
9417
9540
  drawer._selected = {};
9418
9541
  updateTaskSelection(drawer);
9419
9542
  return;
@@ -9434,6 +9557,7 @@
9434
9557
  var built = buildTaskRow(task, { isNow: i === 0, done: false });
9435
9558
  wireTaskDrag(built.row, drawer, threadName);
9436
9559
  wireTaskCheck(built, drawer);
9560
+ wireTaskComment(built, drawer, task);
9437
9561
 
9438
9562
  if (task.depProposed) {
9439
9563
  var dep = document.createElement('span');
@@ -9459,6 +9583,7 @@
9459
9583
  done.forEach(function (task) {
9460
9584
  var built = buildTaskRow(task, { isNow: false, done: true });
9461
9585
  wireTaskCheck(built, drawer);
9586
+ wireTaskComment(built, drawer, task);
9462
9587
  body.appendChild(built.row);
9463
9588
  });
9464
9589
  }
@@ -9505,6 +9630,23 @@
9505
9630
  return btn;
9506
9631
  }
9507
9632
 
9633
+ // Clicking a row's title comments on THAT row, whatever is ticked — the
9634
+ // one-task path Karl asked for, and the reason the editor anchors to the row
9635
+ // rather than always to the bar.
9636
+ function wireTaskComment(built, drawer, task) {
9637
+ built.title.classList.add('cumulus-task-title--clickable');
9638
+ built.title.setAttribute('title', 'Comment on this task');
9639
+ built.title.addEventListener('click', function () {
9640
+ var label = task.id + ' · ' + task.title;
9641
+ if (drawer._commentEditor && drawer._commentEditor._taskId === task.id) {
9642
+ closeTaskComment(drawer);
9643
+ return;
9644
+ }
9645
+ openTaskComment(drawer._panel, drawer, [label], built.row, 'row');
9646
+ if (drawer._commentEditor) drawer._commentEditor._taskId = task.id;
9647
+ });
9648
+ }
9649
+
9508
9650
  function wireTaskCheck(built, drawer) {
9509
9651
  built.check.addEventListener('change', function () {
9510
9652
  var id = built.row.getAttribute('data-task-id');
@@ -9514,10 +9656,163 @@
9514
9656
  });
9515
9657
  }
9516
9658
 
9659
+ // The toolbar shows exactly one of two things. At zero selected it is
9660
+ // Refresh, which acts on the whole file; with rows ticked it is the count and
9661
+ // the actions that act on those rows. Task 178 hid the bar entirely at zero
9662
+ // because a permanent "0 selected" strip answered a question nobody asked —
9663
+ // 179 gives that space a real use, so the bar stays and its contents swap.
9517
9664
  function updateTaskSelection(drawer) {
9518
9665
  var n = Object.keys(drawer._selected).length;
9519
9666
  drawer._selcount.textContent = n + ' selected';
9520
- drawer._commentBtn.disabled = n === 0;
9667
+ var group = drawer._selGroup || [];
9668
+ for (var i = 0; i < group.length; i++) group[i].style.display = n === 0 ? 'none' : '';
9669
+ drawer._refreshBtn.style.display = n === 0 ? '' : 'none';
9670
+ drawer._refreshNote.style.display = n === 0 ? '' : 'none';
9671
+ if (n === 0) closeTaskComment(drawer, 'selection');
9672
+ }
9673
+
9674
+ // Task 179. The button is disabled optimistically rather than on the server's
9675
+ // `running` push: the round-trip is long enough to double-click through, and a
9676
+ // second press is REFUSED by the server, not queued — so an un-disabled button
9677
+ // would just produce an error message for a press the user meant.
9678
+ function requestTasksRefresh(drawer, threadName) {
9679
+ if (drawer._refreshBtn.disabled) return;
9680
+ setTasksRefreshBusy(drawer, true, 'Refreshing…');
9681
+ drawer._errorEl.textContent = '';
9682
+ drawer._errorEl.style.display = 'none';
9683
+ if (connection) connection.send({ type: 'tasks_refresh', threadName: threadName });
9684
+ }
9685
+
9686
+ function setTasksRefreshBusy(drawer, busy, note) {
9687
+ drawer._refreshBtn.disabled = !!busy;
9688
+ drawer._refreshBtn.textContent = busy ? 'Refreshing…' : 'Refresh';
9689
+ drawer._refreshNote.textContent = note || '';
9690
+ }
9691
+
9692
+ // A worker's terminal status. `failed` lands in the same error strip a rejected
9693
+ // drag uses — one place in the drawer where the file's own truth is reported,
9694
+ // whatever refused the write.
9695
+ function applyWorkerStatus(panel, threadName, data) {
9696
+ var drawer = panel.querySelector('.cumulus-tasks-drawer');
9697
+ if (!drawer || drawer._threadName !== threadName) return;
9698
+ if (data.kind !== 'tasks-refresh') return;
9699
+
9700
+ if (data.state === 'running') {
9701
+ setTasksRefreshBusy(drawer, true, 'Refreshing…');
9702
+ return;
9703
+ }
9704
+ setTasksRefreshBusy(drawer, false, data.state === 'done' ? data.detail || '' : '');
9705
+ if (data.state === 'failed') {
9706
+ drawer._errorEl.textContent = data.detail || 'Refresh failed.';
9707
+ drawer._errorEl.style.display = '';
9708
+ }
9709
+ }
9710
+
9711
+ function taskActionButton(label, name, onClick) {
9712
+ var btn = document.createElement('button');
9713
+ btn.className = 'cumulus-tasks-action cumulus-tasks-action--' + name;
9714
+ btn.setAttribute('data-testid', 'webchat-tasks-' + name + '-btn');
9715
+ btn.textContent = label;
9716
+ btn.addEventListener('click', onClick);
9717
+ return btn;
9718
+ }
9719
+
9720
+ function clearTaskSelection(drawer) {
9721
+ var boxes = drawer._body.querySelectorAll('.cumulus-task-check');
9722
+ for (var i = 0; i < boxes.length; i++) boxes[i].checked = false;
9723
+ drawer._selected = {};
9724
+ updateTaskSelection(drawer);
9725
+ }
9726
+
9727
+ // Closing and archiving are the same write with a different glyph, so they are
9728
+ // the same message: the server decides what a disposition does to the file, and
9729
+ // the answer comes back through the one renderer every other write uses.
9730
+ function dispositionSelectedTasks(drawer, threadName, disposition) {
9731
+ var ids = Object.keys(drawer._selected);
9732
+ if (ids.length === 0) return;
9733
+ closeTaskComment(drawer);
9734
+ if (connection)
9735
+ connection.send({
9736
+ type: 'tasks_disposition',
9737
+ threadName: threadName,
9738
+ ids: ids,
9739
+ disposition: disposition,
9740
+ });
9741
+ drawer._selected = {};
9742
+ updateTaskSelection(drawer);
9743
+ }
9744
+
9745
+ // Task 178: the comment editor is part of the drawer, inserted directly below
9746
+ // whatever it is about — the selection bar for several rows, the row itself for
9747
+ // one. It ends at `addAnnotationChip`, the same call the chat's floating
9748
+ // popover makes, so the chip in the composer is identical either way.
9749
+ function openTaskComment(panel, drawer, labels, anchorEl, origin) {
9750
+ closeTaskComment(drawer);
9751
+ if (!labels || labels.length === 0 || !anchorEl || !anchorEl.parentNode) return;
9752
+
9753
+ var editor = document.createElement('div');
9754
+ editor.className = 'cumulus-tasks-comment';
9755
+ editor.setAttribute('data-testid', 'webchat-tasks-comment');
9756
+ editor._origin = origin || 'selection';
9757
+
9758
+ var quote = document.createElement('div');
9759
+ quote.className = 'cumulus-tasks-comment-quote';
9760
+ quote.textContent = labels.join('\n');
9761
+ editor.appendChild(quote);
9762
+
9763
+ var textarea = document.createElement('textarea');
9764
+ textarea.className = 'cumulus-tasks-comment-input';
9765
+ textarea.setAttribute('data-testid', 'webchat-tasks-comment-input');
9766
+ textarea.placeholder = 'Add a comment…';
9767
+ textarea.rows = 3;
9768
+ editor.appendChild(textarea);
9769
+
9770
+ var actions = document.createElement('div');
9771
+ actions.className = 'cumulus-tasks-comment-actions';
9772
+
9773
+ var cancelBtn = document.createElement('button');
9774
+ cancelBtn.className = 'cumulus-tasks-comment-cancel';
9775
+ cancelBtn.setAttribute('data-testid', 'webchat-tasks-comment-cancel');
9776
+ cancelBtn.textContent = 'Cancel';
9777
+ cancelBtn.addEventListener('click', function () {
9778
+ closeTaskComment(drawer);
9779
+ });
9780
+ actions.appendChild(cancelBtn);
9781
+
9782
+ var addBtn = document.createElement('button');
9783
+ addBtn.className = 'cumulus-tasks-comment-add';
9784
+ addBtn.setAttribute('data-testid', 'webchat-tasks-comment-add');
9785
+ addBtn.textContent = 'Add';
9786
+ addBtn.addEventListener('click', function () {
9787
+ var comment = textarea.value.trim();
9788
+ if (comment) addAnnotationChip(panel, labels.join('\n'), comment, false, '');
9789
+ closeTaskComment(drawer);
9790
+ });
9791
+ actions.appendChild(addBtn);
9792
+ editor.appendChild(actions);
9793
+
9794
+ textarea.addEventListener('keydown', function (e) {
9795
+ if (e.key === 'Enter' && !e.shiftKey) {
9796
+ e.preventDefault();
9797
+ addBtn.click();
9798
+ } else if (e.key === 'Escape') {
9799
+ closeTaskComment(drawer);
9800
+ }
9801
+ });
9802
+
9803
+ anchorEl.parentNode.insertBefore(editor, anchorEl.nextSibling);
9804
+ drawer._commentEditor = editor;
9805
+ // Safe to focus here, unlike the chat popover: there is no text selection
9806
+ // for the caret to destroy.
9807
+ textarea.focus();
9808
+ }
9809
+
9810
+ function closeTaskComment(drawer, onlyOrigin) {
9811
+ var editor = drawer._commentEditor;
9812
+ if (!editor) return;
9813
+ if (onlyOrigin && editor._origin !== onlyOrigin) return;
9814
+ if (editor.parentNode) editor.parentNode.removeChild(editor);
9815
+ drawer._commentEditor = null;
9521
9816
  }
9522
9817
 
9523
9818
  function openIncludeOverlay(panel, threadName) {
@@ -10653,6 +10948,18 @@
10653
10948
  }
10654
10949
  break;
10655
10950
 
10951
+ // A gateway-side background worker changed state (task 179). Deliberately
10952
+ // NOT `thread_activity`: a worker is not a turn, so it must not light the
10953
+ // sidebar dots or claim the thread is busy.
10954
+ case 'worker_status':
10955
+ if (data.threadName) {
10956
+ var workerPanel = document.querySelector(
10957
+ '[data-thread-panel="' + data.threadName + '"]'
10958
+ );
10959
+ if (workerPanel) applyWorkerStatus(workerPanel, data.threadName, data);
10960
+ }
10961
+ break;
10962
+
10656
10963
  case 'include_added':
10657
10964
  case 'include_removed':
10658
10965
  // List already updated by the handler — no extra action needed
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Task 179 — the Refresh button's actual work.
3
+ *
4
+ * Karl's three answers set the shape of this file:
5
+ *
6
+ * 1. *"Write it straight away, but commit to git first so I can undo."* — so a
7
+ * snapshot commit is a PRECONDITION, and a file outside a git repo is
8
+ * refused outright. The permission to write without asking was bought with
9
+ * the undo; without a repo there is no undo, so the permission lapses.
10
+ * 2. *"Both — convert to the new format AND re-check every task's state."* — one
11
+ * model call does both, because they are the same rewrite.
12
+ * 3. *"Build it as a general facility."* — which is `worker.ts`; this module is
13
+ * only its first caller.
14
+ *
15
+ * The load-bearing part is not the prompt. A model handed a whole file and asked
16
+ * for a whole file back will, sooner or later, quietly drop a row — so the answer
17
+ * is checked before it is written, and a run that loses a task id, fails to
18
+ * parse, or comes back without the marker is refused WHOLE. The git snapshot is
19
+ * the belt; these checks are the braces.
20
+ */
21
+ import { type TasksPayload } from './tasks.js';
22
+ import { type RunWorkerOptions } from './worker.js';
23
+ /** A refusal with a reason the drawer shows verbatim. */
24
+ export declare class TasksRefreshError extends Error {
25
+ }
26
+ /**
27
+ * Every task id the file currently carries.
28
+ *
29
+ * Deliberately permissive, and matched against the RAW text rather than a parse:
30
+ * the un-migrated files this exists to convert do not parse at all, and they are
31
+ * the ones with the most to lose. Both shapes seen in the wild are covered —
32
+ * `- [ ] 179 · title` (schema) and `- [◕] 178: title` (what came before).
33
+ *
34
+ * The `## Format` section is skipped, and that is not a nicety: the manual
35
+ * carried inside every migrated file demonstrates the grammar with EXAMPLE rows
36
+ * (`- [ ] 073 · StorePool reaping`, `- [⊘] 084 · Creative-writing benchmark`).
37
+ * Counting those as tasks would demand the model preserve four ids that do not
38
+ * exist, so every already-migrated file would fail the drop check and Refresh
39
+ * would work only on un-migrated ones. Found by test, not by reading.
40
+ */
41
+ export declare function taskIdsIn(content: string): string[];
42
+ /** Strip a ```-fenced wrapper, which a chat-tuned model adds however firmly it is told not to. */
43
+ export declare function unfence(answer: string): string;
44
+ /**
45
+ * Commit the file as it stands, and return whether there was anything to commit.
46
+ *
47
+ * Scoped by pathspec on BOTH commands, so a refresh never sweeps up whatever else
48
+ * the user happened to have staged. "Nothing to commit" is success, not failure:
49
+ * the file is already identical to HEAD, so HEAD *is* the undo point.
50
+ *
51
+ * Not being in a git repo is the one hard refusal here — see the module comment.
52
+ */
53
+ export declare function snapshotBeforeRefresh(filePath: string): boolean;
54
+ export interface RefreshPromptInput {
55
+ content: string;
56
+ migrated: boolean;
57
+ docs: string[];
58
+ version: string;
59
+ }
60
+ /**
61
+ * The whole prompt, as one pure function so the instructions are testable
62
+ * without a model.
63
+ *
64
+ * The three facts at the end are the ones the model cannot get from the file and
65
+ * would otherwise invent: which task docs exist, and what the gateway is actually
66
+ * running. That last one is the thing Karl complained about — rows still saying
67
+ * "needs reload" long after the reload happened.
68
+ */
69
+ export declare function buildRefreshPrompt(input: RefreshPromptInput): string;
70
+ export interface RefreshResult {
71
+ payload: TasksPayload;
72
+ /** False when the file was already identical to HEAD — HEAD is then the undo point. */
73
+ committed: boolean;
74
+ /** Ids the model dropped. Non-empty only on the refusal path, for the message. */
75
+ droppedIds: string[];
76
+ }
77
+ export interface RefreshOptions extends RunWorkerOptions {
78
+ /** Test seam: run this instead of the model. Production leaves it unset. */
79
+ runModel?: (prompt: string) => Promise<string>;
80
+ }
81
+ /**
82
+ * Convert and re-check a thread's tasks file in place.
83
+ *
84
+ * Order matters and is the design: resolve → snapshot → ask → CHECK → write.
85
+ * Nothing between the snapshot and the write touches disk, so every refusal below
86
+ * leaves the file byte-identical to what was just committed.
87
+ */
88
+ export declare function refreshTasksFile(threadName: string, opts?: RefreshOptions): Promise<RefreshResult>;
89
+ //# sourceMappingURL=tasks-refresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tasks-refresh.d.ts","sourceRoot":"","sources":["../../src/gateway/tasks-refresh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,OAAO,EAAmC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/D,yDAAyD;AACzD,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAO/C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAWnD;AAED,kGAAkG;AAClG,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAO9C;AAMD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CA6B/D;AAcD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAuCpE;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,YAAY,CAAC;IACtB,uFAAuF;IACvF,SAAS,EAAE,OAAO,CAAC;IACnB,kFAAkF;IAClF,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACtD,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAChD;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,aAAa,CAAC,CAkExB"}