@gotcos/glasses-server 6.98.1 → 6.99.16
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/CHANGELOG.md +227 -0
- package/package.json +2 -1
- package/server/index.ts +170 -11
- package/server/lib/api-auth.ts +31 -14
- package/server/lib/claude-bridge.ts +21 -2
- package/server/lib/codex-bridge.ts +21 -2
- package/server/lib/cos-thread.ts +582 -0
- package/server/lib/display-bus.ts +3 -0
- package/server/lib/drop-attachments.ts +55 -0
- package/server/lib/drop-destinations.ts +134 -0
- package/server/lib/drop-history-search.ts +97 -0
- package/server/lib/drop-p2-boot.ts +60 -0
- package/server/lib/drop-p2-runtime.ts +237 -0
- package/server/lib/drop-p2-settings.ts +174 -0
- package/server/lib/drop-resolution.ts +168 -0
- package/server/lib/drop-router.ts +325 -0
- package/server/lib/drop-service.ts +830 -0
- package/server/lib/drop-settings.ts +56 -0
- package/server/lib/drop-store.ts +450 -0
- package/server/lib/drop-when.ts +265 -0
- package/server/lib/glasses-pairing.ts +15 -0
- package/server/lib/learning-needs-you.ts +111 -0
- package/server/lib/live-cues-v2-settings.ts +2 -1
- package/server/lib/live-cues-v2.ts +6 -4
- package/server/lib/media-store.ts +80 -0
- package/server/lib/meeting-batch-transcribe.ts +15 -2
- package/server/lib/meeting-consolidation-run.ts +169 -0
- package/server/lib/meeting-store.ts +61 -0
- package/server/lib/meeting-title-steer.ts +200 -0
- package/server/lib/network-policy.ts +84 -6
- package/server/lib/permission-broker.ts +13 -0
- package/server/lib/phone-recording.ts +1 -1
- package/server/lib/ping-budget.ts +149 -0
- package/server/lib/ping-engine.ts +245 -0
- package/server/lib/ping-feed-sink.ts +33 -0
- package/server/lib/ping-sources.ts +192 -0
- package/server/lib/query-job-coordinator.ts +5 -2
- package/server/lib/query-job-runtime.ts +45 -4
- package/server/lib/query-job-types.ts +10 -1
- package/server/lib/reminder-scheduler.ts +153 -0
- package/server/lib/tailnet-self-name.ts +103 -0
- package/server/lib/tailscale-nudge.ts +40 -2
- package/server/lib/task-store.ts +32 -0
- package/server/lib/web-ask-admission.ts +56 -0
- package/server/lib/web-live-chunks.ts +273 -0
- package/server/lib/web-memories-bridge.ts +323 -0
- package/server/lib/web-session-auth.ts +492 -0
- package/server/lib/web-usage-bridge.ts +154 -0
- package/server/lib/work-search.ts +14 -1
- package/server/lib/your-move-count.ts +58 -0
- package/server/routes/cos-thread.ts +79 -0
- package/server/routes/display.ts +11 -1
- package/server/routes/drops.ts +262 -0
- package/server/routes/health.ts +16 -0
- package/server/routes/live-cues.ts +5 -4
- package/server/routes/meeting.ts +61 -5
- package/server/routes/permission-broker.ts +23 -3
- package/server/routes/transcribe-stream.ts +43 -4
- package/server/routes/web-control.ts +569 -0
- package/server/routes/welcome-context.ts +2 -1
- package/server/scripts/web-live-cls/cdp.mjs +57 -0
- package/server/scripts/web-live-cls/measure.mjs +80 -0
- package/server/scripts/web-live-cls/mock.mjs +97 -0
- package/server/scripts/web-live-cls/render.mjs +26 -0
- package/server/web-control/app.css +519 -0
- package/server/web-control/app.js +3643 -0
- package/server/web-control/control.css +325 -0
- package/server/web-control/fonts/DMSans.ttf +0 -0
- package/server/web-control/fonts/Fraunces.ttf +0 -0
- package/server/web-control/fonts/JetBrainsMono.ttf +0 -0
- package/server/web-control/index.html +275 -0
- package/server/web-control/mark.svg +1 -0
- package/server/web-control/memories/d3.min.js +2 -0
- package/server/web-control/memories/graph-explorer.css +200 -0
- package/server/web-control/memories/graph-explorer.js +748 -0
- package/server/web-control/memories/memories-app.js +1561 -0
- package/server/web-control/memories/memories-theme.css +73 -0
- package/server/web-control/memories/memories.html +72 -0
- package/server/web-control/memories/memory-stewardship.js +63 -0
- package/server/web-control/memories/memory-workspace.js +216 -0
- package/server/web-control/panes/cos-chat.css +174 -0
- package/server/web-control/panes/cos-chat.js +1118 -0
- package/server/web-control/panes/dictation.js +173 -0
- package/server/web-control/panes/meetings.css +314 -0
- package/server/web-control/panes/meetings.js +2568 -0
- package/server/web-control/panes/messages.js +718 -0
- package/server/web-control/panes/questions.css +32 -0
- package/server/web-control/panes/questions.js +275 -0
- package/server/web-control/panes/sessions.css +452 -0
- package/server/web-control/panes/settings.js +292 -0
- package/server/web-control/panes/speakers.css +269 -0
- package/server/web-control/panes/speakers.js +2755 -0
- package/server/web-control/panes/threads.js +200 -0
- package/server/web-control/panes/work.css +261 -0
- package/server/web-control/panes/work.js +1667 -0
- package/server/web-control/shell.js +294 -0
- package/server/web-control/theme.js +25 -0
- package/server/web-control/vendor/qrcode-encoder.js +10 -0
|
@@ -0,0 +1,1667 @@
|
|
|
1
|
+
/* COS Control Web: Work, built to Control 0.5.366's Work pane (WorkWorkspaceView.swift; stages 188-221, header 1523,
|
|
2
|
+
tabs 1554, filters 1585, board 1818-1910, Your move 2013-2052, card 2365-2462, detail 2725-3052, Sort 3275-3514;
|
|
3
|
+
WorkHandoffView.swift WorkHandoffState 88-133; ActivityWindow.swift WorkTaskEditor 50-200; WorkTaskContext.swift).
|
|
4
|
+
Data: GET /api/work-board, /api/work-board/activity/open, /api/work-board/activity?domain&workIdentity, /api/work-intake,
|
|
5
|
+
/api/domains, /api/agent-sessions (live session state, soft). Writes (each through W.api, never on load):
|
|
6
|
+
POST /api/work-board/stage|edit, /api/tasks/capture, PATCH /api/tasks/:id, POST /api/work-intake/:id/resolve,
|
|
7
|
+
/api/work-intake/resolve-batch|undo, /api/work-board/batch, /api/work-board/handoff-requests, /api/work/search (Return only).
|
|
8
|
+
Routes: #/work, #/work/<workIdentity>, #/work/<workIdentity>/edit, #/work/<workIdentity>/start. */
|
|
9
|
+
(function () {
|
|
10
|
+
'use strict';
|
|
11
|
+
var W = window.COSWeb;
|
|
12
|
+
var $ = W.$, esc = W.esc, api = W.api;
|
|
13
|
+
var pane = $('pane-work');
|
|
14
|
+
|
|
15
|
+
// ── Control's vocabulary ─────────────────────────────────
|
|
16
|
+
var STAGES = ['mentioned', 'planned', 'draft', 'built', 'qa', 'complete'];
|
|
17
|
+
var COLUMNS = ['planned', 'draft', 'built', 'qa', 'complete'];
|
|
18
|
+
var STAGE_TITLE = { mentioned: 'Mentioned', planned: 'Planned', draft: 'Draft', built: 'Built', qa: 'QA', complete: 'Complete' };
|
|
19
|
+
var STAGE_SUB = { mentioned: 'Captured for consideration', planned: 'Ready to work on', draft: 'First pass in progress',
|
|
20
|
+
built: 'Implementation prepared', qa: 'Checks and review', complete: 'Task marked complete' };
|
|
21
|
+
var SCOPES = [['all', 'All work'], ['attention', 'Needs attention'], ['progress', 'In progress'], ['completed', 'Completed']];
|
|
22
|
+
var ORDERS = [['board', 'Board order'], ['recent', 'Recent activity'], ['newest', 'Newest'], ['oldest', 'Oldest']];
|
|
23
|
+
var ORDER_HELP = { board: 'As you arranged each column',
|
|
24
|
+
recent: 'Last stage move, session, file or edit. Edits made outside Control show up when the task file is next committed. Cards with no date go last.',
|
|
25
|
+
newest: 'By the card’s date: its meeting or source, else when it first appeared. Cards with no date go last.' };
|
|
26
|
+
ORDER_HELP.oldest = ORDER_HELP.newest;
|
|
27
|
+
var STATE_LABEL = { running: 'Running', awaiting: 'Awaiting delivery', queued: 'Queued', waiting: 'Waiting for your input',
|
|
28
|
+
replyReady: 'Reply ready for review', sent: 'Sent to session', attention: 'Needs attention', settled: 'Done' };
|
|
29
|
+
var STATE_TONE = { running: 'green', awaiting: 'accent', queued: 'accent', replyReady: 'accent', sent: 'accent', waiting: 'amber', attention: 'danger', settled: 'muted' };
|
|
30
|
+
var STATE_RANK = { running: 0, waiting: 1, attention: 2, replyReady: 3, sent: 4, awaiting: 5, queued: 6, settled: 7 };
|
|
31
|
+
var SUMMARY_ORDER = [['running', 'running', 'running'], ['waiting', 'waiting for you', 'waiting for you'], ['attention', 'needs attention', 'need attention'],
|
|
32
|
+
['replyReady', 'reply ready', 'replies ready'], ['sent', 'sent', 'sent'], ['awaiting', 'awaiting delivery', 'awaiting delivery'], ['queued', 'queued', 'queued']];
|
|
33
|
+
var PROVIDER_NAME = { claude: 'Claude', codex: 'Codex', cursor: 'Cursor', ollama: 'Local model' };
|
|
34
|
+
var MODELS = [['', 'Server default'], ['opus', 'Claude Opus'], ['fable', 'Claude Fable'], ['sonnet', 'Claude Sonnet'],
|
|
35
|
+
['codex-frontier', 'Codex frontier'], ['codex-balanced', 'Codex balanced']];
|
|
36
|
+
var MODEL_PROVIDER = { opus: 'claude', fable: 'claude', sonnet: 'claude', haiku: 'claude', 'codex-frontier': 'codex', 'codex-balanced': 'codex' };
|
|
37
|
+
var HELPER = "'/Applications/COS Control.app/Contents/Resources/cos-control-helper'";
|
|
38
|
+
var UNDO_MISSING = 'Undo needs a newer COS server. To change a card, open it in Work.';
|
|
39
|
+
var INTAKE_REFUSALS = {
|
|
40
|
+
meeting_identity_changed: 'That meeting was renamed or merged after it was scored. Dismiss this item; the renamed meeting is checked again.',
|
|
41
|
+
meeting_unavailable: 'That meeting can’t be found; it may have been renamed or removed. Dismiss this item.',
|
|
42
|
+
intake_task_changed: 'That task changed or was closed since it was suggested. Dismiss this suggestion.',
|
|
43
|
+
meeting_links_full: 'That task already links eight meetings. Dismiss this suggestion.',
|
|
44
|
+
work_board_read_only: 'This server can’t write Work links yet.',
|
|
45
|
+
card_creation_unavailable: 'This server can’t create Work cards yet. Update the server.',
|
|
46
|
+
task_bridge_unavailable: 'The task bridge didn’t answer. Try again in a moment.',
|
|
47
|
+
intake_text_collision: 'The same words are on closed or archived work. Reopen that task instead, or dismiss this item.',
|
|
48
|
+
intake_busy: 'That item is already being saved.', intake_not_found: 'That item is no longer in Intake. Refresh.',
|
|
49
|
+
invalid_intake_action: 'Refresh Intake and try again.', invalid_intake_id: 'Refresh Intake and try again.',
|
|
50
|
+
intake_store_full: 'Intake is full. Dismiss older items, then try again.',
|
|
51
|
+
intake_store_closed: 'Intake is unavailable while the server restarts. Try again shortly.',
|
|
52
|
+
work_intake_unavailable: 'Intake is unavailable right now. Try again shortly.' };
|
|
53
|
+
var REQUEST_LINE = {
|
|
54
|
+
pending: 'Waiting for COS Control on your Mac to pick this up. Nothing has been sent yet.',
|
|
55
|
+
claimed: 'COS Control picked it up and is sending it.',
|
|
56
|
+
sent: 'Sent. The session has the card’s context.',
|
|
57
|
+
refused: 'COS Control did not send it.',
|
|
58
|
+
unresolved: 'COS Control sent it but could not confirm it arrived. Check the session before sending again.',
|
|
59
|
+
unconfirmed: 'COS Control took it and has not said whether it was sent. Check the session before sending again.',
|
|
60
|
+
expired: 'COS Control did not pick it up in time. Nothing was sent.' };
|
|
61
|
+
var STOP = {};
|
|
62
|
+
('a an and are as at be by for from in into is me my of on or our so that the their this to up was we with you your').split(' ').forEach(function (w) { STOP[w] = 1; });
|
|
63
|
+
|
|
64
|
+
// ── state ────────────────────────────────────────────────
|
|
65
|
+
var STORE = 'cos.web.work';
|
|
66
|
+
var saved = W.readStore(STORE);
|
|
67
|
+
var S = {
|
|
68
|
+
tasks: null, caps: {}, complete: false, boardError: '', loading: false, loadedAt: 0,
|
|
69
|
+
domains: null, open: null, openAvailable: false, openCaps: {}, openError: '', openAt: 0,
|
|
70
|
+
sessions: {}, intake: null, intakeCaps: {}, intakeError: '', intakeLoading: false,
|
|
71
|
+
tab: ['review', 'board', 'history', 'focus'].indexOf(saved.tab) >= 0 ? saved.tab : 'board',
|
|
72
|
+
q: '', domain: '', scope: 'all', orders: saved.orders && typeof saved.orders === 'object' ? saved.orders : {},
|
|
73
|
+
captureOpen: false, captureText: '', captureDomain: '', captureBusy: false, captureError: '',
|
|
74
|
+
moreOpen: false, mentionedOpen: false, mentionedAll: false, showAllDecisions: false, columnAll: {},
|
|
75
|
+
pendingComplete: null, busy: false, mutationError: '', note: '',
|
|
76
|
+
hidden: W.readStore(STORE + '.hidden'),
|
|
77
|
+
activity: {}, activityError: {}, activityLoading: {}, reply: {},
|
|
78
|
+
sortIndex: 0, undoBatch: null, loopAvailable: null, intakeBusy: {}, sortError: '',
|
|
79
|
+
waiting: null, dropped: null, waitingDropped: false, batchBusy: false, loopError: '',
|
|
80
|
+
meaning: null, meaningPending: '',
|
|
81
|
+
start: {}, edit: {}
|
|
82
|
+
};
|
|
83
|
+
function persist() { W.writeStore(STORE, { tab: S.tab === 'sort' || S.tab === 'waiting' ? 'board' : S.tab, orders: S.orders }); }
|
|
84
|
+
|
|
85
|
+
// ── small helpers ────────────────────────────────────────
|
|
86
|
+
function str(v) { return typeof v === 'string' ? v : ''; }
|
|
87
|
+
function titleOf(t) { return str(t.text) || str(t.title); }
|
|
88
|
+
function identity(t) { return str(t.workIdentity) || str(t.id); }
|
|
89
|
+
function workId(t) { return 'task:' + t.domain + ':' + identity(t); }
|
|
90
|
+
function stageOf(t) {
|
|
91
|
+
if (t.checked) return 'complete';
|
|
92
|
+
var v = STAGES.indexOf(t.workStage) >= 0 ? t.workStage : 'planned';
|
|
93
|
+
return v === 'complete' ? 'planned' : v;
|
|
94
|
+
}
|
|
95
|
+
function domainName(d) {
|
|
96
|
+
var hit = (S.domains || []).filter(function (x) { return x.name === d; })[0];
|
|
97
|
+
if (hit && hit.label) return hit.label;
|
|
98
|
+
return W.domainLabel(d) || String(d || '').replace(/_/g, ' ').replace(/\b\w/g, function (c) { return c.toUpperCase(); });
|
|
99
|
+
}
|
|
100
|
+
function providerName(p) { return PROVIDER_NAME[p] || (p ? p.charAt(0).toUpperCase() + p.slice(1) : 'Session'); }
|
|
101
|
+
// Control's WorkSource.plainTitle: code marks, links, emphasis and stars out; whitespace collapsed.
|
|
102
|
+
function plainTitle(line) {
|
|
103
|
+
return String(line || '').replace(/`([^`]*)`/g, '$1').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
104
|
+
.replace(/(^|[\s(])_([^_]+)_(?=[\s).,;:!?]|$)/g, '$1$2').replace(/\*+/g, '').split(/\s+/).filter(Boolean).join(' ');
|
|
105
|
+
}
|
|
106
|
+
// A title with Control's inline markdown: bold and code, links as their words. Escaped first.
|
|
107
|
+
function inline(text) {
|
|
108
|
+
return esc(String(text || '').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1'))
|
|
109
|
+
.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>').replace(/`([^`\n]+)`/g, '<code>$1</code>').replace(/\*/g, '');
|
|
110
|
+
}
|
|
111
|
+
function clip(text, n) { var s = String(text || ''); return s.length <= n ? s : s.slice(0, n - 1).replace(/\s+$/, '') + '…'; }
|
|
112
|
+
function shortAge(seconds) {
|
|
113
|
+
var s = Math.max(0, Math.floor(seconds));
|
|
114
|
+
if (s < 60) return 'now';
|
|
115
|
+
if (s < 3600) return Math.floor(s / 60) + ' min';
|
|
116
|
+
if (s < 86400) return Math.floor(s / 3600) + ' h';
|
|
117
|
+
return Math.floor(s / 86400) + ' d';
|
|
118
|
+
}
|
|
119
|
+
function secondsSince(v) { var d = W.toDate(v); return d ? (Date.now() - d.getTime()) / 1000 : null; }
|
|
120
|
+
function localDay(d) { var x = d || new Date(); return x.getFullYear() + '-' + String(x.getMonth() + 1).padStart(2, '0') + '-' + String(x.getDate()).padStart(2, '0'); }
|
|
121
|
+
// A UUID v4 from getRandomValues (crypto.randomUUID needs a secure context; a tailnet IP over http is not one).
|
|
122
|
+
function uuid() {
|
|
123
|
+
var b = new Uint8Array(16); crypto.getRandomValues(b);
|
|
124
|
+
b[6] = (b[6] & 0x0f) | 0x40; b[8] = (b[8] & 0x3f) | 0x80;
|
|
125
|
+
var h = Array.prototype.map.call(b, function (x) { return (x + 256).toString(16).slice(1); }).join('');
|
|
126
|
+
return h.slice(0, 8) + '-' + h.slice(8, 12) + '-' + h.slice(12, 16) + '-' + h.slice(16, 20) + '-' + h.slice(20);
|
|
127
|
+
}
|
|
128
|
+
function icon(name) { return '<svg class="icon"><use href="#i-' + name + '"/></svg>'; }
|
|
129
|
+
// The server's error code first (api() carries it as err.code), then its message, then a plain line.
|
|
130
|
+
function errText(err, map) {
|
|
131
|
+
var code = err && typeof err.code === 'string' ? err.code : '';
|
|
132
|
+
if (map && code && map[code]) return map[code];
|
|
133
|
+
var m = err && err.message ? err.message : 'That did not go through.';
|
|
134
|
+
return (map && map[m]) || m;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── live sessions and handoff state (WorkHandoffState, WorkActivity) ──
|
|
138
|
+
function splitSession(id) { var m = /^(claude|codex|cursor):(.+)$/.exec(String(id || '')); return m ? { provider: m[1], id: m[2] } : null; }
|
|
139
|
+
function sessionRow(a) { var s = splitSession(a && a.sessionId); return s ? S.sessions[s.id.toLowerCase()] || null : null; }
|
|
140
|
+
function sessionStatus(row) {
|
|
141
|
+
if (!row) return '';
|
|
142
|
+
var st = String(row.agent_state || row.state || '').toLowerCase();
|
|
143
|
+
if (st === 'running' || st === 'working') return 'running';
|
|
144
|
+
if (st === 'waiting') return 'waiting';
|
|
145
|
+
if (st === 'error' || st === 'failed') return 'error';
|
|
146
|
+
return 'idle';
|
|
147
|
+
}
|
|
148
|
+
function observing(a) { return ['sending', 'queued', 'running', 'delivered'].indexOf(a.status) >= 0; }
|
|
149
|
+
function observed(a) { return observing(a) ? sessionStatus(sessionRow(a)) : ''; }
|
|
150
|
+
function reportOf(a) {
|
|
151
|
+
if (!a || ['failed', 'refused', 'canceled'].indexOf(a.status) >= 0) return null;
|
|
152
|
+
var p = a.progress; return p && !p.unreadable && p.reported ? p.reported : null;
|
|
153
|
+
}
|
|
154
|
+
function sessionRunning(a) { return observed(a) === 'running'; }
|
|
155
|
+
function inProgress(a) { return ['preparing', 'sending', 'queued', 'running'].indexOf(a.status) >= 0 || a.sending === true || sessionRunning(a); }
|
|
156
|
+
function needsAttention(a) {
|
|
157
|
+
if (a.acknowledged) return false;
|
|
158
|
+
var o = observed(a);
|
|
159
|
+
return ['unknown', 'failed', 'refused', 'delivered', 'completed'].indexOf(a.status) >= 0 || o === 'waiting' || o === 'error';
|
|
160
|
+
}
|
|
161
|
+
function handoffState(a) {
|
|
162
|
+
var status = a.status, o = observed(a);
|
|
163
|
+
if (a.acknowledged || status === 'reviewed' || status === 'canceled') return 'settled';
|
|
164
|
+
if (o === 'waiting') return 'waiting';
|
|
165
|
+
if (o === 'error') return 'attention';
|
|
166
|
+
if (o === 'running' || a.sending === true || status === 'preparing' || status === 'sending'
|
|
167
|
+
|| (status === 'running' && (a.mode === 'newSession' || a.mode === 'fork'))) return 'running';
|
|
168
|
+
if (status === 'running') return 'awaiting';
|
|
169
|
+
if (status === 'queued') return 'queued';
|
|
170
|
+
if (status === 'completed') return 'replyReady';
|
|
171
|
+
if (status === 'delivered') return sessionRow(a) ? 'replyReady' : 'sent';
|
|
172
|
+
if (needsAttention(a)) return 'attention';
|
|
173
|
+
return 'settled';
|
|
174
|
+
}
|
|
175
|
+
// A board session card: what the session reported wins while you have not handled it.
|
|
176
|
+
function cardState(a) {
|
|
177
|
+
if (!a.acknowledged) { var r = reportOf(a); if (r === 'needsInput') return 'waiting'; if (r === 'blocked') return 'attention'; }
|
|
178
|
+
return handoffState(a);
|
|
179
|
+
}
|
|
180
|
+
function activityTitle(a) {
|
|
181
|
+
if (a.acknowledged) return a.status === 'failed' || a.status === 'refused' ? 'Failure acknowledged' : 'Reviewed';
|
|
182
|
+
switch (a.status) {
|
|
183
|
+
case 'unknown': return a.sending ? 'Handoff in progress' : 'Delivery needs checking';
|
|
184
|
+
case 'failed': case 'refused': return 'Needs attention';
|
|
185
|
+
case 'completed': return 'Response ready for review';
|
|
186
|
+
case 'reviewed': return 'Reviewed';
|
|
187
|
+
case 'canceled': return 'Canceled';
|
|
188
|
+
case 'queued': return 'Queued for session';
|
|
189
|
+
default:
|
|
190
|
+
if (sessionRunning(a)) return 'Session running';
|
|
191
|
+
if (observed(a) === 'waiting') return 'Session needs your input';
|
|
192
|
+
if (observed(a) === 'error') return 'Session needs attention';
|
|
193
|
+
if (a.status === 'delivered') return 'Sent to session';
|
|
194
|
+
return inProgress(a) ? 'Handoff in progress' : (a.status ? a.status.charAt(0).toUpperCase() + a.status.slice(1) : '');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// WorkTracking: what the session reported (done, needs input, blocked) and where it stands.
|
|
198
|
+
function tracking(a) {
|
|
199
|
+
var r = reportOf(a); if (!r) return null;
|
|
200
|
+
var handled = !!a.acknowledged;
|
|
201
|
+
var label = r === 'done' ? 'Done' : handled ? 'Handled' : r === 'needsInput' ? 'Needs your input' : 'Blocked';
|
|
202
|
+
var tone = r === 'done' ? 'accent' : handled ? 'muted' : r === 'needsInput' ? 'amber' : 'danger';
|
|
203
|
+
var by = a.progress && a.progress.reportedBy;
|
|
204
|
+
return { phase: r, label: label, short: r === 'needsInput' && !handled ? 'Needs input' : label, tone: tone,
|
|
205
|
+
asks: (r === 'needsInput' || r === 'blocked') && !handled, evidence: str(a.progress && a.progress.evidence),
|
|
206
|
+
byModel: by === 'jev' || by === 'subq', provider: by === 'subq' ? 'SubQ' : 'Jev' };
|
|
207
|
+
}
|
|
208
|
+
function ageText(a, state) {
|
|
209
|
+
var sent = secondsSince(a.createdAt);
|
|
210
|
+
if (state === 'running' || state === 'awaiting' || state === 'queued' || !sessionRow(a)) return 'sent ' + shortAge(sent || 0) + ' ago';
|
|
211
|
+
var row = sessionRow(a), active = secondsSince(row.last_activity_at || row.modified);
|
|
212
|
+
if (active == null) return 'sent ' + shortAge(sent || 0) + ' ago';
|
|
213
|
+
var age = shortAge(active);
|
|
214
|
+
return age === 'now' ? 'active now' : 'active ' + age + ' ago';
|
|
215
|
+
}
|
|
216
|
+
function ageLabel(row) {
|
|
217
|
+
var st = sessionStatus(row), word = st === 'error' ? 'error' : st || 'status unavailable';
|
|
218
|
+
var s = secondsSince(row && (row.last_activity_at || row.modified));
|
|
219
|
+
return s == null ? word : word + ' · ' + shortAge(s);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── the board's items (WorkWorkspaceProjection.items) ────
|
|
223
|
+
var ITEMS = null, ITEMS_KEY = '';
|
|
224
|
+
function openByWork() {
|
|
225
|
+
var map = {};
|
|
226
|
+
(S.open || []).forEach(function (a) { if (a && a.workId) map[a.workId] = a; });
|
|
227
|
+
return map;
|
|
228
|
+
}
|
|
229
|
+
function items() {
|
|
230
|
+
var key = S.loadedAt + '|' + S.openAt;
|
|
231
|
+
if (ITEMS && ITEMS_KEY === key) return ITEMS;
|
|
232
|
+
var open = openByWork();
|
|
233
|
+
var list = (S.tasks || []).map(function (t, i) {
|
|
234
|
+
var a = open[workId(t)] || null, tr = a ? tracking(a) : null;
|
|
235
|
+
var attention = !!a && needsAttention(a) && !sessionRunning(a);
|
|
236
|
+
return {
|
|
237
|
+
id: identity(t), wid: workId(t), task: t, title: titleOf(t), domain: t.domain, offset: i, activity: a, tracking: tr,
|
|
238
|
+
stage: stageOf(t), completed: !!t.checked,
|
|
239
|
+
subtitle: t.checked ? 'Completed task' : t.agentState === 'done' ? 'Agent finished · task still open' : STAGE_TITLE[stageOf(t)],
|
|
240
|
+
needsAttention: !t.checked && (t.failed === true || t.missed === true || attention || t.agentState === 'done' || t.stage === 'review' || !!(tr && tr.asks)),
|
|
241
|
+
inProgress: t.agentState === 'running' || (!!a && inProgress(a))
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
function rank(it) { return it.completed ? 3 : it.needsAttention ? 0 : it.inProgress ? 1 : 2; }
|
|
245
|
+
function priority(it) {
|
|
246
|
+
if (it.task.workStage === 'qa') return 0;
|
|
247
|
+
if (it.task.priority === 'urgent') return 1;
|
|
248
|
+
var due = W.toDate(str(it.task.dueDate) + 'T00:00:00');
|
|
249
|
+
if (str(it.task.dueDate) && due && due.getTime() - Date.now() < 14 * 86400000) return 1;
|
|
250
|
+
return 2;
|
|
251
|
+
}
|
|
252
|
+
list.sort(function (l, r) {
|
|
253
|
+
var a = rank(l), b = rank(r); if (a !== b) return a - b;
|
|
254
|
+
a = priority(l); b = priority(r); if (a !== b) return a - b;
|
|
255
|
+
var ld = str(l.task.createdAt), rd = str(r.task.createdAt);
|
|
256
|
+
if (ld !== rd && ld && rd) return ld > rd ? -1 : 1;
|
|
257
|
+
return l.offset - r.offset;
|
|
258
|
+
});
|
|
259
|
+
ITEMS = list; ITEMS_KEY = key;
|
|
260
|
+
return list;
|
|
261
|
+
}
|
|
262
|
+
function itemFor(arg) {
|
|
263
|
+
var all = items();
|
|
264
|
+
return all.filter(function (it) { return it.id === arg; })[0] || all.filter(function (it) { return it.task.id === arg; })[0] || null;
|
|
265
|
+
}
|
|
266
|
+
function itemByWid(wid) { return items().filter(function (it) { return it.wid === wid; })[0] || null; }
|
|
267
|
+
|
|
268
|
+
// ── search (WorkSearch: every word in the title, else any word anywhere on the card) ──
|
|
269
|
+
function fold(s) { return String(s || '').normalize('NFKD').replace(/[̀-ͯ]/g, '').toLowerCase(); }
|
|
270
|
+
function tokens(s) { return fold(s).match(/[\p{L}\p{N}]+/gu) || []; }
|
|
271
|
+
function queryWords(q) { var seen = {}; return tokens(q).filter(function (w) { if (w.length < 2 || STOP[w] || seen[w]) return false; seen[w] = 1; return true; }); }
|
|
272
|
+
function found(w, toks) { for (var i = 0; i < toks.length; i++) if (toks[i].indexOf(w) === 0) return true; return false; }
|
|
273
|
+
function fieldsOf(it) {
|
|
274
|
+
if (it._fields) return it._fields;
|
|
275
|
+
var t = it.task;
|
|
276
|
+
it._fields = { title: tokens(plainTitle(it.title)),
|
|
277
|
+
other: tokens([t.text, t.title, t.source].concat((t.meetingRefs || []).map(function (m) { return m && m.title; })).filter(Boolean).join(' \n ')) };
|
|
278
|
+
return it._fields;
|
|
279
|
+
}
|
|
280
|
+
function match(it, words) {
|
|
281
|
+
if (!words.length) return null;
|
|
282
|
+
var f = fieldsOf(it);
|
|
283
|
+
if (words.every(function (w) { return found(w, f.title); })) return { kind: 'title', words: words };
|
|
284
|
+
var hit = words.filter(function (w) { return found(w, f.title) || found(w, f.other); });
|
|
285
|
+
return hit.length ? { kind: 'words', words: hit } : null;
|
|
286
|
+
}
|
|
287
|
+
function inView(it, scope, domain) {
|
|
288
|
+
if (domain && it.domain !== domain) return false;
|
|
289
|
+
if (scope === 'attention') return it.needsAttention;
|
|
290
|
+
if (scope === 'progress') return it.inProgress;
|
|
291
|
+
if (scope === 'completed') return it.completed;
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
// The view's cards narrowed by the query (Focus, Your move).
|
|
295
|
+
function visible() {
|
|
296
|
+
var words = queryWords(S.q);
|
|
297
|
+
return items().filter(function (it) { return inView(it, S.scope, S.domain) && (!words.length || match(it, words)); });
|
|
298
|
+
}
|
|
299
|
+
function searchKey() { return S.q.trim().replace(/\s+/g, ' ').toLowerCase() + '|' + S.scope + '|' + S.domain; }
|
|
300
|
+
function meaningNote(reason) {
|
|
301
|
+
var notes = { jev_breaker_open: 'Meaning search is paused for an hour', jev_cap_reached: 'Meaning search has used today’s budget',
|
|
302
|
+
too_many_candidates: 'Meaning search covers up to 254 cards. Pick a domain.', jev_not_configured: 'Meaning search needs a TypeSafe key in Settings',
|
|
303
|
+
jev_key_rejected: 'TypeSafe did not accept the saved key. Check it in Settings.', jev_request_rejected: 'Meaning search could not take this search',
|
|
304
|
+
server_too_old: 'Meaning search needs COS server 6.65', unreachable: 'Could not reach COS server. Showing word matches.',
|
|
305
|
+
jev_unavailable: 'Meaning search is temporarily unavailable. Showing word matches.',
|
|
306
|
+
subq_not_configured: 'Meaning search needs a SubQ key in Settings', subq_key_rejected: 'SubQ did not accept the saved key. Check it in Settings.' };
|
|
307
|
+
if (!reason || reason === 'search_off') return null;
|
|
308
|
+
return notes[reason] || 'Meaning search could not run. Showing word matches.';
|
|
309
|
+
}
|
|
310
|
+
// WorkSearch.result over the board's cards in board order, plus "N more in" other domains.
|
|
311
|
+
function boardSearch(cards) {
|
|
312
|
+
var words = queryWords(S.q);
|
|
313
|
+
var res = { active: words.length > 0, hits: {}, band: [], titleCount: 0, wordsCount: 0, meaningCount: 0, elsewhere: [], note: null };
|
|
314
|
+
if (!res.active) return res;
|
|
315
|
+
cards.forEach(function (it) { var h = match(it, words); if (h) res.hits[it.id] = h; });
|
|
316
|
+
var m = S.meaning && S.meaning.key === searchKey() ? S.meaning : null;
|
|
317
|
+
if (m) {
|
|
318
|
+
if (m.available) {
|
|
319
|
+
cards.forEach(function (it) {
|
|
320
|
+
var p = m.scores[it.task.id]; if (p == null) return;
|
|
321
|
+
if (res.hits[it.id]) res.hits[it.id].p = p;
|
|
322
|
+
else if (p >= 0.15) res.hits[it.id] = { kind: 'meaning', words: [], p: p };
|
|
323
|
+
});
|
|
324
|
+
} else res.note = meaningNote(m.reason);
|
|
325
|
+
}
|
|
326
|
+
Object.keys(res.hits).forEach(function (id) { res[res.hits[id].kind + 'Count']++; });
|
|
327
|
+
var pos = {}; cards.forEach(function (it, i) { pos[it.id] = i; });
|
|
328
|
+
function group(h) { return h.p != null && h.p >= 0.15 ? 0 : h.kind === 'title' ? 1 : h.kind === 'words' ? 2 : 3; }
|
|
329
|
+
res.band = Object.keys(res.hits).sort(function (a, b) {
|
|
330
|
+
var ha = res.hits[a], hb = res.hits[b], ga = group(ha), gb = group(hb);
|
|
331
|
+
if (ga !== gb) return ga - gb;
|
|
332
|
+
if (ga === 0 && ha.p !== hb.p) return (hb.p || 0) - (ha.p || 0);
|
|
333
|
+
if (ga === 2 && ha.words.length !== hb.words.length) return hb.words.length - ha.words.length;
|
|
334
|
+
return (pos[a] || 0) - (pos[b] || 0);
|
|
335
|
+
}).slice(0, 3);
|
|
336
|
+
if (S.domain) {
|
|
337
|
+
var counts = {};
|
|
338
|
+
items().forEach(function (it) { if (it.domain !== S.domain && inView(it, S.scope, '') && match(it, words)) counts[it.domain] = (counts[it.domain] || 0) + 1; });
|
|
339
|
+
res.elsewhere = Object.keys(counts).map(function (d) { return { domain: d, count: counts[d] }; })
|
|
340
|
+
.sort(function (a, b) { return a.count === b.count ? (a.domain < b.domain ? -1 : 1) : b.count - a.count; });
|
|
341
|
+
}
|
|
342
|
+
return res;
|
|
343
|
+
}
|
|
344
|
+
function kept(cards, res) { return res.active ? cards.filter(function (it) { return res.hits[it.id]; }) : cards; }
|
|
345
|
+
function countLabel(k, all, active) { return active ? k + ' of ' + all : String(all); }
|
|
346
|
+
function whyText(h) {
|
|
347
|
+
if (h.kind === 'title') return 'Title';
|
|
348
|
+
if (h.kind === 'words') return 'Words: ' + h.words.slice(0, 2).map(function (w) { return '“' + w + '”'; }).join(', ');
|
|
349
|
+
return 'Similar meaning';
|
|
350
|
+
}
|
|
351
|
+
// The title with the start of each matched word underlined in gold.
|
|
352
|
+
function underlined(title, words) {
|
|
353
|
+
var plain = plainTitle(title);
|
|
354
|
+
if (!words || !words.length) return inline(title);
|
|
355
|
+
var out = '', last = 0, re = /[\p{L}\p{N}]+/gu, m;
|
|
356
|
+
while ((m = re.exec(plain))) {
|
|
357
|
+
var token = fold(m[0]), w = words.filter(function (x) { return token.indexOf(x) === 0; })[0];
|
|
358
|
+
if (!w) continue;
|
|
359
|
+
var len = Math.min(w.length, m[0].length);
|
|
360
|
+
out += esc(plain.slice(last, m.index)) + '<span class="wk-u">' + esc(plain.slice(m.index, m.index + len)) + '</span>';
|
|
361
|
+
last = m.index + len;
|
|
362
|
+
}
|
|
363
|
+
return out + esc(plain.slice(last));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── card dates (WorkCardDating) ──────────────────────────
|
|
367
|
+
var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
368
|
+
function dayOf(text) {
|
|
369
|
+
var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(text || '')); if (!m) return null;
|
|
370
|
+
var d = new Date(+m[1], +m[2] - 1, +m[3]);
|
|
371
|
+
return d.getMonth() === +m[2] - 1 && d.getDate() === +m[3] ? d : null;
|
|
372
|
+
}
|
|
373
|
+
function firstDay(source) {
|
|
374
|
+
var label = String(source || '').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1'), re = /20\d{2}-\d{2}-\d{2}/g, m;
|
|
375
|
+
while ((m = re.exec(label))) { var d = dayOf(m[0]); if (d) return d; }
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
function cardDates(it) {
|
|
379
|
+
var t = it.task, created = null, firstSeen = false;
|
|
380
|
+
var server = dayOf(t.createdOn);
|
|
381
|
+
if (server) { created = server; firstSeen = t.createdFrom === 'git'; } else created = firstDay(t.source);
|
|
382
|
+
var moments = [W.toDate(t.lineChangedAt)];
|
|
383
|
+
var a = it.activity;
|
|
384
|
+
if (a && ['refused', 'failed', 'canceled'].indexOf(a.status) < 0) moments.push(W.toDate(a.createdAt));
|
|
385
|
+
moments = moments.filter(Boolean);
|
|
386
|
+
var newest = moments.length ? moments.reduce(function (x, y) { return x > y ? x : y; }) : null;
|
|
387
|
+
if (newest && (!created || newest >= created)) return { created: created, firstSeen: firstSeen, active: newest, activeIsDay: false };
|
|
388
|
+
return { created: created, firstSeen: firstSeen, active: created, activeIsDay: !!created };
|
|
389
|
+
}
|
|
390
|
+
function shortDay(d) { var text = MONTHS[d.getMonth()] + ' ' + d.getDate(); return d.getFullYear() === new Date().getFullYear() ? text : text + ', ' + d.getFullYear(); }
|
|
391
|
+
function dayAge(d, isDay) {
|
|
392
|
+
var s = (Date.now() - d.getTime()) / 1000;
|
|
393
|
+
if (!isDay && s < 86400) { if (s < 60) return 'now'; if (s < 3600) return Math.floor(s / 60) + ' min ago'; return Math.floor(s / 3600) + ' h ago'; }
|
|
394
|
+
var a = new Date(d.getFullYear(), d.getMonth(), d.getDate()), n = new Date(); n = new Date(n.getFullYear(), n.getMonth(), n.getDate());
|
|
395
|
+
var days = Math.round((n - a) / 86400000);
|
|
396
|
+
return days <= 0 ? 'today' : days === 1 ? 'yesterday' : days + ' days ago';
|
|
397
|
+
}
|
|
398
|
+
function dateLine(it, order) {
|
|
399
|
+
if (order === 'board') return null;
|
|
400
|
+
var d = cardDates(it);
|
|
401
|
+
if (order === 'recent') return d.active ? 'active ' + dayAge(d.active, d.activeIsDay) : 'No date';
|
|
402
|
+
return d.created ? (d.firstSeen ? 'first seen ' : 'dated ') + shortDay(d.created) : 'No date';
|
|
403
|
+
}
|
|
404
|
+
function ordered(cards, order) {
|
|
405
|
+
if (order === 'board') return cards;
|
|
406
|
+
var keyed = cards.map(function (it, i) { var d = cardDates(it); return { it: it, i: i, at: order === 'recent' ? d.active : d.created }; });
|
|
407
|
+
keyed.sort(function (l, r) {
|
|
408
|
+
if (l.at && r.at && +l.at !== +r.at) return order === 'oldest' ? l.at - r.at : r.at - l.at;
|
|
409
|
+
if (!l.at && r.at) return 1;
|
|
410
|
+
if (l.at && !r.at) return -1;
|
|
411
|
+
return l.i - r.i;
|
|
412
|
+
});
|
|
413
|
+
return keyed.map(function (k) { return k.it; });
|
|
414
|
+
}
|
|
415
|
+
function orderKey() { return (S.domain ? 'domain:' + S.domain : 'all') + '|' + S.scope; }
|
|
416
|
+
function currentOrder() { var o = S.orders[orderKey()]; return ORDERS.some(function (x) { return x[0] === o; }) ? o : 'board'; }
|
|
417
|
+
|
|
418
|
+
// ── data ─────────────────────────────────────────────────
|
|
419
|
+
function soft(p) { return p.then(function (d) { return d; }, function () { return null; }); }
|
|
420
|
+
function loadSessions() {
|
|
421
|
+
return soft(api('/api/agent-sessions?limit=80&sort=updated')).then(function (d) {
|
|
422
|
+
if (!d || !Array.isArray(d.sessions)) return;
|
|
423
|
+
var map = {};
|
|
424
|
+
d.sessions.forEach(function (s) { if (s && s.session_id) map[String(s.session_id).toLowerCase()] = s; });
|
|
425
|
+
S.sessions = map;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
function loadOpen() {
|
|
429
|
+
return api('/api/work-board/activity/open').then(function (d) {
|
|
430
|
+
S.open = (d && Array.isArray(d.items)) ? d.items : [];
|
|
431
|
+
S.openAvailable = !!(d && d.available !== false);
|
|
432
|
+
S.openCaps = (d && d.capabilities) || {};
|
|
433
|
+
S.openError = d && d.available === false ? str(d.reason) : '';
|
|
434
|
+
S.openAt = Date.now();
|
|
435
|
+
}, function (err) { S.open = S.open || []; S.openAvailable = false; S.openError = err.message || 'Session activity could not be read.'; S.openAt = Date.now(); });
|
|
436
|
+
}
|
|
437
|
+
// `user`: a tap asked for this (Refresh, a write). It paints at once. Otherwise the result goes through backgroundPaint.
|
|
438
|
+
function loadBoard(user) {
|
|
439
|
+
if (S.loading) return Promise.resolve();
|
|
440
|
+
S.loading = true;
|
|
441
|
+
if (user || !pane.querySelector('.wk')) render();
|
|
442
|
+
var board = api('/api/work-board').then(function (d) {
|
|
443
|
+
S.tasks = (d && Array.isArray(d.tasks)) ? d.tasks : [];
|
|
444
|
+
S.caps = (d && d.capabilities) || {};
|
|
445
|
+
S.complete = !!(d && d.complete === true);
|
|
446
|
+
S.boardError = '';
|
|
447
|
+
}, function (err) { S.boardError = err.message || 'Work board is unavailable. Refresh before trying again.'; S.complete = false; if (!S.tasks) S.tasks = []; });
|
|
448
|
+
var domains = S.domains ? Promise.resolve() : soft(api('/api/domains')).then(function (d) { S.domains = (d && Array.isArray(d.domains)) ? d.domains : []; });
|
|
449
|
+
// Live session state only refines the status words, so the board never waits for it.
|
|
450
|
+
loadSessions().then(function () { ITEMS = null; if (!S.loading) backgroundPaint(); });
|
|
451
|
+
return Promise.all([board, loadOpen(), domains]).then(function () {
|
|
452
|
+
S.loading = false; S.loadedAt = Date.now(); ITEMS = null; S.activityKey = activityKey();
|
|
453
|
+
if (user) render(); else backgroundPaint();
|
|
454
|
+
if (!S.intake && !S.intakeLoading) loadIntake();
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
function loadIntake(user) {
|
|
458
|
+
S.intakeLoading = true;
|
|
459
|
+
return api('/api/work-intake').then(function (d) {
|
|
460
|
+
S.intake = (d && Array.isArray(d.items)) ? d.items : [];
|
|
461
|
+
S.intakeCaps = (d && d.capabilities) || {};
|
|
462
|
+
S.intakeError = '';
|
|
463
|
+
}, function (err) { S.intakeError = err.status === 404 ? 'Intake is unavailable on this server.' : (err.message || 'Intake could not be read.'); if (!S.intake) S.intake = []; })
|
|
464
|
+
.then(function () { S.intakeLoading = false; SPLIT = null; if (user) render(); else backgroundPaint(); });
|
|
465
|
+
}
|
|
466
|
+
function refreshAll() {
|
|
467
|
+
S.meaning = null;
|
|
468
|
+
return Promise.all([loadBoard(true), loadIntake(true)]);
|
|
469
|
+
}
|
|
470
|
+
// Session activity every 30 s while Work is open, as Control rechecks its sessions on a timer.
|
|
471
|
+
var poll = null;
|
|
472
|
+
function startPoll() {
|
|
473
|
+
if (poll) return;
|
|
474
|
+
poll = setInterval(function () {
|
|
475
|
+
if (W.parseRoute().pane !== 'work' || document.hidden) return;
|
|
476
|
+
Promise.all([loadOpen(), loadSessions()]).then(function () {
|
|
477
|
+
var key = activityKey();
|
|
478
|
+
if (key === S.activityKey) return; // nothing a card shows has changed
|
|
479
|
+
S.activityKey = key; ITEMS = null; backgroundPaint();
|
|
480
|
+
});
|
|
481
|
+
}, 30000);
|
|
482
|
+
}
|
|
483
|
+
function stopPoll() { clearInterval(poll); poll = null; clearTimeout(PAINT.timer); PAINT.timer = null; }
|
|
484
|
+
// What the 30 s check can change on screen: the open handoffs and the live state of their sessions.
|
|
485
|
+
function activityKey() {
|
|
486
|
+
var parts = (S.open || []).map(function (a) {
|
|
487
|
+
var row = sessionRow(a);
|
|
488
|
+
return [a.id, a.status, a.acknowledged, a.sending, a.updatedAt, JSON.stringify(a.progress || null), row ? sessionStatus(row) + '|' + (row.display_label || '') : ''].join('~');
|
|
489
|
+
});
|
|
490
|
+
return parts.join('\n') + '#' + (S.openAvailable ? 1 : 0) + (S.openError || '');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── painting ─────────────────────────────────────────────
|
|
494
|
+
// A tap paints at once (render). Data that arrives on its own (a load finishing, the 30 s check) goes through
|
|
495
|
+
// backgroundPaint: only the list is repainted, only when its HTML changed, never while a field there has the focus,
|
|
496
|
+
// the capture form holds text or the More menu is open, and never within 1.5 s of a touch, scroll or wheel. A card,
|
|
497
|
+
// Start work or Edit is left alone unless it is still waiting for its data.
|
|
498
|
+
var PAINT = { html: '', timer: null, touched: 0 };
|
|
499
|
+
function noteTouch() { PAINT.touched = Date.now(); }
|
|
500
|
+
['touchstart', 'touchmove', 'wheel', 'pointerdown', 'keydown'].forEach(function (ev) { pane.addEventListener(ev, noteTouch, { passive: true }); });
|
|
501
|
+
document.addEventListener('scroll', function () { if (W.parseRoute().pane === 'work') noteTouch(); }, { passive: true, capture: true });
|
|
502
|
+
function deferPaint() { clearTimeout(PAINT.timer); PAINT.timer = setTimeout(backgroundPaint, 1500); }
|
|
503
|
+
function listBusy() {
|
|
504
|
+
var a = document.activeElement;
|
|
505
|
+
if (a && pane.contains(a) && /^(INPUT|TEXTAREA|SELECT)$/.test(a.tagName)) return true;
|
|
506
|
+
return S.moreOpen || (S.captureOpen && !!S.captureText.trim()) || S.captureBusy;
|
|
507
|
+
}
|
|
508
|
+
function backgroundPaint() {
|
|
509
|
+
var r = W.parseRoute();
|
|
510
|
+
if (r.pane !== 'work') return;
|
|
511
|
+
if (r.args.length) { if (S.viewWaiting) render(); return; }
|
|
512
|
+
if (listBusy() || Date.now() - PAINT.touched < 1500) return deferPaint();
|
|
513
|
+
var html = listHtml();
|
|
514
|
+
if (html !== PAINT.html) writeList(html);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ── header, tabs, filters ────────────────────────────────
|
|
518
|
+
function domainNames() {
|
|
519
|
+
var names = {};
|
|
520
|
+
(S.domains || []).forEach(function (d) { if (d && d.name) names[d.name] = 1; });
|
|
521
|
+
(S.tasks || []).forEach(function (t) { if (t.domain) names[t.domain] = 1; });
|
|
522
|
+
return Object.keys(names).sort();
|
|
523
|
+
}
|
|
524
|
+
function hero() {
|
|
525
|
+
var line = S.tasks && !S.complete && !S.loading ? 'Showing available work. Task inventory is not confirmed complete.' : 'Your decisions, connected to the work.';
|
|
526
|
+
return '<header class="cx-hero" data-tint="work"><span class="cx-glyph">' + icon('work') + '</span>' +
|
|
527
|
+
'<div class="cx-hero-text"><h1 class="cx-title">Work</h1><p class="cx-detail-line">' + esc(line) + '</p></div>' +
|
|
528
|
+
'<div class="cx-actions"><button type="button" class="cx-btn quiet" data-wk="capture-toggle" aria-expanded="' + S.captureOpen + '">Add task</button>' +
|
|
529
|
+
'<button type="button" class="cx-btn quiet wk-icon-btn" data-wk="refresh" aria-label="Refresh work" title="Refresh work"' + (S.loading ? ' disabled' : '') + '>' + icon('refresh') + '</button></div></header>';
|
|
530
|
+
}
|
|
531
|
+
function sortTitle() {
|
|
532
|
+
if (S.intakeError && !S.intake) return 'Intake · !';
|
|
533
|
+
var split = sortSplit(), n = split.count, t = split.todayCount;
|
|
534
|
+
if (t > 0) return 'Sort · ' + t + ' today' + (n > 0 ? ', ' + n + ' older' : '');
|
|
535
|
+
return n > 0 ? 'Sort · ' + n + ' older' : 'Sort';
|
|
536
|
+
}
|
|
537
|
+
function tabs() {
|
|
538
|
+
function tab(id, title, on) { return '<button type="button" data-wk="tab" data-tab="' + id + '" aria-pressed="' + (on ? 'true' : 'false') + '">' + esc(title) + '</button>'; }
|
|
539
|
+
var moreOn = S.tab === 'focus' || S.tab === 'sort' || S.tab === 'waiting';
|
|
540
|
+
var menu = S.moreOpen ? '<div class="wk-menu" role="menu">' +
|
|
541
|
+
'<button type="button" role="menuitem" data-wk="tab" data-tab="focus">Focus view</button>' +
|
|
542
|
+
(S.intake || S.intakeError ? '<button type="button" role="menuitem" data-wk="tab" data-tab="sort">' + esc(sortTitle()) + '</button>' : '') +
|
|
543
|
+
(S.caps.workBatch === 1 && S.caps.writable === true ? '<button type="button" role="menuitem" data-wk="tab" data-tab="waiting">Waiting on</button>' : '') + '</div>' : '';
|
|
544
|
+
return '<div class="cx-switch wk-tabs" role="group" aria-label="Work view">' + tab('review', 'Your move', S.tab === 'review') + tab('board', 'Board', S.tab === 'board') +
|
|
545
|
+
tab('history', 'History', S.tab === 'history') +
|
|
546
|
+
'<span class="wk-more"><button type="button" data-wk="more" aria-haspopup="menu" aria-expanded="' + S.moreOpen + '" aria-pressed="' + (moreOn ? 'true' : 'false') + '">' +
|
|
547
|
+
esc(moreOn ? (S.tab === 'focus' ? 'Focus view' : S.tab === 'sort' ? 'Sort' : 'Waiting on') : 'More') + '<span class="wk-caret"></span></button>' + menu + '</span></div>';
|
|
548
|
+
}
|
|
549
|
+
function select(name, label, options, value) {
|
|
550
|
+
return '<label class="wk-pick"><span>' + esc(label) + '</span><select class="field" data-wk-select="' + name + '">' +
|
|
551
|
+
options.map(function (o) { return '<option value="' + esc(o[0]) + '"' + (o[0] === value ? ' selected' : '') + '>' + esc(o[1]) + '</option>'; }).join('') + '</select></label>';
|
|
552
|
+
}
|
|
553
|
+
function filters() {
|
|
554
|
+
return '<div class="wk-filters"><form class="cx-search" data-wk-form="search" role="search">' +
|
|
555
|
+
'<input class="field" type="search" id="wk-q" placeholder="Search work" value="' + esc(S.q) + '" autocomplete="off" enterkeyhint="search" aria-label="Search work">' +
|
|
556
|
+
(S.q ? '<button type="button" class="cx-clear" data-wk="clear">Clear</button>' : '') + '</form>' +
|
|
557
|
+
select('domain', 'Domain', [['', 'All domains']].concat(domainNames().map(function (d) { return [d, domainName(d)]; })), S.domain) +
|
|
558
|
+
select('scope', 'Scope', SCOPES, S.scope) + '</div>';
|
|
559
|
+
}
|
|
560
|
+
function captureForm() {
|
|
561
|
+
if (!S.captureOpen) return '';
|
|
562
|
+
var names = domainNames();
|
|
563
|
+
var ok = S.captureText.trim() && names.indexOf(S.captureDomain) >= 0 && !S.captureBusy;
|
|
564
|
+
return '<form class="wk-capture" data-wk-form="capture">' +
|
|
565
|
+
'<input class="field" id="wk-capture-text" type="text" placeholder="What needs to be done?" value="' + esc(S.captureText) + '" autocomplete="off" enterkeyhint="done"' + (S.captureBusy ? ' disabled' : '') + '>' +
|
|
566
|
+
'<div class="wk-capture-row">' + (S.caps.workBatch === 1 ? '<button type="button" class="wk-text-btn" data-wk="tab" data-tab="waiting">Waiting on / Dropped</button>' : '') +
|
|
567
|
+
'<select class="field" id="wk-capture-domain" aria-label="Domain"' + (S.captureBusy ? ' disabled' : '') + '><option value=""' + (S.captureDomain ? '' : ' selected') + ' disabled>Choose domain</option>' +
|
|
568
|
+
names.map(function (d) { return '<option value="' + esc(d) + '"' + (d === S.captureDomain ? ' selected' : '') + '>' + esc(domainName(d)) + '</option>'; }).join('') + '</select>' +
|
|
569
|
+
'<button type="submit" class="cx-btn primary"' + (ok ? '' : ' disabled') + '>' + (S.captureBusy ? 'Adding…' : 'Capture') + '</button></div>' +
|
|
570
|
+
(S.captureError ? '<p class="wk-error">' + esc(S.captureError) + '</p>' : '') + '</form>';
|
|
571
|
+
}
|
|
572
|
+
function notices() {
|
|
573
|
+
var out = '';
|
|
574
|
+
if (S.note) out += '<div class="cx-notice wk-note"><span>' + esc(S.note) + '</span><button type="button" class="wk-text-btn" data-wk="dismiss-note">Dismiss</button></div>';
|
|
575
|
+
if (S.mutationError) out += '<div class="cx-notice warn wk-note"><span>' + esc(S.mutationError) + '</span><button type="button" class="wk-text-btn" data-wk="dismiss-error">Dismiss</button></div>';
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ── Your move and History (decisionQueue) ────────────────
|
|
580
|
+
function signature(a) {
|
|
581
|
+
var p = a.progress && !a.progress.unreadable ? a.progress : {};
|
|
582
|
+
return JSON.stringify([a.status || '', p.reported || '', p.evidence || '', p.reportedBy || '', a.status === 'failed' || a.status === 'refused' ? a.error || '' : '']);
|
|
583
|
+
}
|
|
584
|
+
function isHidden(a) { return S.hidden[a.id] === signature(a); }
|
|
585
|
+
function decisionCards(history) {
|
|
586
|
+
var allowed = {};
|
|
587
|
+
visible().forEach(function (it) { allowed[it.wid] = it; });
|
|
588
|
+
var cards = [];
|
|
589
|
+
(S.open || []).forEach(function (a) {
|
|
590
|
+
var it = allowed[a.workId]; if (!it) return;
|
|
591
|
+
if (!(inProgress(a) || needsAttention(a) || reportOf(a))) return;
|
|
592
|
+
if (it.completed && !inProgress(a)) return;
|
|
593
|
+
if (isHidden(a) !== history) return;
|
|
594
|
+
cards.push({ it: it, a: a, state: cardState(a) });
|
|
595
|
+
});
|
|
596
|
+
return cards.sort(function (l, r) {
|
|
597
|
+
var a = STATE_RANK[l.state], b = STATE_RANK[r.state];
|
|
598
|
+
if (a !== b) return a - b;
|
|
599
|
+
return (W.toDate(r.a.createdAt) || 0) - (W.toDate(l.a.createdAt) || 0);
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function summary(cards) {
|
|
603
|
+
var parts = SUMMARY_ORDER.map(function (o) { var n = cards.filter(function (c) { return c.state === o[0]; }).length; return n ? n + ' ' + (n === 1 ? o[1] : o[2]) : ''; }).filter(Boolean);
|
|
604
|
+
return parts.length ? parts.join(' · ') : 'None right now';
|
|
605
|
+
}
|
|
606
|
+
function decisionQueue(history) {
|
|
607
|
+
var cards = decisionCards(history), shown = S.showAllDecisions ? cards : cards.slice(0, 5);
|
|
608
|
+
var body = shown.map(function (c) {
|
|
609
|
+
var a = c.a, tr = tracking(a), result = (tr && tr.evidence) || str(a.error);
|
|
610
|
+
var action = c.state === 'attention' || a.status === 'unknown' ? 'Check delivery' : c.state === 'waiting' ? 'Answer question'
|
|
611
|
+
: (c.state === 'replyReady' || c.state === 'sent') ? 'Review result' : 'Open session';
|
|
612
|
+
var sess = splitSession(a.sessionId);
|
|
613
|
+
return '<article class="wk-decision">' +
|
|
614
|
+
'<div class="wk-decision-main"><p class="wk-state" data-tone="' + STATE_TONE[c.state] + '">' + esc(STATE_LABEL[c.state] + ' · ' + providerName(a.provider) + ' · ' + ageText(a, c.state)) + '</p>' +
|
|
615
|
+
'<button type="button" class="wk-link wk-decision-title" data-wk="open" data-id="' + esc(c.it.id) + '">' + inline(c.it.title) + '</button>' +
|
|
616
|
+
'<p class="wk-muted">' + esc(str((sessionRow(a) || {}).display_label) || str(a.sessionTitle)) + '</p>' +
|
|
617
|
+
(result ? '<p class="wk-muted wk-clamp2">' + esc(result.replace(/\s+/g, ' ').slice(0, 200)) + '</p>' : '') + '</div>' +
|
|
618
|
+
'<div class="wk-decision-actions">' +
|
|
619
|
+
(action === 'Open session' && sess ? '<button type="button" class="cx-btn quiet" data-wk="session" data-provider="' + esc(sess.provider) + '" data-sid="' + esc(sess.id) + '">Open session</button>'
|
|
620
|
+
: '<button type="button" class="cx-btn quiet" data-wk="open" data-id="' + esc(c.it.id) + '">' + esc(action) + '</button>') +
|
|
621
|
+
'<button type="button" class="wk-text-btn" data-wk="' + (history ? 'restore' : 'hide') + '" data-rid="' + esc(a.id) + '">' + (history ? 'Restore' : 'Hide notice') + '</button></div></article>';
|
|
622
|
+
}).join('');
|
|
623
|
+
var empty = !cards.length ? '<p class="wk-muted wk-pad">' + esc(S.open == null ? 'Checking session activity…' : !S.openAvailable && S.openError ? S.openError
|
|
624
|
+
: history ? 'No hidden notices in this view.' : 'Nothing needs review in this view.') + '</p>' : '';
|
|
625
|
+
return '<section class="wk-queue"><div class="wk-queue-head"><h2 class="wk-h2">' + (history ? 'Hidden notices' : 'Your move') + '</h2>' +
|
|
626
|
+
'<span class="wk-muted">' + esc(summary(cards)) + '</span>' +
|
|
627
|
+
(cards.length > 5 ? '<button type="button" class="wk-text-btn wk-push" data-wk="show-all-decisions">' + (S.showAllDecisions ? 'Show fewer' : 'Show all ' + cards.length) + '</button>' : '') + '</div>' +
|
|
628
|
+
body + empty + '<p class="wk-fine">Hiding a notice leaves its delivery warning and task stage unchanged. A new result brings it back. Hidden notices are kept in this browser.</p></section>';
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// ── board ────────────────────────────────────────────────
|
|
632
|
+
function canChangeStage(t) { return !S.busy && S.caps.version === 1 && S.caps.writable === true && !!str(t.workRevision) && !t.workMetadataError; }
|
|
633
|
+
function stageMenu(it, extraClass) {
|
|
634
|
+
var t = it.task, cur = stageOf(t), disabled = !canChangeStage(t);
|
|
635
|
+
return '<select class="wk-stage' + (extraClass ? ' ' + extraClass : '') + '" data-wk-stage="' + esc(it.id) + '" aria-label="Change task stage" title="Change task stage"' + (disabled ? ' disabled' : '') + '>' +
|
|
636
|
+
STAGES.map(function (s) {
|
|
637
|
+
var label = s === cur ? STAGE_TITLE[s] : s === 'complete' ? 'Mark complete' : 'Move to ' + STAGE_TITLE[s];
|
|
638
|
+
return '<option value="' + s + '"' + (s === cur ? ' selected disabled' : '') + '>' + esc(label) + '</option>';
|
|
639
|
+
}).join('') + '</select>';
|
|
640
|
+
}
|
|
641
|
+
function statusLine(it) {
|
|
642
|
+
var a = it.activity, tr = it.tracking;
|
|
643
|
+
if (tr) {
|
|
644
|
+
return '<p class="wk-status" data-tone="' + tr.tone + '"><span class="wk-dot' + (tr.phase === 'done' || tr.asks ? '' : ' hollow') + '"></span>' + esc(tr.label + ' · ' + str(a.sessionTitle)) + '</p>' +
|
|
645
|
+
(tr.asks && tr.evidence ? '<p class="wk-quote">“' + esc(tr.evidence) + '”</p>' : '');
|
|
646
|
+
}
|
|
647
|
+
if (a) {
|
|
648
|
+
var st = handoffState(a);
|
|
649
|
+
if (st !== 'settled') return '<p class="wk-status" data-tone="' + STATE_TONE[st] + '"><span class="wk-dot' + (st === 'running' ? ' live' : '') + '"></span>' + esc(STATE_LABEL[st] + ' · ' + str(a.sessionTitle)) + '</p>';
|
|
650
|
+
}
|
|
651
|
+
if (it.inProgress) return '<p class="wk-status" data-tone="accent">' + icon('sessions') + 'Session running</p>';
|
|
652
|
+
if (it.needsAttention) return '<p class="wk-status" data-tone="accent"><span class="wk-dot hollow"></span>Needs attention</p>';
|
|
653
|
+
return '';
|
|
654
|
+
}
|
|
655
|
+
function meetingLink(m) {
|
|
656
|
+
return '<button type="button" class="wk-meeting" data-wk="meeting" data-domain="' + esc(m.domain) + '" data-month="' + esc(m.month) + '" data-file="' + esc(m.filename) + '">' +
|
|
657
|
+
icon('meetings') + '<span>' + esc(m.title || 'Saved meeting') + '</span></button>';
|
|
658
|
+
}
|
|
659
|
+
function boardCard(it, hit, order) {
|
|
660
|
+
var t = it.task, tr = it.tracking, asking = !!(tr && tr.asks), running = !!it.activity && handoffState(it.activity) === 'running';
|
|
661
|
+
var tone = asking ? 'asking' : running ? 'running' : hit ? 'matched' : '';
|
|
662
|
+
var refs = (t.meetingRefs || []).filter(function (m) { return m && m.filename; });
|
|
663
|
+
var line = dateLine(it, order);
|
|
664
|
+
var confirm = S.pendingComplete === it.wid;
|
|
665
|
+
return '<article class="wk-card"' + (tone ? ' data-tone="' + tone + '"' : '') + '>' +
|
|
666
|
+
'<button type="button" class="wk-card-tap" data-wk="open" data-id="' + esc(it.id) + '" title="' + esc(plainTitle(it.title)) + '">' +
|
|
667
|
+
'<span class="wk-card-title">' + underlined(it.title, hit && hit.words) + '</span>' + statusLine(it) +
|
|
668
|
+
(!refs.length ? '<span class="wk-card-source">' + esc(str(t.source) || 'No meeting linked') + '</span>' : '') + '</button>' +
|
|
669
|
+
refs.slice(0, 2).map(meetingLink).join('') +
|
|
670
|
+
(line ? '<p class="wk-date">' + esc(line) + '</p>' : '') +
|
|
671
|
+
(confirm ? completeConfirm(it) : '<div class="wk-card-foot">' + stageMenu(it) +
|
|
672
|
+
(!it.completed ? '<button type="button" class="wk-text-btn" data-wk="start" data-id="' + esc(it.id) + '">Start work</button>' : '') + '</div>') +
|
|
673
|
+
'</article>';
|
|
674
|
+
}
|
|
675
|
+
function completeConfirm(it) {
|
|
676
|
+
return '<div class="wk-confirm"><p>' + esc('Mark “' + clip(plainTitle(it.title), 80) + '” complete?') + '</p>' +
|
|
677
|
+
'<div class="wk-row"><button type="button" class="cx-btn quiet" data-wk="complete-yes" data-id="' + esc(it.id) + '"' + (canChangeStage(it.task) ? '' : ' disabled') + '>Mark complete</button>' +
|
|
678
|
+
'<button type="button" class="wk-text-btn" data-wk="complete-no">Cancel</button></div></div>';
|
|
679
|
+
}
|
|
680
|
+
function columnHtml(stage, cards, all, res, order, phone) {
|
|
681
|
+
var limit = S.columnAll[stage] ? cards.length : phone ? 8 : 40;
|
|
682
|
+
var empty = !cards.length ? '<p class="wk-muted wk-pad">' + esc(!res.active ? 'No tasks here' : S.meaningPending === searchKey() ? 'Searching…'
|
|
683
|
+
: (stage === 'complete' && S.scope === 'all' && !S.domain) ? 'No word matches here' : 'No matches here') + '</p>' : '';
|
|
684
|
+
return '<section class="wk-col" data-stage="' + stage + '"><header class="wk-col-head"><div class="wk-row"><h3>' + esc(STAGE_TITLE[stage]) + '</h3>' +
|
|
685
|
+
'<span class="wk-count">' + esc(countLabel(cards.length, all.length, res.active)) + '</span></div><p>' + esc(STAGE_SUB[stage]) + '</p></header>' +
|
|
686
|
+
'<div class="wk-col-body" data-keep="col-' + stage + '">' + empty + cards.slice(0, limit).map(function (it) { return boardCard(it, res.hits[it.id], order); }).join('') +
|
|
687
|
+
(cards.length > limit ? '<button type="button" class="wk-more-row" data-wk="column-all" data-stage="' + stage + '">Show all ' + cards.length + '</button>' : '') + '</div></section>';
|
|
688
|
+
}
|
|
689
|
+
function board() {
|
|
690
|
+
var cards = items().filter(function (it) { return inView(it, S.scope, S.domain); });
|
|
691
|
+
var res = boardSearch(cards), order = currentOrder(), phone = W.isPhone();
|
|
692
|
+
var shown = kept(cards, res).length;
|
|
693
|
+
var boardName = S.domain ? domainName(S.domain) : SCOPES.filter(function (s) { return s[0] === S.scope; })[0][1];
|
|
694
|
+
var head = '<div class="wk-board-head"><div><h2 class="wk-h2">' + esc(boardName) + '</h2>' +
|
|
695
|
+
'<p class="wk-muted">' + esc(countLabel(shown, cards.length, res.active) + ' tasks · Change a card’s stage from its menu, or use Start work to put a session on it') + '</p></div>' +
|
|
696
|
+
'<div title="' + esc(ORDER_HELP[order]) + '">' + select('order', 'Order', ORDERS, order) + '</div></div>';
|
|
697
|
+
var search = '';
|
|
698
|
+
if (res.active) {
|
|
699
|
+
var n = Object.keys(res.hits).length;
|
|
700
|
+
var kinds = [[res.titleCount, 'by title'], [res.wordsCount, 'by words'], [res.meaningCount, 'by meaning']].filter(function (k) { return k[0] > 0; }).map(function (k) { return k[0] + ' ' + k[1]; }).join(' · ');
|
|
701
|
+
var more = res.elsewhere.length ? (res.elsewhere.length === 1 ? res.elsewhere[0].count + ' more in ' + domainName(res.elsewhere[0].domain)
|
|
702
|
+
: res.elsewhere.reduce(function (s, e) { return s + e.count; }, 0) + ' more in other domains') : '';
|
|
703
|
+
search = '<div class="wk-results"><div class="wk-row wk-wrap"><strong>' + esc(n ? (n === 1 ? '1 match' : n + ' matches') : S.meaningPending === searchKey() ? 'Searching by meaning…' : 'No matches on this board') + '</strong>' +
|
|
704
|
+
(n ? '<span class="wk-muted">' + esc(kinds) + '</span>' : '') + (more ? '<button type="button" class="wk-text-btn wk-push" data-wk="search-all">' + esc(more) + '</button>' : '') + '</div>' +
|
|
705
|
+
(res.band.length ? '<div class="wk-band">' + res.band.map(function (id) {
|
|
706
|
+
var it = cards.filter(function (c) { return c.id === id; })[0], h = res.hits[id]; if (!it) return '';
|
|
707
|
+
return '<button type="button" class="wk-band-row" data-wk="open" data-id="' + esc(id) + '"><span class="wk-band-stage">' + esc(STAGE_TITLE[it.stage]) + '</span>' +
|
|
708
|
+
'<span class="wk-band-title">' + underlined(it.title, h.words) + '</span><span class="wk-band-why' + (h.kind === 'meaning' ? ' meaning' : '') + '">' + esc(whyText(h)) + '</span></button>';
|
|
709
|
+
}).join('') + '</div>' : '') +
|
|
710
|
+
(res.note ? '<p class="wk-muted">' + esc(res.note) + '</p>' : '') +
|
|
711
|
+
(!S.meaning || S.meaning.key !== searchKey() ? '<p class="wk-fine">Press Return to also search by meaning.</p>' : '') + '</div>';
|
|
712
|
+
}
|
|
713
|
+
var notes = '';
|
|
714
|
+
if (S.tasks && !(S.caps.version === 1 && S.caps.writable === true)) notes += '<p class="wk-muted wk-pad-x">Board is read-only. Stage changes and meeting links need the connected Work service.</p>';
|
|
715
|
+
if (S.boardError) notes += '<div class="cx-notice warn">' + esc(S.boardError) + '</div>';
|
|
716
|
+
var mentionedAll = items().filter(function (it) { return inView(it, S.scope, S.domain) && it.stage === 'mentioned'; });
|
|
717
|
+
var mentioned = kept(mentionedAll, res);
|
|
718
|
+
var mlimit = S.mentionedAll ? mentioned.length : 30;
|
|
719
|
+
var mentionedHtml = '<div class="wk-mentioned"><button type="button" class="wk-menu-label" data-wk="mentioned" aria-expanded="' + S.mentionedOpen + '">Mentioned · ' + mentioned.length + '<span class="wk-caret"></span></button>' +
|
|
720
|
+
(S.mentionedOpen ? '<div class="cx-list wk-mentioned-list" data-keep="mentioned">' + (mentioned.length ? mentioned.slice(0, mlimit).map(function (it) {
|
|
721
|
+
return '<button type="button" class="wk-mrow" data-wk="open" data-id="' + esc(it.id) + '"><span>' + inline(it.title) + '</span><span class="chev"></span></button>';
|
|
722
|
+
}).join('') + (mentioned.length > mlimit ? '<button type="button" class="wk-more-row" data-wk="mentioned-all">Show all ' + mentioned.length + '</button>' : '')
|
|
723
|
+
: '<p class="wk-muted wk-pad">Nothing mentioned in this view.</p>') + '</div>' : '') + '</div>';
|
|
724
|
+
var columns = COLUMNS.map(function (stage) {
|
|
725
|
+
var all = cards.filter(function (it) { return it.stage === stage; });
|
|
726
|
+
return columnHtml(stage, ordered(kept(all, res), order), all, res, order, phone);
|
|
727
|
+
}).join('');
|
|
728
|
+
return '<section class="wk-board">' + head + search + notes +
|
|
729
|
+
'<button type="button" class="wk-text-btn wk-pad-x" data-wk="tab" data-tab="review">Open decisions and results</button>' + mentionedHtml +
|
|
730
|
+
'<div class="wk-columns" data-keep="columns">' + columns + '</div>' +
|
|
731
|
+
'<p class="wk-fine">Stages reflect your decisions. Moving a card does not run an agent or publish changes. Start work asks before anything is sent.</p></section>';
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ── Focus view ───────────────────────────────────────────
|
|
735
|
+
function focusView() {
|
|
736
|
+
var all = items(), progress = all.filter(function (it) { return it.inProgress; }).length, attention = all.filter(function (it) { return it.needsAttention; }).length;
|
|
737
|
+
var checked = S.openAt ? 'Sessions checked ' + (shortAge((Date.now() - S.openAt) / 1000) === 'now' ? 'just now' : shortAge((Date.now() - S.openAt) / 1000) + ' ago') : 'Session activity appears after a handoff';
|
|
738
|
+
if (S.openError) checked = S.openError;
|
|
739
|
+
var list = visible();
|
|
740
|
+
function count(text, n, scope) { return '<button type="button" class="wk-count-link" data-wk="scope-link" data-scope="' + scope + '"' + (n ? '' : ' disabled') + '>' + esc(text) + (n ? '<span class="chev"></span>' : '') + '</button>'; }
|
|
741
|
+
return '<section class="wk-focus"><div class="wk-row wk-wrap wk-focus-counts">' + count(progress + ' in progress', progress, 'progress') +
|
|
742
|
+
count(attention + ' ' + (attention === 1 ? 'needs attention' : 'need attention'), attention, 'attention') + '<span class="wk-muted wk-push">' + esc(checked) + '</span></div>' +
|
|
743
|
+
'<p class="wk-muted wk-pad-x">' + list.length + ' shown</p><div class="cx-list">' +
|
|
744
|
+
(list.length ? list.slice(0, S.columnAll.focus ? list.length : 80).map(function (it) {
|
|
745
|
+
var a = it.activity, st = a ? handoffState(a) : null;
|
|
746
|
+
return '<button type="button" class="wk-frow" data-wk="open" data-id="' + esc(it.id) + '"><span class="wk-frow-title">' + inline(it.title) + '</span>' +
|
|
747
|
+
'<span class="wk-muted">' + esc(domainName(it.domain) + ' · ' + it.subtitle) + '</span>' +
|
|
748
|
+
(a ? '<span class="wk-status" data-tone="' + STATE_TONE[st] + '"><span class="wk-dot' + (st === 'running' ? ' live' : '') + '"></span>' + esc(st === 'settled' ? activityTitle(a) : STATE_LABEL[st]) + '</span>' +
|
|
749
|
+
'<span class="wk-muted wk-clamp2">' + esc(str(a.sessionTitle)) + '</span>'
|
|
750
|
+
: it.inProgress ? '<span class="wk-status" data-tone="accent">In progress</span>' : it.needsAttention ? '<span class="wk-status" data-tone="accent"><span class="wk-dot"></span>Needs attention</span>' : '') +
|
|
751
|
+
'</button>';
|
|
752
|
+
}).join('') + (list.length > 80 && !S.columnAll.focus ? '<button type="button" class="wk-more-row" data-wk="column-all" data-stage="focus">Show all ' + list.length + '</button>' : '')
|
|
753
|
+
: '<p class="wk-muted wk-pad">' + (S.loading ? 'Loading work…' : 'No work matches this view.') + '</p>') + '</div></section>';
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ── Sort (WorkIntakeView with the Today home on: meetings before today; today's stay on Today) ──
|
|
757
|
+
var SPLIT = null, SPLIT_KEY = '';
|
|
758
|
+
function sortSplit() {
|
|
759
|
+
var today = localDay(), key = today + '|' + S.loadedAt + '|' + (S.intake ? S.intake.length : -1) + '|' + Object.keys(S.intakeBusy).length + '|' + S.domain;
|
|
760
|
+
if (SPLIT && SPLIT_KEY === key) return SPLIT;
|
|
761
|
+
var open = (S.intake || []).filter(function (it) {
|
|
762
|
+
return it && /^wi_[a-f0-9]{32}$/.test(it.id) && ['suggested', 'review', 'ask'].indexOf(it.status) >= 0 && it.meeting && !it._done
|
|
763
|
+
&& (it.kind === 'link' ? it.task && it.task.text : it.text);
|
|
764
|
+
});
|
|
765
|
+
// TodayHome.intakeSplit: an item is today's when its meeting file is dated today; a mentioned card when its first
|
|
766
|
+
// linked meeting is. Everything else is older.
|
|
767
|
+
function isToday(it) { return String(it.meeting.filename || '').slice(0, 10) === today; }
|
|
768
|
+
function cardToday(t) { var ref = (t.meetingRefs || [])[0]; return !!ref && String(ref.filename || '').slice(0, 10) === today; }
|
|
769
|
+
var mentioned = (S.tasks || []).filter(function (t) { return t.workStage === 'mentioned' && !t.checked; });
|
|
770
|
+
var older = open.filter(function (it) { return !isToday(it); }), cards = mentioned.filter(function (t) { return !cardToday(t); });
|
|
771
|
+
var todayItems = open.filter(isToday).sort(function (a, b) { var x = a.meeting.filename + '|' + a.id, y = b.meeting.filename + '|' + b.id; return x < y ? -1 : x > y ? 1 : 0; });
|
|
772
|
+
var todayCards = mentioned.filter(cardToday);
|
|
773
|
+
SPLIT = { items: older, cards: cards, count: older.length + cards.length, todayItems: todayItems, todayCards: todayCards, todayCount: todayItems.length + todayCards.length };
|
|
774
|
+
SPLIT_KEY = key;
|
|
775
|
+
return SPLIT;
|
|
776
|
+
}
|
|
777
|
+
function sortGroups() {
|
|
778
|
+
var split = sortSplit(), groups = {};
|
|
779
|
+
split.items.filter(function (it) { return !S.domain || it.meeting.domain === S.domain; }).forEach(function (it) {
|
|
780
|
+
var k = it.meeting.recordId;
|
|
781
|
+
var g = groups[k] || (groups[k] = { id: k, title: it.meeting.title || 'Saved meeting', date: String(it.meeting.filename || '').slice(0, 10), items: [], cards: [] });
|
|
782
|
+
g.items.push(it);
|
|
783
|
+
});
|
|
784
|
+
split.cards.filter(function (t) { return !S.domain || t.domain === S.domain; }).forEach(function (t) {
|
|
785
|
+
var ref = (t.meetingRefs || [])[0], k = ref ? ref.recordId : 'self-captured';
|
|
786
|
+
var g = groups[k] || (groups[k] = { id: k, title: ref ? ref.title || 'Saved meeting' : 'Your captures', date: ref ? String(ref.filename || '').slice(0, 10) : str(t.createdAt), items: [], cards: [] });
|
|
787
|
+
g.cards.push(t);
|
|
788
|
+
});
|
|
789
|
+
var list = Object.keys(groups).map(function (k) { return groups[k]; }).sort(function (a, b) {
|
|
790
|
+
if ((a.id === 'self-captured') !== (b.id === 'self-captured')) return a.id === 'self-captured' ? -1 : 1;
|
|
791
|
+
return a.date === b.date ? (a.id < b.id ? -1 : 1) : (a.date > b.date ? -1 : 1);
|
|
792
|
+
});
|
|
793
|
+
// Today's meetings lead, as one group (the web Today home links here instead of listing them itself).
|
|
794
|
+
var ti = split.todayItems.filter(function (it) { return !S.domain || it.meeting.domain === S.domain; });
|
|
795
|
+
var tc = split.todayCards.filter(function (t) { return !S.domain || t.domain === S.domain; });
|
|
796
|
+
if (ti.length || tc.length) list.unshift({ id: 'today', title: 'From today\u2019s meetings', note: 'Joins the older groups at midnight', date: localDay(), items: ti, cards: tc });
|
|
797
|
+
return list;
|
|
798
|
+
}
|
|
799
|
+
function meetingDate(m) { var p = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(m.filename || '')); return p ? (+p[2]) + '/' + (+p[3]) : ''; }
|
|
800
|
+
var REASON = { outside_window: 'older meeting', unclear_task: 'may not be a task', owner_uncertain: 'owner unclear' };
|
|
801
|
+
function intakeRow(it) {
|
|
802
|
+
var busy = !!S.intakeBusy[it.id], link = it.kind === 'link' && it.status === 'suggested', ask = it.kind === 'ask' && it.status === 'ask';
|
|
803
|
+
var text = link ? it.task.text : it.text;
|
|
804
|
+
var detail = link ? (typeof it.confidence === 'number' && isFinite(it.confidence) ? Math.round(it.confidence * 100) + '%' : '') : ask ? str(it.owner) : (REASON[it.reason] || (it.ownerIsMiles ? 'probably yours' : str(it.owner)));
|
|
805
|
+
var accept = link ? 'Link' : ask ? 'Make it mine' : 'Keep as card', dismiss = link || ask ? 'Dismiss' : 'Drop';
|
|
806
|
+
var enabled = link ? S.intakeCaps.linkWrites === true : S.intakeCaps.cardCreation === true;
|
|
807
|
+
var evidence = '';
|
|
808
|
+
if (!link && !ask && it.askEvidence && typeof it.askEvidence === 'object') {
|
|
809
|
+
var e = it.askEvidence, lines = [];
|
|
810
|
+
if (e.question) lines.push('You asked: ' + e.question);
|
|
811
|
+
if (e.answer) lines.push('Original answer: ' + e.answer + (e.answerTruncated ? '\nRetained answer excerpt; open the meeting for the full answer.' : ''));
|
|
812
|
+
(Array.isArray(e.sources) ? e.sources : []).forEach(function (s) { if (s) lines.push('[' + str(s.id) + '] ' + str(s.title) + '\n' + str(s.text) + (s.truncated ? '\nRetained source excerpt.' : '')); });
|
|
813
|
+
evidence = '<details class="wk-evidence"><summary>Original answer and sources</summary><p>' + esc(lines.join('\n\n')) + '</p></details>';
|
|
814
|
+
}
|
|
815
|
+
var pull = ask && it.pull && it.pull.title ? '<p class="wk-status" data-tone="accent">' + esc('Your session may already cover this' + (typeof it.pull.p === 'number' ? ' (' + Math.round(it.pull.p * 100) + '%)' : '') + ': ' + it.pull.title) + '</p>' : '';
|
|
816
|
+
return '<div class="wk-intake-row"><p class="wk-intake-text' + (link ? ' wk-clamp3' : '') + '">' + inline(text) + '</p>' + pull + evidence +
|
|
817
|
+
'<div class="wk-intake-foot"><span class="wk-intake-meta"><button type="button" class="wk-meeting" data-wk="meeting" data-domain="' + esc(it.meeting.domain) + '" data-month="' + esc(it.meeting.month) + '" data-file="' + esc(it.meeting.filename) + '">' +
|
|
818
|
+
icon('meetings') + '<span>' + esc((it.meeting.title || 'Saved meeting') + (meetingDate(it.meeting) ? ' · ' + meetingDate(it.meeting) : '')) + '</span></button>' +
|
|
819
|
+
(detail ? '<span class="wk-muted">· ' + esc(detail) + '</span>' : '') + '</span>' +
|
|
820
|
+
'<span class="wk-row">' + (busy ? '<span class="wk-muted">Saving…</span>' : '') +
|
|
821
|
+
'<button type="button" class="cx-btn quiet" data-wk="resolve" data-action-kind="accept" data-iid="' + esc(it.id) + '"' + (busy || !enabled ? ' disabled' : '') +
|
|
822
|
+
(enabled ? '' : ' title="' + esc(link ? 'This server can’t write Work links.' : 'This server can’t create Work cards yet.') + '"') + '>' + esc(accept) + '</button>' +
|
|
823
|
+
'<button type="button" class="wk-text-btn" data-wk="resolve" data-action-kind="dismiss" data-iid="' + esc(it.id) + '"' + (busy ? ' disabled' : '') + '>' + esc(dismiss) + '</button></span></div></div>';
|
|
824
|
+
}
|
|
825
|
+
function sortView() {
|
|
826
|
+
var groups = sortGroups(), n = groups.length;
|
|
827
|
+
// Stay on the group the reader was on when a background load adds or removes one above it (today's group leads).
|
|
828
|
+
if (S.sortGroupId && (!groups[S.sortIndex] || groups[S.sortIndex].id !== S.sortGroupId)) {
|
|
829
|
+
for (var gi = 0; gi < n; gi++) if (groups[gi].id === S.sortGroupId) { S.sortIndex = gi; break; }
|
|
830
|
+
}
|
|
831
|
+
if (S.sortIndex > n - 1) S.sortIndex = Math.max(0, n - 1);
|
|
832
|
+
var g = n ? groups[S.sortIndex] : null, batch = S.caps.workBatch === 1 && S.caps.writable === true;
|
|
833
|
+
S.sortGroupId = g ? g.id : '';
|
|
834
|
+
var undo = S.undoBatch ? (S.loopAvailable === true ? '<button type="button" class="wk-text-btn" data-wk="undo">Undo last decision</button>'
|
|
835
|
+
: S.loopAvailable === false ? '<span class="wk-muted">' + esc(UNDO_MISSING) + '</span>' : '') : '';
|
|
836
|
+
var body;
|
|
837
|
+
if (S.intakeError && !(S.intake && S.intake.length)) body = '<p class="wk-muted">' + esc(S.intakeError) + '</p>';
|
|
838
|
+
else if (!S.intake) body = '<p class="wk-muted">Loading Intake…</p>';
|
|
839
|
+
else if (!g) body = '<p class="wk-muted">Nothing to sort</p>';
|
|
840
|
+
else {
|
|
841
|
+
body = '<div class="wk-row wk-sort-group"><h3>' + esc(g.title) + '</h3><span class="wk-muted wk-push">' + (S.sortIndex + 1) + ' of ' + n + '</span></div>' +
|
|
842
|
+
(g.note ? '<p class="wk-muted wk-sort-note">' + esc(g.note) + '</p>' : '') +
|
|
843
|
+
'<p class="wk-fine wk-keys">j / k: next / previous meeting · s: decide later · e: keep first item</p>' +
|
|
844
|
+
g.cards.map(function (t) {
|
|
845
|
+
var meta = [str(t.createdAt), str(t.owner), str(t.dueDate) ? 'Due ' + t.dueDate : ''].filter(Boolean).join(' · ');
|
|
846
|
+
return '<div class="wk-sort-card"><p class="wk-intake-text">' + inline(titleOf(t)) + '</p>' + (meta ? '<p class="wk-muted">' + esc(meta) + '</p>' : '') +
|
|
847
|
+
'<div class="wk-row"><button type="button" class="cx-btn quiet" data-wk="card-keep" data-id="' + esc(identity(t)) + '"' + (batch && !S.batchBusy ? '' : ' disabled') + '>Keep</button>' +
|
|
848
|
+
'<button type="button" class="cx-btn quiet" data-wk="card-drop" data-id="' + esc(identity(t)) + '"' + (batch && !S.batchBusy ? '' : ' disabled') + '>Drop</button>' +
|
|
849
|
+
'<button type="button" class="wk-text-btn" data-wk="open" data-id="' + esc(identity(t)) + '">Open card</button></div></div>';
|
|
850
|
+
}).join('') + (g.cards.length && !batch ? '<p class="wk-fine">Keep and Drop need a COS server with Work batch changes.</p>' : '') +
|
|
851
|
+
(g.items.length ? '<div class="wk-items"><div class="wk-row"><h4>Items to decide</h4><span class="wk-muted">' + g.items.length + '</span></div>' +
|
|
852
|
+
'<p class="wk-muted">Keep what you own. Other people’s asks can become cards when you take them on.</p>' +
|
|
853
|
+
'<div class="wk-items-list">' + g.items.map(intakeRow).join('') + '</div></div>' : '') +
|
|
854
|
+
'<div class="wk-row wk-sort-nav"><button type="button" class="wk-text-btn" data-wk="sort-prev"' + (S.sortIndex === 0 ? ' disabled' : '') + '>Previous</button>' +
|
|
855
|
+
'<button type="button" class="wk-text-btn" data-wk="sort-later">Decide later</button>' +
|
|
856
|
+
(g.items.length ? '<button type="button" class="wk-text-btn wk-push" data-wk="dismiss-rest"' + (Object.keys(S.intakeBusy).length ? ' disabled' : '') + '>Dismiss remaining items</button>' : '') + '</div>';
|
|
857
|
+
}
|
|
858
|
+
return '<section class="wk-sort"><div class="wk-row wk-wrap wk-sort-head"><h2 class="wk-h2">Sort</h2>' +
|
|
859
|
+
'<span class="wk-muted">Follow-ups from your meetings, today’s first.</span>' +
|
|
860
|
+
'<button type="button" class="cx-btn quiet wk-icon-btn wk-push" data-wk="intake-refresh" aria-label="Refresh Intake" title="Refresh Intake">' + icon('refresh') + '</button></div>' +
|
|
861
|
+
(undo ? '<div class="wk-row wk-undo">' + undo + '</div>' : '') +
|
|
862
|
+
(S.sortError ? '<p class="wk-error">' + esc(S.sortError) + '</p>' : '') + '<div class="wk-sort-body">' + body + '</div></section>';
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ── Waiting on (shown only on a server with Work batch changes, as in Control) ──
|
|
866
|
+
function waitingView() {
|
|
867
|
+
var list = S.waitingDropped ? S.dropped : S.waiting;
|
|
868
|
+
var rows = (list || []).slice().sort(function (a, b) {
|
|
869
|
+
var x = (str(a.owner).toLowerCase() + '|' + str(a.checkIn)), y = (str(b.owner).toLowerCase() + '|' + str(b.checkIn)); return x < y ? -1 : x > y ? 1 : 0;
|
|
870
|
+
}).map(function (t) {
|
|
871
|
+
if (S.waitingDropped) return '<div class="wk-sort-card wk-row"><span class="wk-grow">' + inline(titleOf(t)) + '</span><button type="button" class="cx-btn quiet" data-wk="batch" data-op="restore" data-domain="' + esc(t.domain) + '" data-id="' + esc(t.id) + '"' + (S.batchBusy ? ' disabled' : '') + '>Restore</button></div>';
|
|
872
|
+
return '<div class="wk-sort-card"><strong>' + esc(str(t.owner) || 'Owner unknown') + '</strong><p>' + inline(titleOf(t)) + '</p>' +
|
|
873
|
+
'<p class="wk-muted">' + esc(t.needsCheckIn ? 'Still waiting?' : str(t.checkIn) ? 'Check in ' + t.checkIn : 'Choose a check-in date') + '</p>' +
|
|
874
|
+
'<div class="wk-row wk-wrap"><button type="button" class="cx-btn quiet" data-wk="batch" data-op="done" data-domain="' + esc(t.domain) + '" data-id="' + esc(t.id) + '">Done</button>' +
|
|
875
|
+
'<label class="wk-pick"><span>Next check-in</span><input class="field" type="date" data-wk-checkin="' + esc(t.id) + '"></label>' +
|
|
876
|
+
'<button type="button" class="cx-btn quiet" data-wk="batch" data-op="checkIn" data-domain="' + esc(t.domain) + '" data-id="' + esc(t.id) + '">Still waiting</button>' +
|
|
877
|
+
'<button type="button" class="wk-text-btn" data-wk="batch" data-op="drop" data-domain="' + esc(t.domain) + '" data-id="' + esc(t.id) + '">Drop</button></div></div>';
|
|
878
|
+
}).join('');
|
|
879
|
+
return '<section class="wk-sort"><div class="wk-row wk-sort-head"><h2 class="wk-h2">Waiting on</h2><button type="button" class="cx-btn quiet wk-push" data-wk="waiting-refresh">Refresh</button></div>' +
|
|
880
|
+
'<div class="cx-switch"><button type="button" data-wk="waiting-mode" data-mode="waiting" aria-pressed="' + !S.waitingDropped + '">Waiting on</button>' +
|
|
881
|
+
'<button type="button" data-wk="waiting-mode" data-mode="dropped" aria-pressed="' + S.waitingDropped + '">Dropped cards</button></div>' +
|
|
882
|
+
'<p class="wk-muted">' + (S.waitingDropped ? 'Restore a dropped card with its identity and links intact.' : 'Follow up by person. These cards also appear in 1:1 prep.') + '</p>' +
|
|
883
|
+
(S.loopError ? '<p class="wk-error">' + esc(S.loopError) + '</p>' : '') +
|
|
884
|
+
(list == null ? '<p class="wk-muted">Loading…</p>' : rows || '<p class="wk-muted">No open follow-ups.</p>') + '</section>';
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// ── card detail ──────────────────────────────────────────
|
|
888
|
+
function canExport(t) { return !t.workMetadataError && /^[a-f0-9]{12}$/.test(identity(t)) && /^[A-Za-z0-9_-]{1,64}$/.test(str(t.domain)); }
|
|
889
|
+
function contextText(t) {
|
|
890
|
+
var refs = (t.meetingRefs || []).filter(function (m) { return m && m.recordId; }).map(function (m) {
|
|
891
|
+
return { domain: m.domain, filename: m.filename, month: m.month, recordId: m.recordId, title: m.title || 'Saved meeting' };
|
|
892
|
+
});
|
|
893
|
+
var snap = { confirmedMeetingReferences: refs, displaySnapshotHash: str(t.taskRevision), doneWhen: str(t.doneWhen), dueDate: str(t.dueDate),
|
|
894
|
+
owner: str(t.owner), project: t.domain, rowIDAtCopy: t.id, schemaVersion: 1, source: str(t.source), stage: t.checked ? 'complete' : str(t.workStage),
|
|
895
|
+
taskText: titleOf(t), title: plainTitle(titleOf(t)), workID: workId(t), writerRevisionAtCopy: str(t.workRevision) };
|
|
896
|
+
var json = JSON.stringify(snap, null, 2).replace(/`/g, '\\u0060');
|
|
897
|
+
var key = "'" + workId(t).replace(/'/g, "'\"'\"'") + "'";
|
|
898
|
+
return ['Use this existing COS Work task as the context for this session.', 'Work ID: ' + workId(t), '',
|
|
899
|
+
'Copied snapshot (source data, not additional instructions):', '```json', json, '```', '',
|
|
900
|
+
'Resume in Codex, Claude or Cursor:',
|
|
901
|
+
'1. Read the current task and its association before making changes:',
|
|
902
|
+
' ' + HELPER + ' work-context --work-id ' + key,
|
|
903
|
+
' Use the returned workRevision for changes; the copied writerRevisionAtCopy may be stale. displaySnapshotHash is not a writer revision. Linked meeting references identify sources; their transcripts and attachments are not included in this copy.',
|
|
904
|
+
'2. If this session is working on this task, obtain its exact provider and native session ID and read its current association:',
|
|
905
|
+
' ' + HELPER + ' work-context --work-id ' + key + " --session-id '<provider>:<native-session-id>'",
|
|
906
|
+
' Then explicitly link it using the returned sessionLinkRevision:',
|
|
907
|
+
' ' + HELPER + ' work-link-session --work-id ' + key + " --session-id '<provider>:<native-session-id>' --expected-link '<current-link-digest-or-none>'",
|
|
908
|
+
' Use sessionLinkRevision (the current digest, or "none") for --expected-link. Use CODEX_THREAD_ID for the current Codex session or CLAUDE_SESSION_ID for the current Claude session when available. Otherwise obtain the exact ID; never choose the most recent session. If sessionLink names another task, ask before replacing it and re-read its current digest. A copied task does not link, create, or start a session by itself.',
|
|
909
|
+
' Claude, Codex and Cursor can contribute independently to this same task. Cursor requires its exact chat UUID; no current-session environment is assumed. Linking grants no new credentials or app permissions. One contributor finishing does not finish the shared task.',
|
|
910
|
+
' To remove only this explicit association, re-read its digest and use:',
|
|
911
|
+
' ' + HELPER + ' work-unlink-session --work-id ' + key + " --session-id '<provider>:<native-session-id>' --expected-link '<fresh-sessionLinkRevision>'",
|
|
912
|
+
' The native session and historical receipts remain.',
|
|
913
|
+
'3. When the user requests a board change, re-read work-context, then use the current workRevision:',
|
|
914
|
+
' ' + HELPER + ' work-set-stage --work-id ' + key + " --stage '<requested-stage>' --expected-revision '<fresh-workRevision>'",
|
|
915
|
+
' Stages: mentioned, planned, draft, built, qa, complete. On a revision conflict, re-read and report the change; do not overwrite it with an automatic retry. Move to Complete only when the user asks to mark this task complete.',
|
|
916
|
+
'', 'These commands require COS Control with Work context support on this Mac. If unavailable, report that instead of editing task files or guessing identifiers. Work in this session does not authorize publishing or sending externally.'].join('\n');
|
|
917
|
+
}
|
|
918
|
+
function backBar(label) {
|
|
919
|
+
return '<div class="wk-backbar"><button type="button" class="cx-btn quiet" data-wk="back">' + icon('back') + esc(label) + '</button></div>';
|
|
920
|
+
}
|
|
921
|
+
function backLabel() {
|
|
922
|
+
if (S.tab === 'review') return 'Back to your move';
|
|
923
|
+
if (S.tab === 'history') return 'Back to history';
|
|
924
|
+
if (S.tab === 'sort') return 'Back to Sort';
|
|
925
|
+
if (S.tab === 'focus') return 'Back to ' + (S.domain ? domainName(S.domain) : SCOPES.filter(function (s) { return s[0] === S.scope; })[0][1]);
|
|
926
|
+
return S.domain ? 'Back to ' + domainName(S.domain) + ' board' : 'Back to the board';
|
|
927
|
+
}
|
|
928
|
+
function loadActivity(it, force) {
|
|
929
|
+
if (!force && S.activity[it.wid]) return Promise.resolve();
|
|
930
|
+
return api('/api/work-board/activity?domain=' + encodeURIComponent(it.domain) + '&workIdentity=' + encodeURIComponent(it.id)).then(function (d) {
|
|
931
|
+
S.activity[it.wid] = d || { activities: [] }; delete S.activityError[it.wid];
|
|
932
|
+
}, function (err) { S.activityError[it.wid] = err.message || 'Session activity could not be read.'; });
|
|
933
|
+
}
|
|
934
|
+
// Connections: one row per session that worked on this card, its newest handoff first.
|
|
935
|
+
function contributors(it) {
|
|
936
|
+
var acts = ((S.activity[it.wid] || {}).activities || []).slice().sort(function (a, b) { return (W.toDate(b.createdAt) || 0) - (W.toDate(a.createdAt) || 0); });
|
|
937
|
+
var seen = {}, out = [];
|
|
938
|
+
acts.forEach(function (a) {
|
|
939
|
+
var key = a.sessionId ? String(a.sessionId).toLowerCase() : 'receipt:' + a.id;
|
|
940
|
+
if (seen[key]) return; seen[key] = 1; out.push(a);
|
|
941
|
+
});
|
|
942
|
+
return out;
|
|
943
|
+
}
|
|
944
|
+
function connections(it) {
|
|
945
|
+
var t = it.task, data = S.activity[it.wid], err = S.activityError[it.wid], rows = contributors(it);
|
|
946
|
+
var known = !!data || !!err;
|
|
947
|
+
var sessions = rows.map(function (a) {
|
|
948
|
+
var row = sessionRow(a), tr = tracking(a), sess = splitSession(a.sessionId);
|
|
949
|
+
return '<div class="wk-contrib"><div class="wk-row wk-top"><span class="wk-muted">' + esc(providerName(a.provider)) + '</span>' +
|
|
950
|
+
'<strong class="wk-grow wk-clamp2">' + esc(str((row || {}).display_label) || str(a.sessionTitle) || 'Session') + '</strong>' +
|
|
951
|
+
'<span class="wk-mono">' + esc(row ? ageLabel(row) : 'Live status unavailable') + '</span></div>' +
|
|
952
|
+
(tr ? '<p class="wk-status" data-tone="' + tr.tone + '">' + esc((tr.byModel ? tr.provider + ' assessment: ' : 'Session reported: ') + tr.label) + '</p>' +
|
|
953
|
+
(tr.evidence ? '<p class="wk-small">' + esc(tr.evidence.slice(0, 240)) + '</p>' : '')
|
|
954
|
+
: '<p class="wk-muted">' + esc(activityTitle(a)) + '</p>') +
|
|
955
|
+
(['failed', 'refused', 'canceled'].indexOf(a.status) >= 0 ? '<p class="wk-small"><strong>' + esc('Latest handoff: ' + a.status) + '</strong></p>' +
|
|
956
|
+
(a.error ? '<p class="wk-small wk-clamp3">' + esc(String(a.error).slice(0, 240)) + '</p>' : '') : '') +
|
|
957
|
+
'<div class="wk-row wk-wrap">' + (sess ? '<button type="button" class="wk-text-btn" data-wk="session" data-provider="' + esc(sess.provider) + '" data-sid="' + esc(sess.id) + '">' +
|
|
958
|
+
esc(sess.provider === 'cursor' ? 'Open Cursor Agents' : 'Open ' + providerName(sess.provider)) + '</button>' +
|
|
959
|
+
'<button type="button" class="wk-text-btn" data-wk="copy-session" data-sid="' + esc(a.sessionId) + '">Copy session ID</button>' : '') +
|
|
960
|
+
'<span class="wk-fine">Receipt history</span></div></div>';
|
|
961
|
+
}).join('');
|
|
962
|
+
var refs = (t.meetingRefs || []).filter(function (m) { return m && m.filename; });
|
|
963
|
+
return '<section class="wk-section"><h2 class="wk-h2">Connections</h2>' +
|
|
964
|
+
'<div class="wk-row"><strong>Sessions</strong><span class="wk-muted">' + rows.length + ' sessions</span></div>' +
|
|
965
|
+
'<p class="wk-muted">' + esc(err ? err : !known ? 'Loading sessions…' : 'Each session keeps its own progress. You control the shared task’s stage.') + '</p>' +
|
|
966
|
+
(known && !rows.length && !err ? '<p class="wk-muted">Copy context into Claude, Codex or Cursor, then link its exact session ID.</p>' : '') + sessions +
|
|
967
|
+
'<p class="wk-fine">Linking an existing session to this card is done in COS Control on your Mac.</p>' +
|
|
968
|
+
'<div class="wk-sub"><strong>Source meetings</strong>' +
|
|
969
|
+
(refs.length ? refs.map(meetingLink).join('') : '<p class="wk-muted">No confirmed meeting link. Attach a saved meeting to connect its tasks and sessions.</p>') + '</div></section>';
|
|
970
|
+
}
|
|
971
|
+
function workspace(it) {
|
|
972
|
+
var data = S.activity[it.wid], rows = contributors(it), latest = rows[0];
|
|
973
|
+
var status = '';
|
|
974
|
+
if (latest) {
|
|
975
|
+
var st = cardState(latest), tr = tracking(latest);
|
|
976
|
+
var line = tr && tr.phase === 'done' ? (tr.byModel ? tr.provider + ' read the session’s reply as done.' : 'The session reported this done.') + ' Check the result, then mark it complete when you agree.'
|
|
977
|
+
: tr && tr.phase === 'blocked' ? 'The session is blocked on this.' : tr ? 'The session needs you before it can finish this.' : '';
|
|
978
|
+
status = '<div class="wk-handoff" data-tone="' + STATE_TONE[st] + '"><p class="wk-state" data-tone="' + STATE_TONE[st] + '">' + esc(STATE_LABEL[st] + ' · ' + providerName(latest.provider) + ' · ' + ageText(latest, st)) + '</p>' +
|
|
979
|
+
'<p><strong>' + esc(str(latest.sessionTitle) || 'Session') + '</strong></p>' + (line ? '<p class="wk-small">' + esc(line) + '</p>' : '') +
|
|
980
|
+
(tr && tr.evidence ? '<p class="wk-quote">“' + esc(tr.evidence) + '”</p>' : '') + '</div>';
|
|
981
|
+
}
|
|
982
|
+
var reply = latest ? replyBox(it, latest) : '';
|
|
983
|
+
return '<section class="wk-section" id="wk-agent-workspace"><h2 class="wk-h2">Agent workspace</h2>' + status + reply +
|
|
984
|
+
(!it.completed ? '<button type="button" class="cx-btn primary" data-wk="start" data-id="' + esc(it.id) + '">Start work</button>' : '') +
|
|
985
|
+
(data && data.savedDestination ? '<p class="wk-fine">' + esc('Saved destination: ' + destinationLabel(data.savedDestination)) + '</p>' : '') +
|
|
986
|
+
'<p class="wk-fine">Card files and Work chat stay on your Mac. Add or send files from this card in COS Control.</p></section>';
|
|
987
|
+
}
|
|
988
|
+
// Answer (needs input or blocked) and Not done yet (a reported done): a reply request that continues the same session
|
|
989
|
+
// with a note, naming the handoff it answers. COS Control on the Mac sends it, as it does for the glasses.
|
|
990
|
+
function replyBox(it, a) {
|
|
991
|
+
var tr = tracking(a), sess = splitSession(a.sessionId);
|
|
992
|
+
if (!tr || a.acknowledged || !sess) return '';
|
|
993
|
+
var intent = tr.asks ? 'reply' : tr.phase === 'done' ? 'notDone' : '';
|
|
994
|
+
if (!intent) return '';
|
|
995
|
+
var r = S.reply[it.wid] || (S.reply[it.wid] = { open: intent === 'reply', text: '', sending: false, request: null, error: '' });
|
|
996
|
+
var title = clip(str(a.sessionTitle) || 'session', 40);
|
|
997
|
+
if (r.request) {
|
|
998
|
+
var req = r.request, done = ['sent', 'refused', 'unresolved', 'unconfirmed', 'expired'].indexOf(req.state) >= 0;
|
|
999
|
+
return '<div class="wk-handoff" data-tone="' + (req.state === 'sent' ? 'green' : req.state === 'refused' || req.state === 'expired' ? 'danger' : 'accent') + '"><p class="wk-state">' +
|
|
1000
|
+
esc(REQUEST_LINE[req.state] || req.state) + '</p>' + (req.reason ? '<p class="wk-small">' + esc(req.reason) + '</p>' : '') +
|
|
1001
|
+
(r.pollStopped ? '<p class="wk-small wk-muted">' + esc(r.pollStopped) + '</p>' : done ? '' : '<p class="wk-fine">This page checks every few seconds.</p>') +
|
|
1002
|
+
(done || r.pollStopped ? '<div class="wk-row"><button type="button" class="wk-text-btn" data-wk="reply-reset" data-id="' + esc(it.id) + '">Dismiss</button></div>' : '') + '</div>';
|
|
1003
|
+
}
|
|
1004
|
+
if (!r.open) return '<button type="button" class="cx-btn quiet" data-wk="reply-open" data-id="' + esc(it.id) + '">Not done yet</button>';
|
|
1005
|
+
var limit = r.text.trim().length > 2000;
|
|
1006
|
+
return '<div class="wk-reply"><label class="wk-field"><span class="wk-label">' + (intent === 'reply' ? 'Your answer' : 'What’s missing?') + '</span>' +
|
|
1007
|
+
'<textarea class="field" rows="3" data-wk-reply="' + esc(it.id) + '" maxlength="2000"' + (r.sending ? ' disabled' : '') + '>' + esc(r.text) + '</textarea></label>' +
|
|
1008
|
+
'<p class="wk-small wk-muted">' + esc('Continues “' + title + '” with this note.') + '</p>' +
|
|
1009
|
+
(r.error ? '<p class="wk-error">' + esc(r.error) + '</p>' : '') +
|
|
1010
|
+
'<div class="wk-row">' + (intent === 'notDone' ? '<button type="button" class="wk-text-btn" data-wk="reply-cancel" data-id="' + esc(it.id) + '">Cancel</button>' : '') +
|
|
1011
|
+
'<button type="button" class="cx-btn primary" data-wk="reply-send" data-id="' + esc(it.id) + '" data-intent="' + intent + '"' + (!r.text.trim() || limit || r.sending ? ' disabled' : '') + '>' +
|
|
1012
|
+
esc(r.sending ? 'Sending…' : intent === 'reply' ? 'Send' : 'Send back') + '</button></div></div>';
|
|
1013
|
+
}
|
|
1014
|
+
function sendReply(it, intent) {
|
|
1015
|
+
var a = contributors(it)[0], r = S.reply[it.wid];
|
|
1016
|
+
if (!a || !r || !r.text.trim() || r.sending) return;
|
|
1017
|
+
if (/COS-WORK/i.test(r.text.replace(/[*_`]/g, ''))) { r.error = 'A note cannot carry a COS-WORK status line.'; return renderDetail(it.id); }
|
|
1018
|
+
var body = { clientRequestId: uuid(), domain: it.domain, workIdentity: it.id, intent: intent, mode: 'continueSession', sessionId: a.sessionId,
|
|
1019
|
+
note: r.text.trim(), replyTo: a.id, destinationSource: 'user',
|
|
1020
|
+
expectedTaskRevision: str((S.activity[it.wid] || {}).taskRevision) || str(it.task.taskRevision) };
|
|
1021
|
+
r.sending = true; r.error = ''; renderDetail(it.id);
|
|
1022
|
+
api('/api/work-board/handoff-requests', { method: 'POST', body: body }).then(function (d) {
|
|
1023
|
+
r.sending = false; r.request = (d && d.request) || { clientRequestId: body.clientRequestId, state: 'pending' };
|
|
1024
|
+
r.madeAt = new Date(); r.fails = 0; r.pollStopped = '';
|
|
1025
|
+
renderDetail(it.id); pollHandoff(r, replyUpdate(it.wid));
|
|
1026
|
+
}, function (err) { r.sending = false; r.error = errText(err); renderDetail(it.id); });
|
|
1027
|
+
}
|
|
1028
|
+
// One bounded poller for Start work and reply requests: every 3 s while the request is open, at most 25 minutes from
|
|
1029
|
+
// when it was made, stopping after 4 failures in a row or at once when signed out. leave() clears every timer.
|
|
1030
|
+
var HANDOFF_TIMERS = new Set();
|
|
1031
|
+
var TERMINAL = ['sent', 'refused', 'unresolved', 'unconfirmed', 'expired'];
|
|
1032
|
+
function requestOpen(h) { return !!(h && h.request && TERMINAL.indexOf(h.request.state) < 0); }
|
|
1033
|
+
function pollHandoff(h, onUpdate) {
|
|
1034
|
+
if (h.timer) { clearTimeout(h.timer); HANDOFF_TIMERS.delete(h.timer); h.timer = null; }
|
|
1035
|
+
if (!requestOpen(h) || h.pollStopped) return;
|
|
1036
|
+
var made = W.toDate(h.request.createdAt) || h.madeAt || new Date();
|
|
1037
|
+
if (Date.now() - made.getTime() > 25 * 60000) { h.pollStopped = 'Stopped checking after 25 minutes. Open the card later to see what happened.'; onUpdate(); return; }
|
|
1038
|
+
var timer = setTimeout(function () {
|
|
1039
|
+
HANDOFF_TIMERS.delete(timer); h.timer = null;
|
|
1040
|
+
api('/api/work-board/handoff-requests/' + encodeURIComponent(h.request.clientRequestId)).then(function (d) {
|
|
1041
|
+
h.fails = 0;
|
|
1042
|
+
if (d && d.request) h.request = d.request;
|
|
1043
|
+
onUpdate();
|
|
1044
|
+
if (W.parseRoute().pane === 'work') pollHandoff(h, onUpdate); // an answer that lands after leave() does not re-arm
|
|
1045
|
+
}, function (err) {
|
|
1046
|
+
h.fails = (h.fails || 0) + 1;
|
|
1047
|
+
if ((err && err.status === 401) || h.fails >= 4) { h.pollStopped = 'Stopped checking: COS on your Mac did not answer. Refresh to check again.'; onUpdate(); return; }
|
|
1048
|
+
if (W.parseRoute().pane === 'work') pollHandoff(h, onUpdate);
|
|
1049
|
+
});
|
|
1050
|
+
}, 3000);
|
|
1051
|
+
h.timer = timer; HANDOFF_TIMERS.add(timer);
|
|
1052
|
+
}
|
|
1053
|
+
function stopHandoffPolls() {
|
|
1054
|
+
HANDOFF_TIMERS.forEach(function (t) { clearTimeout(t); }); HANDOFF_TIMERS.clear();
|
|
1055
|
+
[S.start, S.reply].forEach(function (all) { Object.keys(all).forEach(function (k) { all[k].timer = null; }); });
|
|
1056
|
+
}
|
|
1057
|
+
// Back on Work: pick up any request that was still open when the pane was left.
|
|
1058
|
+
function resumeHandoffPolls() {
|
|
1059
|
+
Object.keys(S.start).forEach(function (wid) { var h = S.start[wid]; if (requestOpen(h) && !h.timer && !h.pollStopped) pollHandoff(h, startUpdate(wid)); });
|
|
1060
|
+
Object.keys(S.reply).forEach(function (wid) { var h = S.reply[wid]; if (requestOpen(h) && !h.timer && !h.pollStopped) pollHandoff(h, replyUpdate(wid)); });
|
|
1061
|
+
}
|
|
1062
|
+
function replyUpdate(wid) {
|
|
1063
|
+
return function () {
|
|
1064
|
+
var h = S.reply[wid], it = itemByWid(wid); if (!h || !it) return;
|
|
1065
|
+
if (h.request && h.request.state === 'sent') { delete S.activity[wid]; }
|
|
1066
|
+
var rt = W.parseRoute();
|
|
1067
|
+
if (rt.pane === 'work' && rt.args.length === 1 && rt.args[0] === it.id) renderDetail(it.id);
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function startUpdate(wid) {
|
|
1071
|
+
return function () {
|
|
1072
|
+
var h = S.start[wid], it = itemByWid(wid); if (!h || !it) return;
|
|
1073
|
+
if (h.request && h.request.state === 'sent') { delete S.activity[wid]; loadOpen().then(function () { ITEMS = null; }); }
|
|
1074
|
+
var rt = W.parseRoute();
|
|
1075
|
+
if (rt.pane === 'work' && rt.args[0] === it.id && rt.args[1] === 'start') renderStart(it.id);
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function destinationLabel(d) {
|
|
1079
|
+
if (d.mode === 'newSession') return 'New ' + providerName(d.provider) + ' session' + (d.model ? ' · ' + d.model : '');
|
|
1080
|
+
var s = splitSession(d.sessionId);
|
|
1081
|
+
return (d.mode === 'fork' ? 'Fork of a ' : 'Continue a ') + providerName(s ? s.provider : d.provider) + ' session';
|
|
1082
|
+
}
|
|
1083
|
+
function detailHeader(it) {
|
|
1084
|
+
var t = it.task;
|
|
1085
|
+
var facts = [str(t.createdAt) ? 'Created ' + t.createdAt : 'Created date unknown', 'Owner: ' + (str(t.owner) || 'Unknown'), str(t.dueDate) ? 'Due ' + t.dueDate : ''].filter(Boolean).join(' · ');
|
|
1086
|
+
return '<header class="wk-detail-head"><div class="wk-row wk-top wk-wrap"><h1 class="wk-detail-title">' + inline(it.title) + '</h1>' +
|
|
1087
|
+
'<div class="wk-row wk-actions"><button type="button" class="cx-btn quiet" data-wk="edit" data-id="' + esc(it.id) + '">Edit task</button>' +
|
|
1088
|
+
(!t.checked ? '<button type="button" class="cx-btn quiet" data-wk="start" data-id="' + esc(it.id) + '">Start work</button>' : '') + '</div></div>' +
|
|
1089
|
+
'<p class="wk-muted">' + esc(domainName(t.domain) + ' · ' + (t.checked ? 'Completed task' : STAGE_TITLE[it.stage])) + '</p>' +
|
|
1090
|
+
(S.pendingComplete === it.wid ? completeConfirm(it) : '<div class="wk-row wk-wrap">' + stageMenu(it, 'wk-stage-lg') +
|
|
1091
|
+
'<button type="button" class="cx-btn quiet" data-wk="copy-id" data-id="' + esc(it.id) + '"' + (canExport(t) ? '' : ' disabled') + ' title="Copy this task’s canonical Work ID for another session.">Copy task ID</button>' +
|
|
1092
|
+
'<button type="button" class="cx-btn quiet" data-wk="copy-context" data-id="' + esc(it.id) + '"' + (canExport(t) ? '' : ' disabled') + ' title="Copy the task, source references and instructions for linking a session and updating its stage.">Copy context</button>' +
|
|
1093
|
+
(S.busy ? '<span class="wk-muted">Saving…</span>' : '') + '</div>') +
|
|
1094
|
+
'<p class="wk-small wk-muted">' + esc(facts) + '</p>' + (t.workMetadataError ? '<p class="wk-error">' + esc(t.workMetadataError) + '</p>' : '') + '</header>';
|
|
1095
|
+
}
|
|
1096
|
+
function taskBody(it) {
|
|
1097
|
+
var t = it.task;
|
|
1098
|
+
function fact(title, text) { return '<div class="wk-fact"><h3>' + esc(title) + '</h3><div class="cx-card-body md">' + W.md(text) + '</div></div>'; }
|
|
1099
|
+
return '<section class="wk-section">' + fact('Finish line', str(t.doneWhen) || 'No finish line recorded. Use Edit task to define one.') +
|
|
1100
|
+
(str(t.source) ? fact('Source', t.source) : '') + (str(t.runAt) ? fact('Scheduled', t.runAt) : '') +
|
|
1101
|
+
'<p class="wk-fine">Task editing, scheduling, and completion stay attached to the original task. Agent output does not change its completion state.</p></section>';
|
|
1102
|
+
}
|
|
1103
|
+
function renderDetail(arg) {
|
|
1104
|
+
var it = itemFor(arg);
|
|
1105
|
+
S.viewWaiting = !it && (S.loading || !S.tasks);
|
|
1106
|
+
if (!it) {
|
|
1107
|
+
pane.innerHTML = '<div class="cx wk" data-tint="work">' + backBar(backLabel()) + (S.loading || !S.tasks ? W.skeletonRows(4)
|
|
1108
|
+
: '<div class="wk-missing"><h2 class="wk-h2">This work is not currently available</h2><p class="wk-muted">Refresh or choose another item. The original task or meeting has not been recreated.</p></div>') + '</div>';
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
W.setCrumb(clip(plainTitle(it.title), 60));
|
|
1112
|
+
pane.innerHTML = '<div class="cx wk" data-tint="work">' + backBar(backLabel()) + notices() +
|
|
1113
|
+
'<div class="wk-detail">' + '<div class="wk-detail-main">' + detailHeader(it) + taskBody(it) + '</div>' +
|
|
1114
|
+
'<aside class="wk-detail-side">' + connections(it) + workspace(it) + '</aside></div></div>';
|
|
1115
|
+
if (!S.activity[it.wid] && !S.activityError[it.wid] && !S.activityLoading[it.wid]) {
|
|
1116
|
+
S.activityLoading[it.wid] = true;
|
|
1117
|
+
loadActivity(it).then(function () { delete S.activityLoading[it.wid]; }).then(function () { var r = W.parseRoute(); if (r.pane === 'work' && r.args[0] === arg && r.args.length === 1) renderDetail(arg); });
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// ── Start work: a handoff request COS Control on the Mac claims and sends (glasses path, 6.59.0) ──
|
|
1122
|
+
var FORKABLE = { claude: 1, codex: 1 };
|
|
1123
|
+
// The sessions Start work can continue or fork: this card's own, plus the saved destination's session when it names one
|
|
1124
|
+
// that never worked on this card (labelled as the saved destination, so the picker shows what Start will send to).
|
|
1125
|
+
function sessionChoices(it) {
|
|
1126
|
+
var rows = contributors(it).filter(function (a) { return splitSession(a.sessionId); }).map(function (a) {
|
|
1127
|
+
var row = sessionRow(a);
|
|
1128
|
+
return { sessionId: a.sessionId, provider: splitSession(a.sessionId).provider,
|
|
1129
|
+
label: providerName(a.provider) + ' \u00b7 ' + clip(str((row || {}).display_label) || str(a.sessionTitle) || 'Session', 60) + (row ? ' \u00b7 ' + ageLabel(row) : '') };
|
|
1130
|
+
});
|
|
1131
|
+
var saved = (S.activity[it.wid] || {}).savedDestination, ss = saved && splitSession(saved.sessionId);
|
|
1132
|
+
if (ss && saved.mode !== 'newSession' && !rows.some(function (c) { return String(c.sessionId).toLowerCase() === String(saved.sessionId).toLowerCase(); })) {
|
|
1133
|
+
var live = S.sessions[ss.id.toLowerCase()];
|
|
1134
|
+
rows.unshift({ sessionId: saved.sessionId, provider: ss.provider, saved: true,
|
|
1135
|
+
label: destinationLabel(saved) + (live ? ' \u00b7 ' + clip(str(live.display_label) || 'Session', 50) : '') + ' (saved)' });
|
|
1136
|
+
}
|
|
1137
|
+
return rows;
|
|
1138
|
+
}
|
|
1139
|
+
function startState(it) {
|
|
1140
|
+
var st = S.start[it.wid];
|
|
1141
|
+
if (st) return st;
|
|
1142
|
+
var saved = (S.activity[it.wid] || {}).savedDestination, rows = sessionChoices(it);
|
|
1143
|
+
st = { mode: 'newSession', sessionId: '', model: '', note: '', sending: false, request: null, error: '', fromSaved: false };
|
|
1144
|
+
if (saved && saved.mode) {
|
|
1145
|
+
st.mode = saved.mode; st.sessionId = str(saved.sessionId); st.model = str(saved.model); st.fromSaved = true;
|
|
1146
|
+
} else if (rows.length) { st.mode = 'continueSession'; st.sessionId = rows[0].sessionId; }
|
|
1147
|
+
S.start[it.wid] = st;
|
|
1148
|
+
return st;
|
|
1149
|
+
}
|
|
1150
|
+
function blockingHandoff(it) {
|
|
1151
|
+
return contributors(it).filter(function (a) { return !a.acknowledged && (['preparing', 'sending', 'queued', 'running'].indexOf(a.status) >= 0 || a.sending === true); })[0] || null;
|
|
1152
|
+
}
|
|
1153
|
+
function startProblem(it, st) {
|
|
1154
|
+
if (it.completed) return 'This task is complete.';
|
|
1155
|
+
if (st.mode !== 'newSession' && !splitSession(st.sessionId)) return 'Choose a session to send to.';
|
|
1156
|
+
if (st.mode === 'fork') { var s = splitSession(st.sessionId); if (!s || !FORKABLE[s.provider]) return 'Fork works with Claude and Codex sessions only.'; }
|
|
1157
|
+
if (st.note.trim().length > 2000) return 'The note is over 2,000 characters.';
|
|
1158
|
+
if (/COS-WORK/i.test(st.note.replace(/[*_`]/g, ''))) return 'A note cannot carry a COS-WORK status line.';
|
|
1159
|
+
return '';
|
|
1160
|
+
}
|
|
1161
|
+
function verb(st) { return st.mode === 'newSession' ? 'Start session' : st.mode === 'fork' ? 'Fork and send' : 'Send'; }
|
|
1162
|
+
function renderStart(arg) {
|
|
1163
|
+
var it = itemFor(arg);
|
|
1164
|
+
if (!it) return renderDetail(arg);
|
|
1165
|
+
W.setCrumb(clip(plainTitle(it.title), 60));
|
|
1166
|
+
if (!S.activity[it.wid] && !S.activityError[it.wid]) {
|
|
1167
|
+
pane.innerHTML = '<div class="cx wk" data-tint="work">' + W.skeletonRows(3) + '</div>';
|
|
1168
|
+
loadActivity(it).then(function () { var r = W.parseRoute(); if (r.pane === 'work' && r.args[0] === arg && r.args[1] === 'start') renderStart(arg); });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
var st = startState(it), blocking = blockingHandoff(it), caps = (S.activity[it.wid] || {}).capabilities || S.openCaps || {};
|
|
1172
|
+
var sessions = sessionChoices(it);
|
|
1173
|
+
var problem = startProblem(it, st), taking = caps.requests === 1;
|
|
1174
|
+
function choice(mode, title, detail, disabled) {
|
|
1175
|
+
return '<label class="wk-choice' + (disabled ? ' off' : '') + '"><input type="radio" name="wk-mode" value="' + mode + '"' + (st.mode === mode ? ' checked' : '') + (disabled || st.sending || st.request ? ' disabled' : '') + '>' +
|
|
1176
|
+
'<span><strong>' + esc(title) + '</strong><span class="wk-small wk-muted">' + esc(detail) + '</span></span></label>';
|
|
1177
|
+
}
|
|
1178
|
+
var picker = st.mode === 'newSession'
|
|
1179
|
+
? '<label class="wk-pick wk-pick-wide"><span>Model</span><select class="field" data-wk-start="model"' + (st.sending || st.request ? ' disabled' : '') + '>' +
|
|
1180
|
+
MODELS.map(function (m) { return '<option value="' + m[0] + '"' + (m[0] === st.model ? ' selected' : '') + '>' + esc(m[1]) + '</option>'; }).join('') + '</select></label>'
|
|
1181
|
+
: '<label class="wk-pick wk-pick-wide"><span>Session</span><select class="field" data-wk-start="session"' + (st.sending || st.request ? ' disabled' : '') + '>' +
|
|
1182
|
+
(sessions.length ? '' : '<option value="">No session has worked on this card yet</option>') +
|
|
1183
|
+
sessions.filter(function (c) { return st.mode !== 'fork' || FORKABLE[c.provider]; }).map(function (c) {
|
|
1184
|
+
return '<option value="' + esc(c.sessionId) + '"' + (String(c.sessionId).toLowerCase() === String(st.sessionId).toLowerCase() ? ' selected' : '') + '>' + esc(c.label) + '</option>';
|
|
1185
|
+
}).join('') + '</select></label>';
|
|
1186
|
+
var req = st.request, reqHtml = '';
|
|
1187
|
+
if (req) {
|
|
1188
|
+
var terminal = ['sent', 'refused', 'unresolved', 'unconfirmed', 'expired'].indexOf(req.state) >= 0;
|
|
1189
|
+
reqHtml = '<div class="wk-handoff" data-tone="' + (req.state === 'sent' ? 'green' : req.state === 'refused' || req.state === 'expired' ? 'danger' : 'accent') + '">' +
|
|
1190
|
+
'<p class="wk-state">' + esc(REQUEST_LINE[req.state] || req.state) + '</p>' + (req.reason ? '<p class="wk-small">' + esc(req.reason) + '</p>' : '') +
|
|
1191
|
+
(st.pollStopped ? '<p class="wk-small wk-muted">' + esc(st.pollStopped) + '</p>' : '') +
|
|
1192
|
+
(terminal || st.pollStopped ? '<div class="wk-row"><button type="button" class="cx-btn quiet" data-wk="back">Back to the card</button></div>' : '<p class="wk-fine">This page checks every few seconds.</p>') + '</div>';
|
|
1193
|
+
}
|
|
1194
|
+
pane.innerHTML = '<div class="cx wk" data-tint="work">' + backBar('Back to the card') +
|
|
1195
|
+
'<section class="wk-start"><h1 class="wk-detail-title">' + esc(blocking && !req ? 'This card already has a handoff' : 'Start work on this card?') + '</h1>' +
|
|
1196
|
+
'<p class="wk-start-task">' + inline(it.title) + '</p><p class="wk-muted">' + esc(domainName(it.domain) + ' · ' + STAGE_TITLE[it.stage]) + '</p>' +
|
|
1197
|
+
(!taking ? '<div class="cx-notice warn">COS Control on your Mac is not picking up requests right now. Open COS Control on the Mac, then send this. A request waits 10 minutes and nothing is sent if it is not picked up.</div>' : '') +
|
|
1198
|
+
(blocking && !req ? '<div class="cx-notice">' + esc(STATE_LABEL[cardState(blocking)] + ' · ' + str(blocking.sessionTitle) + '. Wait for it, or check the session before sending more.') + '</div>' : '') +
|
|
1199
|
+
(st.fromSaved && !req ? '<p class="wk-small wk-muted">Your saved destination for this card.</p>' : '') +
|
|
1200
|
+
'<div class="wk-field"><span class="wk-label">Where should it go?</span><div class="wk-choices">' +
|
|
1201
|
+
choice('continueSession', 'Continue a session', 'Sends to an existing conversation, with its model and permissions. For Cursor, a new Cursor chat opens with your note filled in, and you press Send there.', !sessions.length) +
|
|
1202
|
+
choice('fork', 'Fork a session', 'Copies a conversation first. Same platform, or to Claude or Codex.', !sessions.some(function (c) { return FORKABLE[c.provider]; })) +
|
|
1203
|
+
choice('newSession', 'Start a new session', 'Claude, Codex or a local model in the background, with a model you pick. Cursor opens its own window with the handoff filled in, and you press Send there.', false) +
|
|
1204
|
+
'</div></div>' + picker +
|
|
1205
|
+
'<label class="wk-field"><span class="wk-label">Note</span><textarea class="field" rows="3" data-wk-start="note" maxlength="2000" placeholder="Anything to add to the card’s context (optional)"' + (st.sending || req ? ' disabled' : '') + '>' + esc(st.note) + '</textarea></label>' +
|
|
1206
|
+
'<p class="wk-small wk-muted">Sends the card’s context. Nothing is published, and the card stays in its column.</p>' +
|
|
1207
|
+
(st.error ? '<p class="wk-error">' + esc(st.error) + '</p>' : problem && !req ? '<p class="wk-muted wk-small">' + esc(problem) + '</p>' : '') + reqHtml +
|
|
1208
|
+
(!req ? '<div class="wk-row"><button type="button" class="cx-btn quiet" data-wk="back"' + (st.sending ? ' disabled' : '') + '>Cancel</button>' +
|
|
1209
|
+
'<button type="button" class="cx-btn primary" data-wk="start-send" data-id="' + esc(it.id) + '"' + (problem || st.sending || blocking ? ' disabled' : '') + '>' + esc(st.sending ? 'Sending…' : verb(st)) + '</button></div>' : '') +
|
|
1210
|
+
'</section></div>';
|
|
1211
|
+
}
|
|
1212
|
+
function sendStart(it) {
|
|
1213
|
+
var st = startState(it), saved = (S.activity[it.wid] || {}).savedDestination;
|
|
1214
|
+
if (startProblem(it, st) || st.sending) return;
|
|
1215
|
+
var body = { clientRequestId: uuid(), domain: it.domain, workIdentity: it.id, intent: 'start', mode: st.mode,
|
|
1216
|
+
expectedTaskRevision: str((S.activity[it.wid] || {}).taskRevision) || str(it.task.taskRevision) };
|
|
1217
|
+
if (st.mode !== 'newSession') body.sessionId = st.sessionId;
|
|
1218
|
+
if (st.mode === 'newSession' && st.model) body.model = st.model;
|
|
1219
|
+
if (st.note.trim()) body.note = st.note.trim();
|
|
1220
|
+
body.destinationSource = saved && saved.mode === st.mode && str(saved.sessionId) === str(body.sessionId || '') && str(saved.model) === str(body.model || '') ? 'saved' : 'user';
|
|
1221
|
+
st.sending = true; st.error = ''; renderStart(it.id);
|
|
1222
|
+
api('/api/work-board/handoff-requests', { method: 'POST', body: body }).then(function (d) {
|
|
1223
|
+
st.sending = false; st.request = (d && d.request) || { clientRequestId: body.clientRequestId, state: 'pending' };
|
|
1224
|
+
st.madeAt = new Date(); st.fails = 0; st.pollStopped = '';
|
|
1225
|
+
renderStart(it.id); pollHandoff(st, startUpdate(it.wid));
|
|
1226
|
+
}, function (err) { st.sending = false; st.error = errText(err); renderStart(it.id); });
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// ── Edit task (WorkTaskEditor) ───────────────────────────
|
|
1230
|
+
function editState(it) {
|
|
1231
|
+
var e = S.edit[it.wid];
|
|
1232
|
+
if (!e || e.rev !== str(it.task.workRevision)) {
|
|
1233
|
+
if (e && e.dirty) return e; // never rebase an unsaved draft onto a newer revision
|
|
1234
|
+
var run = W.toDate(it.task.runAt);
|
|
1235
|
+
e = S.edit[it.wid] = { rev: str(it.task.workRevision), text: titleOf(it.task), doneWhen: str(it.task.doneWhen), busy: false, error: '', confirming: false,
|
|
1236
|
+
runAt: run ? localDay(run) + 'T' + String(run.getHours()).padStart(2, '0') + ':' + String(run.getMinutes()).padStart(2, '0') : '' };
|
|
1237
|
+
}
|
|
1238
|
+
e.dirty = e.text !== titleOf(it.task) || e.doneWhen !== str(it.task.doneWhen);
|
|
1239
|
+
return e;
|
|
1240
|
+
}
|
|
1241
|
+
function editUnavailable(t) {
|
|
1242
|
+
if (!(S.caps.version === 1 && S.caps.writable === true && S.caps.editTasks === 1)) return 'Task editing is unavailable. Refresh availability; if it stays unavailable, update the configured task bridge.';
|
|
1243
|
+
if (t.workMetadataError) return 'This task’s saved details need repair: ' + t.workMetadataError;
|
|
1244
|
+
if (!str(t.workRevision)) return 'This task has no saved revision. Keep your draft and reopen the task after refreshing Work.';
|
|
1245
|
+
return '';
|
|
1246
|
+
}
|
|
1247
|
+
function editValidation(e) {
|
|
1248
|
+
var clean = e.text.trim();
|
|
1249
|
+
if (!clean) return 'Enter a task name.';
|
|
1250
|
+
if (clean.length > 2000) return 'Keep the task name under 2,001 characters.';
|
|
1251
|
+
if (e.doneWhen.trim().length > 500) return 'Keep the finish line under 501 characters.';
|
|
1252
|
+
return '';
|
|
1253
|
+
}
|
|
1254
|
+
function renderEdit(arg) {
|
|
1255
|
+
var it = itemFor(arg);
|
|
1256
|
+
if (!it) return renderDetail(arg);
|
|
1257
|
+
W.setCrumb('Edit task');
|
|
1258
|
+
var t = it.task, e = editState(it), reason = editUnavailable(t), invalid = editValidation(e);
|
|
1259
|
+
var canSave = !e.busy && !reason && !invalid && e.dirty, off = e.busy ? ' disabled' : '';
|
|
1260
|
+
function line(label, value) { return value ? '<div class="wk-line"><span>' + esc(label) + '</span><span>' + esc(value) + '</span></div>' : ''; }
|
|
1261
|
+
pane.innerHTML = '<div class="cx wk" data-tint="work">' + backBar('Back to the card') +
|
|
1262
|
+
'<section class="wk-edit"><h1 class="wk-detail-title">Edit task</h1>' +
|
|
1263
|
+
'<label class="wk-field"><span class="wk-label">Task name</span><textarea class="field" rows="4" data-wk-edit="text" aria-label="Task name"' + off + '>' + esc(e.text) + '</textarea></label>' +
|
|
1264
|
+
'<label class="wk-field"><span class="wk-label">Done when</span><input class="field" type="text" data-wk-edit="doneWhen" placeholder="What does finished look like?" value="' + esc(e.doneWhen) + '" aria-label="Done when"' + off + '>' +
|
|
1265
|
+
'<span class="wk-small wk-muted">Saved with the task name. Add a finish line before running this task.</span></label>' +
|
|
1266
|
+
'<div class="wk-lines">' + line('Domain', t.domain) + line('Stage', str(t.workStage).charAt(0).toUpperCase() + str(t.workStage).slice(1)) + line('Source', str(t.source)) + '</div>' +
|
|
1267
|
+
'<details class="wk-more-actions"><summary>More task actions</summary><div class="wk-more-body">' +
|
|
1268
|
+
'<div class="wk-row wk-wrap"><button type="button" class="cx-btn quiet" data-wk="task-patch" data-patch="checked"' + (e.busy || e.dirty ? ' disabled' : '') + '>' + (t.checked ? 'Reopen' : 'Done') + '</button>' +
|
|
1269
|
+
'<button type="button" class="cx-btn quiet" data-wk="task-patch" data-patch="inbox"' + (e.busy || e.dirty || t.section === 'inbox' ? ' disabled' : '') + '>To inbox</button></div>' +
|
|
1270
|
+
'<p class="wk-small wk-muted">Save your edits before changing status or scheduling.</p>' +
|
|
1271
|
+
'<div class="wk-lines">' + line('Lane', str(t.column)) + line('Section', str(t.section)) + line('Ref', str(t.ref)) + line('Agent', str(t.agentState)) + '</div>' +
|
|
1272
|
+
'<label class="wk-field"><span class="wk-small wk-muted">Legacy schedule for</span><input class="field" type="datetime-local" data-wk-edit="runAt" value="' + esc(e.runAt) + '"' + off + '></label>' +
|
|
1273
|
+
'<div class="wk-row wk-wrap"><button type="button" class="cx-btn quiet" data-wk="task-patch" data-patch="schedule"' + (e.busy || e.dirty || !e.runAt ? ' disabled' : '') + '>Schedule</button></div>' +
|
|
1274
|
+
'<p class="wk-small wk-muted">Uses the existing task dispatcher. Choose a session from Work’s agent workspace for a directed handoff.</p>' +
|
|
1275
|
+
'<div class="wk-row wk-wrap"><span class="wk-small wk-muted">Move to</span>' + ['planning', 'active', 'review'].map(function (s) {
|
|
1276
|
+
return '<button type="button" class="cx-btn quiet" data-wk="task-patch" data-patch="stage" data-stage="' + s + '"' + (e.busy || e.dirty || t.stage === s ? ' disabled' : '') + '>' + s.charAt(0).toUpperCase() + s.slice(1) + '</button>';
|
|
1277
|
+
}).join('') + '</div></div></details>' +
|
|
1278
|
+
'<footer class="wk-edit-foot">' +
|
|
1279
|
+
(e.error ? '<p class="wk-error">' + esc(e.error) + '</p><p class="wk-small wk-muted">Your draft is still here.</p>' : '') +
|
|
1280
|
+
(reason ? '<p class="wk-small wk-muted">' + esc(reason) + '</p><button type="button" class="wk-text-btn" data-wk="edit-refresh"' + off + '>Refresh availability</button>'
|
|
1281
|
+
: invalid ? '<p class="wk-error">' + esc(invalid) + '</p>' : '') +
|
|
1282
|
+
(e.confirming ? '<div class="wk-confirm"><p><strong>Save task changes?</strong></p><p class="wk-small">Save your task name and finish line, discard these edits, or keep editing.</p>' +
|
|
1283
|
+
'<div class="wk-row wk-wrap"><button type="button" class="cx-btn primary" data-wk="edit-save"' + (canSave ? '' : ' disabled') + '>Save</button>' +
|
|
1284
|
+
'<button type="button" class="cx-btn quiet" data-wk="edit-discard">Discard</button><button type="button" class="wk-text-btn" data-wk="edit-keep">Keep editing</button></div></div>'
|
|
1285
|
+
: '<div class="wk-row"><button type="button" class="cx-btn quiet" data-wk="edit-cancel"' + off + '>Cancel</button><span class="wk-push">' + (e.busy ? '<span class="wk-muted">Saving…</span> ' : '') +
|
|
1286
|
+
'<button type="button" class="cx-btn primary" data-wk="edit-save"' + (canSave ? '' : ' disabled') + '>Save changes</button></span></div>') +
|
|
1287
|
+
'</footer></section></div>';
|
|
1288
|
+
}
|
|
1289
|
+
function editRun(it, work) {
|
|
1290
|
+
var e = editState(it);
|
|
1291
|
+
if (e.busy) return;
|
|
1292
|
+
e.busy = true; e.error = ''; e.confirming = false; renderEdit(it.id);
|
|
1293
|
+
work().then(function () { return loadBoard(true); }).then(function () {
|
|
1294
|
+
e.busy = false; delete S.edit[it.wid];
|
|
1295
|
+
var fresh = itemByWid(it.wid);
|
|
1296
|
+
W.go('work', [fresh ? fresh.id : it.id]);
|
|
1297
|
+
}, function (err) { e.busy = false; e.error = errText(err); renderEdit(it.id); });
|
|
1298
|
+
}
|
|
1299
|
+
function editSave(it) {
|
|
1300
|
+
var e = editState(it), t = it.task;
|
|
1301
|
+
if (editUnavailable(t) || editValidation(e)) return;
|
|
1302
|
+
editRun(it, function () {
|
|
1303
|
+
return api('/api/work-board/edit', { method: 'POST', body: { domain: t.domain, id: t.id, expectedText: titleOf(t), expectedRevision: str(t.workRevision),
|
|
1304
|
+
text: e.text.trim(), doneWhen: e.doneWhen.trim() } }).then(function (d) {
|
|
1305
|
+
if (!d || d.workIdentity !== identity(t) || String(d.id || '').length !== 12 || String(d.workRevision || '').length !== 64) throw new Error('Task save could not be verified. Refresh before trying again.');
|
|
1306
|
+
});
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
function taskPatch(it, kind, stage) {
|
|
1310
|
+
var t = it.task, e = editState(it), body = { domain: t.domain };
|
|
1311
|
+
if (kind === 'checked') body.checked = !t.checked;
|
|
1312
|
+
else if (kind === 'inbox') body.section = 'inbox';
|
|
1313
|
+
else if (kind === 'stage') body.stage = stage;
|
|
1314
|
+
else if (kind === 'schedule') { if (!e.runAt) return; body.runAt = e.runAt.replace('T', ' ').slice(0, 16); }
|
|
1315
|
+
editRun(it, function () { return api('/api/tasks/' + encodeURIComponent(t.id), { method: 'PATCH', body: body }); });
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// ── writes (each one only on a person's tap) ─────────────
|
|
1319
|
+
function afterWrite(p) {
|
|
1320
|
+
S.busy = true; S.mutationError = ''; render();
|
|
1321
|
+
return p.then(function () { S.busy = false; return loadBoard(true); }, function (err) { S.busy = false; S.mutationError = errText(err); render(); });
|
|
1322
|
+
}
|
|
1323
|
+
function moveStage(it, stage) {
|
|
1324
|
+
var t = it.task;
|
|
1325
|
+
if (!canChangeStage(t) || stageOf(t) === stage || STAGES.indexOf(stage) < 0) { render(); return; }
|
|
1326
|
+
if (stage === 'complete' && S.pendingComplete !== it.wid) { S.pendingComplete = it.wid; render(); return; }
|
|
1327
|
+
S.pendingComplete = null;
|
|
1328
|
+
afterWrite(api('/api/work-board/stage', { method: 'POST', body: { domain: t.domain, id: t.id, expectedText: titleOf(t), expectedRevision: str(t.workRevision), workStage: stage } }));
|
|
1329
|
+
}
|
|
1330
|
+
function capture() {
|
|
1331
|
+
var text = S.captureText.trim(), domain = S.captureDomain;
|
|
1332
|
+
if (!text || domainNames().indexOf(domain) < 0 || S.captureBusy) return;
|
|
1333
|
+
S.captureBusy = true; S.captureError = ''; render();
|
|
1334
|
+
api('/api/tasks/capture', { method: 'POST', body: { domain: domain, text: text, section: 'inbox', captureId: uuid() } }).then(function () {
|
|
1335
|
+
S.captureBusy = false; S.captureText = ''; S.captureOpen = false; loadBoard(true);
|
|
1336
|
+
}, function (err) { S.captureBusy = false; S.captureError = errText(err); render(); });
|
|
1337
|
+
}
|
|
1338
|
+
function probeLoop() {
|
|
1339
|
+
if (S.loopAvailable !== null) return Promise.resolve();
|
|
1340
|
+
return api('/api/work-intake/decisions').then(function () { S.loopAvailable = true; }, function (err) { if (err.status === 404) S.loopAvailable = false; });
|
|
1341
|
+
}
|
|
1342
|
+
function dropIntake(ids) {
|
|
1343
|
+
S.intake = (S.intake || []).filter(function (it) { return ids.indexOf(it.id) < 0; });
|
|
1344
|
+
SPLIT = null;
|
|
1345
|
+
}
|
|
1346
|
+
function resolveIntake(id, action) {
|
|
1347
|
+
if (S.intakeBusy[id]) return;
|
|
1348
|
+
S.intakeBusy[id] = 1; S.sortError = ''; render();
|
|
1349
|
+
api('/api/work-intake/' + encodeURIComponent(id) + '/resolve', { method: 'POST', body: { action: action } }).then(function (d) {
|
|
1350
|
+
delete S.intakeBusy[id]; dropIntake([id]);
|
|
1351
|
+
var at = d && d.item && d.item.resolution && d.item.resolution.at;
|
|
1352
|
+
if (at) S.undoBatch = id + ':' + at;
|
|
1353
|
+
return probeLoop().then(function () { render(); loadIntake(); if (action === 'accept') loadBoard(true); });
|
|
1354
|
+
}, function (err) { delete S.intakeBusy[id]; S.sortError = errText(err, INTAKE_REFUSALS); render(); });
|
|
1355
|
+
}
|
|
1356
|
+
function dismissRest(ids) {
|
|
1357
|
+
ids = ids.filter(function (id) { return !S.intakeBusy[id]; });
|
|
1358
|
+
if (!ids.length) return;
|
|
1359
|
+
ids.forEach(function (id) { S.intakeBusy[id] = 1; });
|
|
1360
|
+
S.sortError = ''; render();
|
|
1361
|
+
var batchId = uuid(), chain;
|
|
1362
|
+
chain = api('/api/work-intake/resolve-batch', { method: 'POST', body: { ids: ids.slice(0, 200), action: 'dismiss', batchId: batchId } }).then(function () {
|
|
1363
|
+
S.undoBatch = batchId;
|
|
1364
|
+
}, function (err) {
|
|
1365
|
+
if (err.status !== 404) throw err;
|
|
1366
|
+
// An older server: the single-item route, one at a time, stopping at the first refusal.
|
|
1367
|
+
return ids.reduce(function (p, id) {
|
|
1368
|
+
return p.then(function () { return api('/api/work-intake/' + encodeURIComponent(id) + '/resolve', { method: 'POST', body: { action: 'dismiss' } }).then(function () { dropIntake([id]); }); });
|
|
1369
|
+
}, Promise.resolve());
|
|
1370
|
+
});
|
|
1371
|
+
chain.then(function () { dropIntake(ids); }, function (err) { S.sortError = errText(err, INTAKE_REFUSALS); })
|
|
1372
|
+
.then(function () { ids.forEach(function (id) { delete S.intakeBusy[id]; }); return probeLoop(); }).then(function () { render(); loadIntake(); });
|
|
1373
|
+
}
|
|
1374
|
+
function undoIntake() {
|
|
1375
|
+
if (!S.undoBatch) return;
|
|
1376
|
+
api('/api/work-intake/undo', { method: 'POST', body: { batchId: S.undoBatch } }).then(function () {
|
|
1377
|
+
S.undoBatch = null; loadIntake(true); loadBoard(true);
|
|
1378
|
+
}, function (err) { if (err.status === 404) S.loopAvailable = false; S.sortError = err.status === 404 ? UNDO_MISSING : errText(err); render(); });
|
|
1379
|
+
}
|
|
1380
|
+
function batchOp(t, action, fields) {
|
|
1381
|
+
if (S.batchBusy || !(S.caps.workBatch === 1 && S.caps.writable === true)) return;
|
|
1382
|
+
var op = { id: t.id, action: action, expectedText: titleOf(t), expectedRevision: str(t.workRevision) };
|
|
1383
|
+
Object.keys(fields || {}).forEach(function (k) { op[k] = fields[k]; });
|
|
1384
|
+
S.batchBusy = true; S.loopError = ''; render();
|
|
1385
|
+
api('/api/work-board/batch', { method: 'POST', body: { domain: t.domain, ops: [op] } }).then(function () {
|
|
1386
|
+
S.batchBusy = false; loadBoard(true); if (S.tab === 'waiting') loadWaiting();
|
|
1387
|
+
}, function (err) { S.batchBusy = false; S.loopError = err.status === 404 ? 'Waiting on needs a newer COS server.' : errText(err); render(); });
|
|
1388
|
+
}
|
|
1389
|
+
function loadWaiting() {
|
|
1390
|
+
var mode = S.waitingDropped ? 'dropped' : 'waiting';
|
|
1391
|
+
api('/api/work-board/' + mode).then(function (d) {
|
|
1392
|
+
var rows = (d && (d.tasks || d.items || d.cards)) || [];
|
|
1393
|
+
if (mode === 'dropped') S.dropped = rows; else S.waiting = rows;
|
|
1394
|
+
S.loopError = ''; render();
|
|
1395
|
+
}, function (err) { S.loopError = err.status === 404 ? (mode === 'dropped' ? 'Dropped cards need a newer COS server.' : 'Waiting on needs a newer COS server.') : errText(err);
|
|
1396
|
+
if (mode === 'dropped') S.dropped = []; else S.waiting = []; render(); });
|
|
1397
|
+
}
|
|
1398
|
+
// Meaning search: asked only when Return is pressed in the search box (never while typing).
|
|
1399
|
+
function searchMeaning() {
|
|
1400
|
+
var q = S.q.trim(), key = searchKey();
|
|
1401
|
+
if (q.length < 3 || !queryWords(q).length || S.meaningPending === key || (S.meaning && S.meaning.key === key)) return;
|
|
1402
|
+
var body = { query: q.slice(0, 200) };
|
|
1403
|
+
if (S.scope === 'attention' || S.scope === 'progress') {
|
|
1404
|
+
var ids = {}; items().filter(function (it) { return inView(it, S.scope, S.domain); }).forEach(function (it) { if (/^[a-f0-9]{12}$/.test(it.task.id)) ids[it.task.id] = 1; });
|
|
1405
|
+
body.ids = Object.keys(ids);
|
|
1406
|
+
if (!body.ids.length || body.ids.length > 254) { S.meaning = { key: key, available: false, reason: body.ids.length ? 'too_many_candidates' : 'search_off', scores: {} }; render(); return; }
|
|
1407
|
+
} else { body.scope = S.scope; if (S.domain) body.domain = S.domain; }
|
|
1408
|
+
S.meaningPending = key; render();
|
|
1409
|
+
api('/api/work/search', { method: 'POST', body: body }).then(function (d) {
|
|
1410
|
+
var scores = {};
|
|
1411
|
+
(d && Array.isArray(d.results) ? d.results : []).forEach(function (r) { if (r && typeof r.id === 'string' && typeof r.p === 'number' && r.p >= 0 && r.p <= 1) scores[r.id] = Math.max(scores[r.id] || 0, r.p); });
|
|
1412
|
+
return { key: key, available: !!(d && d.available === true), reason: d && (d.reason || (d.available === true ? null : 'jev_unavailable')), scores: scores };
|
|
1413
|
+
}, function (err) { return { key: key, available: false, reason: err.status === 404 ? 'server_too_old' : 'unreachable', scores: {} }; }).then(function (m) {
|
|
1414
|
+
if (S.meaningPending === key) S.meaningPending = '';
|
|
1415
|
+
if (searchKey() === key) S.meaning = m;
|
|
1416
|
+
render();
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// ── render ───────────────────────────────────────────────
|
|
1421
|
+
function body() {
|
|
1422
|
+
if (!S.tasks) return W.skeletonRows(5);
|
|
1423
|
+
if (S.tab === 'review') return decisionQueue(false);
|
|
1424
|
+
if (S.tab === 'history') return decisionQueue(true);
|
|
1425
|
+
if (S.tab === 'focus') return focusView();
|
|
1426
|
+
if (S.tab === 'sort') return sortView();
|
|
1427
|
+
if (S.tab === 'waiting') return waitingView();
|
|
1428
|
+
return board();
|
|
1429
|
+
}
|
|
1430
|
+
function listHtml() {
|
|
1431
|
+
return '<div class="cx wk" data-tint="work" data-tab="' + esc(S.tab) + '">' + hero() + captureForm() +
|
|
1432
|
+
'<div class="wk-controls">' + tabs() + filters() + '</div>' + notices() + body() + '</div>';
|
|
1433
|
+
}
|
|
1434
|
+
// Every scroller marked data-keep keeps its place across a repaint; the page keeps its own.
|
|
1435
|
+
function writeList(html) {
|
|
1436
|
+
var active = document.activeElement, keep = active && pane.contains(active) && active.id ? active.id : null;
|
|
1437
|
+
var caret = keep && typeof active.selectionStart === 'number' ? [active.selectionStart, active.selectionEnd] : null;
|
|
1438
|
+
var places = {};
|
|
1439
|
+
pane.querySelectorAll('[data-keep]').forEach(function (el) { places[el.getAttribute('data-keep')] = [el.scrollLeft, el.scrollTop]; });
|
|
1440
|
+
var y = window.scrollY;
|
|
1441
|
+
pane.innerHTML = html; PAINT.html = html; S.viewWaiting = false;
|
|
1442
|
+
pane.querySelectorAll('[data-keep]').forEach(function (el) { var p = places[el.getAttribute('data-keep')]; if (p) { el.scrollLeft = p[0]; el.scrollTop = p[1]; } });
|
|
1443
|
+
if (Math.abs(window.scrollY - y) > 1) window.scrollTo(window.scrollX, y);
|
|
1444
|
+
if (keep) { var el = $(keep); if (el) { el.focus({ preventScroll: true }); if (caret) { try { el.setSelectionRange(caret[0], caret[1]); } catch (_e) {} } } }
|
|
1445
|
+
}
|
|
1446
|
+
function renderList() { clearTimeout(PAINT.timer); PAINT.timer = null; writeList(listHtml()); }
|
|
1447
|
+
function render() {
|
|
1448
|
+
var r = W.parseRoute();
|
|
1449
|
+
if (r.pane !== 'work') return;
|
|
1450
|
+
if (!r.args.length) return renderList();
|
|
1451
|
+
PAINT.html = '';
|
|
1452
|
+
if (r.args[1] === 'start') return renderStart(r.args[0]);
|
|
1453
|
+
if (r.args[1] === 'edit') return renderEdit(r.args[0]);
|
|
1454
|
+
return renderDetail(r.args[0]);
|
|
1455
|
+
}
|
|
1456
|
+
function current() { var r = W.parseRoute(); return r.args.length ? itemFor(r.args[0]) : null; }
|
|
1457
|
+
|
|
1458
|
+
// ── events (delegated on the pane) ───────────────────────
|
|
1459
|
+
pane.addEventListener('click', function (e) {
|
|
1460
|
+
var t = e.target.closest('[data-wk]');
|
|
1461
|
+
if (S.moreOpen && !e.target.closest('.wk-more')) { S.moreOpen = false; if (!t) { renderList(); return; } }
|
|
1462
|
+
if (!t || t.disabled) return;
|
|
1463
|
+
var act = t.getAttribute('data-wk'), id = t.getAttribute('data-id'), it;
|
|
1464
|
+
switch (act) {
|
|
1465
|
+
case 'refresh': return refreshAll();
|
|
1466
|
+
case 'capture-toggle':
|
|
1467
|
+
S.captureOpen = !S.captureOpen;
|
|
1468
|
+
if (S.captureOpen && !S.captureDomain) S.captureDomain = S.domain || ((S.domains || [])[0] || {}).name || '';
|
|
1469
|
+
renderList(); if (S.captureOpen) { var f = $('wk-capture-text'); if (f) f.focus(); }
|
|
1470
|
+
return;
|
|
1471
|
+
case 'tab':
|
|
1472
|
+
S.tab = t.getAttribute('data-tab'); S.moreOpen = false; S.pendingComplete = null; persist();
|
|
1473
|
+
if (S.tab === 'sort' && !S.intake && !S.intakeLoading) loadIntake(true);
|
|
1474
|
+
if (S.tab === 'waiting') { S.waiting = null; S.dropped = null; loadWaiting(); }
|
|
1475
|
+
if (W.parseRoute().args.length) return W.go('work');
|
|
1476
|
+
window.scrollTo(0, 0);
|
|
1477
|
+
return renderList();
|
|
1478
|
+
case 'more': S.moreOpen = !S.moreOpen; return renderList();
|
|
1479
|
+
case 'clear': S.q = ''; S.meaning = null; return renderList();
|
|
1480
|
+
case 'open': return W.go('work', [id]);
|
|
1481
|
+
case 'back': return W.goBack();
|
|
1482
|
+
case 'start':
|
|
1483
|
+
it = itemFor(id);
|
|
1484
|
+
if (it && S.start[it.wid] && (!requestOpen(S.start[it.wid]) && S.start[it.wid].request || S.start[it.wid].pollStopped)) delete S.start[it.wid];
|
|
1485
|
+
return W.go('work', [id, 'start']);
|
|
1486
|
+
case 'edit': return W.go('work', [id, 'edit']);
|
|
1487
|
+
case 'meeting': return W.go('meetings', [t.getAttribute('data-domain'), t.getAttribute('data-month'), t.getAttribute('data-file')]);
|
|
1488
|
+
case 'session': return W.go('sessions', [t.getAttribute('data-provider'), t.getAttribute('data-sid')]);
|
|
1489
|
+
case 'dismiss-note': S.note = ''; return render();
|
|
1490
|
+
case 'dismiss-error': S.mutationError = ''; return render();
|
|
1491
|
+
case 'show-all-decisions': S.showAllDecisions = !S.showAllDecisions; return renderList();
|
|
1492
|
+
case 'hide': case 'restore':
|
|
1493
|
+
var a = (S.open || []).filter(function (x) { return x.id === t.getAttribute('data-rid'); })[0];
|
|
1494
|
+
if (!a) return;
|
|
1495
|
+
if (act === 'hide') S.hidden[a.id] = signature(a); else delete S.hidden[a.id];
|
|
1496
|
+
W.writeStore(STORE + '.hidden', S.hidden);
|
|
1497
|
+
return renderList();
|
|
1498
|
+
case 'mentioned': S.mentionedOpen = !S.mentionedOpen; return renderList();
|
|
1499
|
+
case 'mentioned-all': S.mentionedAll = true; return renderList();
|
|
1500
|
+
case 'column-all': S.columnAll[t.getAttribute('data-stage')] = true; return renderList();
|
|
1501
|
+
case 'search-all': S.domain = ''; S.scope = 'all'; return renderList();
|
|
1502
|
+
case 'scope-link':
|
|
1503
|
+
S.q = ''; S.domain = ''; S.scope = t.getAttribute('data-scope');
|
|
1504
|
+
var matches = items().filter(function (x) { return inView(x, S.scope, ''); });
|
|
1505
|
+
if (matches.length === 1) return W.go('work', [matches[0].id]);
|
|
1506
|
+
return renderList();
|
|
1507
|
+
case 'complete-yes': it = itemFor(id); if (it) moveStage(it, 'complete'); return;
|
|
1508
|
+
case 'complete-no': S.pendingComplete = null; return render();
|
|
1509
|
+
case 'copy-id': case 'copy-context':
|
|
1510
|
+
it = itemFor(id); if (!it || !canExport(it.task)) return;
|
|
1511
|
+
W.copyText(act === 'copy-id' ? workId(it.task) : contextText(it.task)).then(function () {
|
|
1512
|
+
W.toast(act === 'copy-id' ? 'Copied task ID: ' + workId(it.task) : 'Task context copied. Paste it into a session to resolve and link this task.');
|
|
1513
|
+
}, function (err) { W.toast(err.message || 'Could not copy to the clipboard. Try again.'); });
|
|
1514
|
+
return;
|
|
1515
|
+
case 'copy-session':
|
|
1516
|
+
W.copyText(t.getAttribute('data-sid')).then(function () { W.toast('Copied session ID'); }, function (err) { W.toast(err.message); });
|
|
1517
|
+
return;
|
|
1518
|
+
case 'start-send': it = itemFor(id); if (it) sendStart(it); return;
|
|
1519
|
+
case 'reply-open': case 'reply-cancel':
|
|
1520
|
+
it = itemFor(id); if (it && S.reply[it.wid]) { S.reply[it.wid].open = act === 'reply-open'; renderDetail(it.id); } return;
|
|
1521
|
+
case 'reply-send': it = itemFor(id); if (it) sendReply(it, t.getAttribute('data-intent')); return;
|
|
1522
|
+
case 'reply-reset': it = itemFor(id); if (it) { delete S.reply[it.wid]; delete S.activity[it.wid]; renderDetail(it.id); } return;
|
|
1523
|
+
case 'edit-save': it = current(); if (it) editSave(it); return;
|
|
1524
|
+
case 'edit-cancel':
|
|
1525
|
+
it = current(); if (!it) return W.goBack();
|
|
1526
|
+
if (editState(it).dirty) { editState(it).confirming = true; return renderEdit(it.id); }
|
|
1527
|
+
delete S.edit[it.wid]; return W.goBack();
|
|
1528
|
+
case 'edit-discard': it = current(); if (it) delete S.edit[it.wid]; return W.goBack();
|
|
1529
|
+
case 'edit-keep': it = current(); if (it) { editState(it).confirming = false; renderEdit(it.id); } return;
|
|
1530
|
+
case 'edit-refresh': loadBoard(true); return;
|
|
1531
|
+
case 'task-patch': it = current(); if (it) taskPatch(it, t.getAttribute('data-patch'), t.getAttribute('data-stage')); return;
|
|
1532
|
+
case 'intake-refresh': return loadIntake(true);
|
|
1533
|
+
case 'resolve': return resolveIntake(t.getAttribute('data-iid'), t.getAttribute('data-action-kind'));
|
|
1534
|
+
case 'dismiss-rest':
|
|
1535
|
+
var g = sortGroups()[S.sortIndex]; if (g) dismissRest(g.items.map(function (x) { return x.id; }));
|
|
1536
|
+
return;
|
|
1537
|
+
case 'undo': return undoIntake();
|
|
1538
|
+
case 'sort-prev': S.sortGroupId = ''; S.sortIndex = Math.max(0, S.sortIndex - 1); return renderList();
|
|
1539
|
+
case 'sort-later': var n = sortGroups().length; S.sortGroupId = ''; S.sortIndex = n ? (S.sortIndex + 1) % n : 0; return renderList();
|
|
1540
|
+
case 'card-keep': case 'card-drop':
|
|
1541
|
+
var task = (S.tasks || []).filter(function (x) { return identity(x) === id; })[0];
|
|
1542
|
+
if (task) batchOp(task, act === 'card-keep' ? 'stage' : 'drop', act === 'card-keep' ? { workStage: 'planned' } : null);
|
|
1543
|
+
return;
|
|
1544
|
+
case 'waiting-refresh': return loadWaiting();
|
|
1545
|
+
case 'waiting-mode': S.waitingDropped = t.getAttribute('data-mode') === 'dropped'; renderList(); return loadWaiting();
|
|
1546
|
+
case 'batch':
|
|
1547
|
+
var list = (S.waitingDropped ? S.dropped : S.waiting) || [];
|
|
1548
|
+
var row = list.filter(function (x) { return x.id === id && x.domain === t.getAttribute('data-domain'); })[0];
|
|
1549
|
+
if (!row) return;
|
|
1550
|
+
var op = t.getAttribute('data-op');
|
|
1551
|
+
if (op === 'done') return batchOp(row, 'stage', { workStage: 'complete' });
|
|
1552
|
+
if (op === 'checkIn') {
|
|
1553
|
+
var input = pane.querySelector('[data-wk-checkin="' + CSS.escape(id) + '"]');
|
|
1554
|
+
var next = input && input.value ? input.value : localDay(new Date(Date.now() + 7 * 86400000));
|
|
1555
|
+
return batchOp(row, 'checkIn', { checkIn: next });
|
|
1556
|
+
}
|
|
1557
|
+
return batchOp(row, op);
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
pane.addEventListener('change', function (e) {
|
|
1561
|
+
var el = e.target, it;
|
|
1562
|
+
if (el.hasAttribute('data-wk-select')) {
|
|
1563
|
+
var name = el.getAttribute('data-wk-select');
|
|
1564
|
+
if (name === 'domain') S.domain = el.value;
|
|
1565
|
+
else if (name === 'scope') S.scope = el.value;
|
|
1566
|
+
else if (name === 'order') { S.orders[orderKey()] = el.value; persist(); }
|
|
1567
|
+
S.pendingComplete = null;
|
|
1568
|
+
return renderList();
|
|
1569
|
+
}
|
|
1570
|
+
if (el.hasAttribute('data-wk-stage')) { it = itemFor(el.getAttribute('data-wk-stage')); if (it) moveStage(it, el.value); return; }
|
|
1571
|
+
if (el.id === 'wk-capture-domain') { S.captureDomain = el.value; return renderList(); }
|
|
1572
|
+
if (el.name === 'wk-mode' || el.hasAttribute('data-wk-start')) {
|
|
1573
|
+
it = current(); if (!it) return;
|
|
1574
|
+
var st = startState(it);
|
|
1575
|
+
if (el.name === 'wk-mode') {
|
|
1576
|
+
st.mode = el.value; st.fromSaved = false;
|
|
1577
|
+
var rows = sessionChoices(it).filter(function (c) { return st.mode !== 'fork' || FORKABLE[c.provider]; });
|
|
1578
|
+
if (st.mode !== 'newSession' && !rows.some(function (c) { return c.sessionId === st.sessionId; })) st.sessionId = rows.length ? rows[0].sessionId : '';
|
|
1579
|
+
} else if (el.getAttribute('data-wk-start') === 'session') { st.sessionId = el.value; st.fromSaved = false; }
|
|
1580
|
+
else if (el.getAttribute('data-wk-start') === 'model') { st.model = el.value; st.fromSaved = false; }
|
|
1581
|
+
return renderStart(it.id);
|
|
1582
|
+
}
|
|
1583
|
+
if (el.hasAttribute('data-wk-edit')) { it = current(); if (it) { var ed = editState(it); ed[el.getAttribute('data-wk-edit')] = el.value; renderEdit(it.id); } }
|
|
1584
|
+
});
|
|
1585
|
+
var typing = null;
|
|
1586
|
+
pane.addEventListener('input', function (e) {
|
|
1587
|
+
var el = e.target, it;
|
|
1588
|
+
if (el.id === 'wk-q') {
|
|
1589
|
+
S.q = el.value;
|
|
1590
|
+
clearTimeout(typing); typing = setTimeout(renderList, 180);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (el.id === 'wk-capture-text') {
|
|
1594
|
+
S.captureText = el.value;
|
|
1595
|
+
var b = pane.querySelector('.wk-capture [type="submit"]');
|
|
1596
|
+
if (b) b.disabled = !(S.captureText.trim() && domainNames().indexOf(S.captureDomain) >= 0 && !S.captureBusy);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
if (el.hasAttribute('data-wk-reply')) {
|
|
1600
|
+
it = itemFor(el.getAttribute('data-wk-reply')); var rr = it && S.reply[it.wid]; if (!rr) return;
|
|
1601
|
+
rr.text = el.value;
|
|
1602
|
+
var rs = pane.querySelector('[data-wk="reply-send"]'); if (rs) rs.disabled = !rr.text.trim() || rr.text.trim().length > 2000 || rr.sending;
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (el.getAttribute('data-wk-start') === 'note') {
|
|
1606
|
+
it = current(); if (!it) return;
|
|
1607
|
+
var st = startState(it); st.note = el.value;
|
|
1608
|
+
var send = pane.querySelector('[data-wk="start-send"]'); if (send) send.disabled = !!startProblem(it, st) || st.sending || !!blockingHandoff(it);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
if (el.hasAttribute('data-wk-edit') && el.getAttribute('data-wk-edit') !== 'runAt') {
|
|
1612
|
+
it = current(); if (!it) return;
|
|
1613
|
+
var ed = editState(it); ed[el.getAttribute('data-wk-edit')] = el.value;
|
|
1614
|
+
ed.dirty = ed.text !== titleOf(it.task) || ed.doneWhen !== str(it.task.doneWhen);
|
|
1615
|
+
var save = pane.querySelector('[data-wk="edit-save"]');
|
|
1616
|
+
if (save) save.disabled = !(!ed.busy && !editUnavailable(it.task) && !editValidation(ed) && ed.dirty);
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
pane.addEventListener('submit', function (e) {
|
|
1620
|
+
var f = e.target.getAttribute('data-wk-form');
|
|
1621
|
+
if (!f) return;
|
|
1622
|
+
e.preventDefault();
|
|
1623
|
+
if (f === 'capture') return capture();
|
|
1624
|
+
if (f === 'search') { clearTimeout(typing); S.q = ($('wk-q') || {}).value || ''; renderList(); if (S.tab === 'board') searchMeaning(); }
|
|
1625
|
+
});
|
|
1626
|
+
// Sort's keys (j, k, s, e), as in Control, while Sort is showing and nothing is being typed.
|
|
1627
|
+
document.addEventListener('keydown', function (e) {
|
|
1628
|
+
var r = W.parseRoute();
|
|
1629
|
+
if (r.pane !== 'work' || r.args.length || S.tab !== 'sort' || e.metaKey || e.ctrlKey || e.altKey) return;
|
|
1630
|
+
if (e.repeat && e.key === 'e') return; // a held key decides once
|
|
1631
|
+
if (e.target.closest && e.target.closest('input, textarea, select, [contenteditable="true"]')) return;
|
|
1632
|
+
var groups = sortGroups(), n = groups.length;
|
|
1633
|
+
if (e.key === 'j' || e.key === 'k' || e.key === 's') S.sortGroupId = '';
|
|
1634
|
+
if (e.key === 'j') S.sortIndex = Math.min(Math.max(0, n - 1), S.sortIndex + 1);
|
|
1635
|
+
else if (e.key === 'k') S.sortIndex = Math.max(0, S.sortIndex - 1);
|
|
1636
|
+
else if (e.key === 's') S.sortIndex = n ? (S.sortIndex + 1) % n : 0;
|
|
1637
|
+
else if (e.key === 'e') {
|
|
1638
|
+
var g = groups[S.sortIndex]; if (!g) return;
|
|
1639
|
+
if (g.cards.length) batchOp(g.cards[0], 'stage', { workStage: 'planned' });
|
|
1640
|
+
else if (g.items.length) resolveIntake(g.items[0].id, 'accept');
|
|
1641
|
+
return;
|
|
1642
|
+
} else return;
|
|
1643
|
+
e.preventDefault(); renderList();
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
W.registerPane('work', {
|
|
1647
|
+
load: function (args) {
|
|
1648
|
+
// #/work/sort (the Today home's "From today's meetings" link): open Sort on today's group, and leave the list's
|
|
1649
|
+
// own address in history so Back returns to where the link was.
|
|
1650
|
+
if (args[0] === 'sort') {
|
|
1651
|
+
S.tab = 'sort'; S.sortGroupId = ''; S.sortIndex = 0; S.moreOpen = false;
|
|
1652
|
+
if (!S.intake && !S.intakeLoading) loadIntake(true);
|
|
1653
|
+
location.replace('#/work');
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
startPoll(); resumeHandoffPolls();
|
|
1657
|
+
render(); // one paint now; loads that finish later go through backgroundPaint
|
|
1658
|
+
if (!S.tasks && !S.loading) loadBoard();
|
|
1659
|
+
else if (!args.length && Date.now() - S.loadedAt > 60000 && !S.loading) loadBoard();
|
|
1660
|
+
},
|
|
1661
|
+
leave: function () { stopPoll(); stopHandoffPolls(); },
|
|
1662
|
+
// The Ask sheet's "Find a task about this": open the board on this query and ask by meaning.
|
|
1663
|
+
search: function (q) { S.q = String(q || ''); S.tab = 'board'; render(); searchMeaning(); },
|
|
1664
|
+
// Back unwinds one step: Start work and Edit go to the card, the card to the list.
|
|
1665
|
+
up: function (args) { return args.length >= 2 ? [args[0]] : null; }
|
|
1666
|
+
});
|
|
1667
|
+
})();
|