@gotcos/glasses-server 6.99.33 → 7.0.18
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/.env.example +37 -0
- package/CHANGELOG.md +587 -0
- package/README.md +3 -1
- package/docs/web-key-verifier-contract.md +378 -0
- package/managed-runtime-contract.json +12 -1
- package/package.json +10 -3
- package/server/bootstrap.ts +10 -0
- package/server/index.ts +155 -30
- package/server/lib/agent-session-output-images.ts +627 -49
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +27 -14
- package/server/lib/agent-session-tool-images.ts +224 -0
- package/server/lib/agent-session-turns.ts +19 -2
- package/server/lib/api-auth.ts +43 -1
- package/server/lib/ask-work-plan.ts +3 -1
- package/server/lib/calendar-meetings.ts +70 -0
- package/server/lib/calendar-source.ts +536 -0
- package/server/lib/claude-hooks-installer.ts +111 -12
- package/server/lib/codex-bridge.ts +4 -1
- package/server/lib/codex-run-ledger.ts +29 -0
- package/server/lib/context-builder.ts +2 -1
- package/server/lib/conversation.ts +56 -28
- package/server/lib/cors-reject-log.ts +51 -0
- package/server/lib/disk-io-meter.ts +202 -0
- package/server/lib/drop-p2-runtime.ts +1 -1
- package/server/lib/drop-service.ts +47 -12
- package/server/lib/drop-store.ts +31 -5
- package/server/lib/event-loop-stall.ts +62 -0
- package/server/lib/full-hold-engine.ts +114 -0
- package/server/lib/full-hold.ts +124 -0
- package/server/lib/g2-turn-provenance.ts +128 -9
- package/server/lib/learning-needs-you.ts +24 -7
- package/server/lib/listener-startup.ts +18 -0
- package/server/lib/live-events.ts +533 -0
- package/server/lib/local-path-redact.ts +272 -0
- package/server/lib/meeting-ask-orientation.ts +40 -6
- package/server/lib/meeting-ask-runner.ts +30 -10
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- package/server/lib/morning-brief-runtime.ts +4 -2
- package/server/lib/native-mac-caller.ts +29 -0
- package/server/lib/permission-broker.ts +443 -11
- package/server/lib/ping-engine.ts +1 -1
- package/server/lib/ping-sources.ts +11 -6
- package/server/lib/preview-content.ts +172 -0
- package/server/lib/preview-error-code.ts +10 -0
- package/server/lib/preview-folders-setting.ts +133 -0
- package/server/lib/preview-log-throttle.ts +18 -0
- package/server/lib/preview-thumbs.ts +139 -0
- package/server/lib/secret-path-guard.ts +83 -0
- package/server/lib/session-cache-writer.ts +8 -1
- package/server/lib/session-files-setting.ts +88 -0
- package/server/lib/session-hook-events.ts +23 -1
- package/server/lib/session-hooks-runtime.ts +34 -0
- package/server/lib/session-links.ts +280 -0
- package/server/lib/session-log.ts +313 -4
- package/server/lib/session-state-derive.ts +36 -0
- package/server/lib/session-stream-events.ts +25 -1
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +640 -85
- package/server/lib/turn-end-ask.ts +320 -0
- package/server/lib/turn-footer.ts +3 -3
- package/server/lib/web-control-integrity.ts +160 -0
- package/server/lib/web-control-offnet-page.ts +58 -0
- package/server/lib/web-control-static.ts +53 -9
- package/server/lib/web-key-authority.ts +372 -0
- package/server/lib/web-key-envelope.ts +98 -0
- package/server/lib/web-key-handoff.ts +135 -0
- package/server/lib/web-key-origin.ts +21 -0
- package/server/lib/web-key-passkey.ts +155 -0
- package/server/lib/web-key-registry.ts +825 -0
- package/server/lib/web-key-signer-app.ts +152 -0
- package/server/lib/web-memories-bridge.ts +3 -1
- package/server/lib/web-session-auth.ts +133 -15
- package/server/lib/whisper-local.ts +93 -7
- package/server/lib/whisper-preview.ts +5 -3
- package/server/lib/work-activity.ts +56 -6
- package/server/lib/work-orchestrator/snapshot.ts +17 -3
- package/server/lib/work-review-runtime.ts +17 -4
- package/server/lib/work-session-activity.ts +260 -0
- package/server/lib/your-move-count.ts +9 -3
- package/server/routes/agent-session-bindings.ts +29 -1
- package/server/routes/agent-session-stream.ts +23 -0
- package/server/routes/agent-sessions.ts +146 -11
- package/server/routes/ask-work-plan.ts +4 -1
- package/server/routes/claude-sessions.ts +5 -1
- package/server/routes/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +46 -1
- package/server/routes/live-events.ts +334 -0
- package/server/routes/permission-broker.ts +95 -5
- package/server/routes/query-jobs.ts +3 -0
- package/server/routes/session-preview.ts +200 -0
- package/server/routes/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +223 -12
- package/server/routes/today-brief.ts +134 -44
- package/server/routes/web-control.ts +37 -4
- package/server/routes/web-key.ts +284 -0
- package/server/routes/welcome-context.ts +28 -4
- package/server/routes/work-board.ts +9 -2
- package/server/routes/work-orchestrator.ts +12 -7
- package/server/scripts/write-web-control-manifest.mjs +43 -0
- package/server/web-control/.manifest.json +44 -0
- package/server/web-control/app.css +76 -1
- package/server/web-control/app.js +2247 -98
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/control.css +25 -0
- package/server/web-control/index.html +21 -3
- package/server/web-control/memory-review.js +100 -0
- package/server/web-control/panes/cos-chat.css +94 -9
- package/server/web-control/panes/cos-chat.js +628 -44
- package/server/web-control/panes/meetings.css +18 -2
- package/server/web-control/panes/meetings.js +24 -8
- package/server/web-control/panes/memories.css +17 -2
- package/server/web-control/panes/memories.js +127 -42
- package/server/web-control/panes/messages.js +21 -2
- package/server/web-control/panes/preview.js +318 -0
- package/server/web-control/panes/questions.js +162 -21
- package/server/web-control/panes/reader-media.js +409 -23
- package/server/web-control/panes/sessions.css +32 -3
- package/server/web-control/panes/settings.js +97 -6
- package/server/web-control/panes/today-actions.css +46 -0
- package/server/web-control/panes/today-actions.js +119 -17
- package/server/web-control/panes/work.css +13 -0
- package/server/web-control/panes/work.js +188 -37
- package/server/web-control/shell.js +17 -14
- package/server/web-signer/.manifest.json +11 -0
- package/server/web-signer/index.html +74 -0
- package/server/web-signer/signer-proof.js +348 -0
- package/server/web-signer/signer-store.js +39 -0
- package/server/web-signer/signer.css +23 -0
- package/server/web-signer/signer.js +341 -0
- package/shared/web-approval-packets.golden.json +124 -0
- package/shared/web-turn-envelope.ts +271 -0
|
@@ -33,9 +33,56 @@
|
|
|
33
33
|
var PROVIDER_NAME = { claude: 'Claude', codex: 'Codex', cursor: 'Cursor', ollama: 'Local model' };
|
|
34
34
|
var MODELS = [['', 'Server default'], ['opus', 'Claude Opus'], ['fable', 'Claude Fable'], ['sonnet', 'Claude Sonnet'],
|
|
35
35
|
['codex-frontier', 'Codex frontier'], ['codex-balanced', 'Codex balanced']];
|
|
36
|
+
// ── The models a New session may start on (Q1 P1, P2): COS Control's own catalog rule, ported as is from
|
|
37
|
+
// HelperSources/main.swift workModelCatalog (b02d1ad) over the same two reads, GET /api/health and GET /api/models.
|
|
38
|
+
// A slot is available only with durable jobs on, its provider ready and one unambiguous catalog entry (Claude slots
|
|
39
|
+
// are aliases and need no entry). Read once a minute at most; null when either read failed.
|
|
40
|
+
var WM_SLOTS = [['sonnet', 'claude', 'Claude Sonnet'], ['opus', 'claude', 'Claude Opus'], ['fable', 'claude', 'Claude Fable'], ['haiku', 'claude', 'Claude Haiku'],
|
|
41
|
+
['codex-frontier', 'codex', 'OpenAI via Codex · Frontier'], ['codex-balanced', 'codex', 'OpenAI via Codex · Balanced'],
|
|
42
|
+
['cursor-grok', 'cursor', 'Grok via Cursor'], ['cursor-composer', 'cursor', 'Cursor Composer'], ['ollama', 'ollama', 'Ollama']];
|
|
43
|
+
var WM = { models: null, at: 0, loading: null, failed: false };
|
|
44
|
+
function workModelCatalog(health, catalog) {
|
|
45
|
+
var f = (health && health.features) || {}, c = catalog || {};
|
|
46
|
+
var durable = f.durableQueryJobs === true && f.durableQueryJobsProtocol === 1;
|
|
47
|
+
var options = Array.isArray(c.options) ? c.options : [];
|
|
48
|
+
return WM_SLOTS.map(function (slot) {
|
|
49
|
+
var matches = options.filter(function (o) { return o && o.preference === slot[0]; });
|
|
50
|
+
var concrete = matches.length === 1 && typeof matches[0].id === 'string' ? matches[0].id : null;
|
|
51
|
+
var catalogValid = slot[1] === 'claude' || !!(concrete && concrete.length <= 160);
|
|
52
|
+
var providerReady = f[slot[1]] === true && (slot[1] !== 'cursor' || c.cursorReady === true) && (slot[1] !== 'ollama' || c.ollamaReady === true);
|
|
53
|
+
return { id: slot[0], provider: slot[1], title: slot[2], available: !!(durable && providerReady && catalogValid) };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function loadWorkModels(force) {
|
|
57
|
+
if (WM.loading) return WM.loading;
|
|
58
|
+
if (!force && WM.at && Date.now() - WM.at < 60000) return Promise.resolve(WM.models);
|
|
59
|
+
WM.loading = Promise.all([api('/api/health'), api('/api/models')]).then(function (both) {
|
|
60
|
+
WM.models = workModelCatalog(both[0], both[1]); WM.failed = false;
|
|
61
|
+
}, function () { WM.models = null; WM.failed = true; }).then(function () { WM.at = Date.now(); WM.loading = null; return WM.models; });
|
|
62
|
+
return WM.loading;
|
|
63
|
+
}
|
|
64
|
+
// WorkOrchestrator.preferredModel: Claude's Opus when it is available, else the provider's first available model.
|
|
65
|
+
function preferredModel(provider, models) {
|
|
66
|
+
var open = (models || []).filter(function (m) { return m.provider === provider && m.available; }).map(function (m) { return m.id; });
|
|
67
|
+
if (provider === 'claude') { var opus = open.indexOf('opus') >= 0 ? 'opus' : open.filter(function (id) { return /opus/i.test(id); })[0]; if (opus) return opus; }
|
|
68
|
+
return open[0] || '';
|
|
69
|
+
}
|
|
70
|
+
// WorkHandoffStore.defaultDraft: a card with no session of its own starts on Claude (Opus), else Codex, whichever has
|
|
71
|
+
// an available model first.
|
|
72
|
+
function defaultNewModel(models) {
|
|
73
|
+
var provider = ['claude', 'codex'].filter(function (p) { return (models || []).some(function (m) { return m.provider === p && m.available; }); })[0];
|
|
74
|
+
return provider ? preferredModel(provider, models) : '';
|
|
75
|
+
}
|
|
76
|
+
// WorkOrchestrator.newSessionProviders: a provider with an available model whose plan usage is not used up.
|
|
77
|
+
function newSessionProviders(models, usage) {
|
|
78
|
+
var out = {}; (usage || []).forEach(function (u) { if (u.state === 'exhausted') out[u.provider] = 1; });
|
|
79
|
+
return ['claude', 'codex'].filter(function (p) { return !out[p] && (models || []).some(function (m) { return m.provider === p && m.available; }); });
|
|
80
|
+
}
|
|
36
81
|
var MODEL_PROVIDER = { opus: 'claude', fable: 'claude', sonnet: 'claude', haiku: 'claude', 'codex-frontier': 'codex', 'codex-balanced': 'codex' };
|
|
37
82
|
var HELPER = "'/Applications/COS Control.app/Contents/Resources/cos-control-helper'";
|
|
38
|
-
|
|
83
|
+
// W1 (7.0.x): no COS server serves the undo or batch routes yet (audit 02 F6), so Undo, Keep and Drop are offered only
|
|
84
|
+
// where the server answers for them, and nothing on the page promises a newer server.
|
|
85
|
+
var UNDO_FAILED = 'Undo did not go through. To change a card, open it in Work.';
|
|
39
86
|
var INTAKE_REFUSALS = {
|
|
40
87
|
meeting_identity_changed: 'That meeting was renamed or merged after it was scored. Dismiss this item; the renamed meeting is checked again.',
|
|
41
88
|
meeting_unavailable: 'That meeting can’t be found; it may have been renamed or removed. Dismiss this item.',
|
|
@@ -51,13 +98,21 @@
|
|
|
51
98
|
intake_store_closed: 'Intake is unavailable while the server restarts. Try again shortly.',
|
|
52
99
|
work_intake_unavailable: 'Intake is unavailable right now. Try again shortly.' };
|
|
53
100
|
var REQUEST_LINE = {
|
|
54
|
-
pending: 'Waiting for COS Control on your Mac to pick this up. Nothing has been sent yet.',
|
|
101
|
+
pending: 'Waiting for COS Control on your Mac to pick this up. COS Control has to be open on the Mac. Nothing has been sent yet.',
|
|
55
102
|
claimed: 'COS Control picked it up and is sending it.',
|
|
56
103
|
sent: 'Sent. The session has the card’s context.',
|
|
57
104
|
refused: 'COS Control did not send it.',
|
|
58
105
|
unresolved: 'COS Control sent it but could not confirm it arrived. Check the session before sending again.',
|
|
59
106
|
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
|
|
107
|
+
expired: 'COS Control on your Mac did not pick this up within 10 minutes, so nothing was sent. Open COS Control on the Mac, then send it again.' };
|
|
108
|
+
// W1 (audit 02 F8): a request from this page only goes through when COS Control on the Mac picks it up, within 10 minutes
|
|
109
|
+
// of when it was made (lib/work-handoff-requests.ts pendingMs). While it waits, the line says how long is left.
|
|
110
|
+
function requestLine(req) {
|
|
111
|
+
var line = REQUEST_LINE[req.state] || req.state;
|
|
112
|
+
if (req.state !== 'pending') return line;
|
|
113
|
+
var until = W.toDate(req.expiresAt), left = until ? Math.ceil((until.getTime() - Date.now()) / 60000) : 0;
|
|
114
|
+
return left > 0 ? line + ' It waits ' + (left === 1 ? '1 more minute.' : left + ' more minutes.') : line;
|
|
115
|
+
}
|
|
61
116
|
var STOP = {};
|
|
62
117
|
('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
118
|
|
|
@@ -831,8 +886,7 @@
|
|
|
831
886
|
if (S.sortIndex > n - 1) S.sortIndex = Math.max(0, n - 1);
|
|
832
887
|
var g = n ? groups[S.sortIndex] : null, batch = S.caps.workBatch === 1 && S.caps.writable === true;
|
|
833
888
|
S.sortGroupId = g ? g.id : '';
|
|
834
|
-
var undo = S.undoBatch
|
|
835
|
-
: S.loopAvailable === false ? '<span class="wk-muted">' + esc(UNDO_MISSING) + '</span>' : '') : '';
|
|
889
|
+
var undo = S.undoBatch && S.loopAvailable === true ? '<button type="button" class="wk-text-btn" data-wk="undo">Undo last decision</button>' : '';
|
|
836
890
|
var body;
|
|
837
891
|
if (S.intakeError && !(S.intake && S.intake.length)) body = '<p class="wk-muted">' + esc(S.intakeError) + '</p>';
|
|
838
892
|
else if (!S.intake) body = '<p class="wk-muted">Loading Intake…</p>';
|
|
@@ -844,10 +898,10 @@
|
|
|
844
898
|
g.cards.map(function (t) {
|
|
845
899
|
var meta = [str(t.createdAt), str(t.owner), str(t.dueDate) ? 'Due ' + t.dueDate : ''].filter(Boolean).join(' · ');
|
|
846
900
|
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"
|
|
848
|
-
'<button type="button" class="cx-btn quiet" data-wk="card-drop" data-id="' + esc(identity(t)) + '"' + (
|
|
901
|
+
'<div class="wk-row">' + (batch ? '<button type="button" class="cx-btn quiet" data-wk="card-keep" data-id="' + esc(identity(t)) + '"' + (S.batchBusy ? ' disabled' : '') + '>Keep</button>' +
|
|
902
|
+
'<button type="button" class="cx-btn quiet" data-wk="card-drop" data-id="' + esc(identity(t)) + '"' + (S.batchBusy ? ' disabled' : '') + '>Drop</button>' : '') +
|
|
849
903
|
'<button type="button" class="wk-text-btn" data-wk="open" data-id="' + esc(identity(t)) + '">Open card</button></div></div>';
|
|
850
|
-
}).join('') +
|
|
904
|
+
}).join('') +
|
|
851
905
|
(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
906
|
'<p class="wk-muted">Keep what you own. Other people’s asks can become cards when you take them on.</p>' +
|
|
853
907
|
'<div class="wk-items-list">' + g.items.map(intakeRow).join('') + '</div></div>' : '') +
|
|
@@ -1016,7 +1070,7 @@
|
|
|
1016
1070
|
if (r.request) {
|
|
1017
1071
|
var req = r.request, done = ['sent', 'refused', 'unresolved', 'unconfirmed', 'expired'].indexOf(req.state) >= 0;
|
|
1018
1072
|
return '<div class="wk-handoff" data-tone="' + (req.state === 'sent' ? 'green' : req.state === 'refused' || req.state === 'expired' ? 'danger' : 'accent') + '"><p class="wk-state">' +
|
|
1019
|
-
esc(
|
|
1073
|
+
esc(requestLine(req)) + '</p>' + (req.reason ? '<p class="wk-small">' + esc(req.reason) + '</p>' : '') +
|
|
1020
1074
|
(r.pollStopped ? '<p class="wk-small wk-muted">' + esc(r.pollStopped) + '</p>' : done ? '' : '<p class="wk-fine">This page checks every few seconds.</p>') +
|
|
1021
1075
|
(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>';
|
|
1022
1076
|
}
|
|
@@ -1126,6 +1180,13 @@
|
|
|
1126
1180
|
(str(t.source) ? fact('Source', t.source) : '') + (str(t.runAt) ? fact('Scheduled', t.runAt) : '') +
|
|
1127
1181
|
'<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>';
|
|
1128
1182
|
}
|
|
1183
|
+
// Duo tables: the divider between a card and its side panel (app.js splitKit, 900 px and up). The side panel is the
|
|
1184
|
+
// column on the right, so it grows as the line moves left; it keeps 320 px and the card 380 px, past a 22 px divider
|
|
1185
|
+
// that is the old gap. A repaint in place keeps the same divider. (DIVIDER, since SPLIT is the Sort split.)
|
|
1186
|
+
// A page without the kit (a test's stand-in W) draws no divider and keeps working.
|
|
1187
|
+
var SPLIT_KIT = W.splitKit || { handle: function () { return ''; }, mount: function () { return null; } };
|
|
1188
|
+
var DIVIDER = { key: 'cos_web_work_split', label: 'Side panel', side: 'end', sized: '.wk-detail-side', cssVar: '--split-side-share', media: '(min-width: 900px)', listMin: 320, readerMin: 380, handleW: 22 };
|
|
1189
|
+
var SPLIT_HANDLE = SPLIT_KIT.handle({ id: 'wk-handle', controls: 'wk-part-side', label: DIVIDER.label });
|
|
1129
1190
|
function renderDetail(arg) {
|
|
1130
1191
|
var it = itemFor(arg);
|
|
1131
1192
|
S.viewWaiting = !it && (S.loading || !S.tasks);
|
|
@@ -1151,11 +1212,12 @@
|
|
|
1151
1212
|
} else {
|
|
1152
1213
|
S.chatDeferred = false;
|
|
1153
1214
|
pane.innerHTML = '<div class="cx wk" data-tint="work">' + backBar(backLabel()) + '<div id="wk-part-notices" class="wk-part">' + parts.notices + '</div>' +
|
|
1154
|
-
'<div class="wk-detail" data-wid="' + esc(it.wid) + '"><div class="wk-detail-main"><div id="wk-part-main" class="wk-part">' + parts.main + '</div>' + chatSectionHtml(it) + '</div>' +
|
|
1155
|
-
'<aside class="wk-detail-side" id="wk-part-side">' + parts.side + '</aside></div></div>';
|
|
1215
|
+
'<div class="wk-detail' + (SPLIT_HANDLE ? ' split-side' : '') + '" data-wid="' + esc(it.wid) + '"><div class="wk-detail-main"><div id="wk-part-main" class="wk-part">' + parts.main + '</div>' + chatSectionHtml(it) + '</div>' +
|
|
1216
|
+
SPLIT_HANDLE + '<aside class="wk-detail-side" id="wk-part-side">' + parts.side + '</aside></div></div>';
|
|
1156
1217
|
[['wk-part-notices', parts.notices], ['wk-part-main', parts.main], ['wk-part-side', parts.side]].forEach(function (p) { var el = $(p[0]); if (el) el.__html = p[1]; });
|
|
1157
1218
|
var hist = $('wk-part-chat'); if (hist) hist.__html = hist.innerHTML;
|
|
1158
1219
|
}
|
|
1220
|
+
SPLIT_KIT.mount(pane.querySelector('.wk-detail'), $('wk-handle'), DIVIDER);
|
|
1159
1221
|
chatSyncComposer(it);
|
|
1160
1222
|
if (!S.activity[it.wid] && !S.activityError[it.wid] && !S.activityLoading[it.wid]) {
|
|
1161
1223
|
S.activityLoading[it.wid] = true;
|
|
@@ -1258,9 +1320,10 @@
|
|
|
1258
1320
|
state: CHAT_ROUND_WORD[r.state] ? r.state : 'planFailed', roundState: chatClean(r.roundState, 40), inHistory: r.inHistory === true,
|
|
1259
1321
|
states: r.states && typeof r.states === 'object' ? r.states : {}, error: chatClean(r.error, 400), at: Number(r.at) || 0, sentAt: Number(r.sentAt) || 0,
|
|
1260
1322
|
sendUnknown: r.sendUnknown === true, context: chatClean(r.context, 4000), skipped: r.skipped && typeof r.skipped === 'object' ? r.skipped : {},
|
|
1261
|
-
pollSince: Number(r.pollSince) || 0, pollStopped: r.pollStopped === true, up: Number(r.up) || 0
|
|
1323
|
+
pollSince: Number(r.pollSince) || 0, pollStopped: r.pollStopped === true, up: Number(r.up) || 0,
|
|
1324
|
+
offerNewSession: CHAT_NEW_PROVIDERS.indexOf(r.offerNewSession) >= 0 ? r.offerNewSession : '' };
|
|
1262
1325
|
// A plan in flight lives only in this page. After a reload it is gone, and nothing was saved or sent for it.
|
|
1263
|
-
if (out.state === 'planning') { out.state = 'planFailed'; out.error = 'Planning stopped when this page closed. Nothing was sent.
|
|
1326
|
+
if (out.state === 'planning') { out.state = 'planFailed'; out.error = 'Planning stopped when this page closed. Nothing was sent. Plan again to plan it.'; }
|
|
1264
1327
|
return out;
|
|
1265
1328
|
}
|
|
1266
1329
|
function chatOf(wid) {
|
|
@@ -1328,7 +1391,8 @@
|
|
|
1328
1391
|
function chatStored(r) {
|
|
1329
1392
|
return { key: r.key, roundId: r.roundId, question: r.question, answer: r.answer, assignments: r.assignments, planner: r.planner, model: r.model,
|
|
1330
1393
|
effort: r.effort, note: r.note, meter: r.meter, state: r.state, roundState: r.roundState, inHistory: r.inHistory, states: r.states, error: r.error,
|
|
1331
|
-
at: r.at, sentAt: r.sentAt, sendUnknown: r.sendUnknown, context: r.context, skipped: r.skipped, pollSince: r.pollSince || 0, pollStopped: !!r.pollStopped
|
|
1394
|
+
at: r.at, sentAt: r.sentAt, sendUnknown: r.sendUnknown, context: r.context, skipped: r.skipped, pollSince: r.pollSince || 0, pollStopped: !!r.pollStopped,
|
|
1395
|
+
offerNewSession: r.offerNewSession || '' };
|
|
1332
1396
|
}
|
|
1333
1397
|
function chatTrim(rounds) {
|
|
1334
1398
|
var newest = rounds.slice(-CHAT_KEEP.rounds);
|
|
@@ -1451,6 +1515,29 @@
|
|
|
1451
1515
|
return 'COS found ' + total + ' session' + (total === 1 ? '' : 's') + ' for this task, none can take a Continue now' +
|
|
1452
1516
|
(named.length ? ': ' + named.join('; ') + (more > 0 ? '; and ' + more + ' more' : '') : '') + '. The planner can fork one or start a new session.';
|
|
1453
1517
|
}
|
|
1518
|
+
// ── An empty card (Control 0.5.386, Miles 2026-11-09; WorkOrchestrator.swift "An empty card") ──
|
|
1519
|
+
// A card with no session that can take a Continue or a Fork plans exactly one New session: Claude, or Codex when Claude's
|
|
1520
|
+
// plan usage is low (a used-up provider is never offered). COS Control on the Mac picks the model when it sends: Opus when
|
|
1521
|
+
// available on 0.5.386. Nothing starts until a Send.
|
|
1522
|
+
var CHAT_NEW_PROVIDERS = ['claude', 'codex'];
|
|
1523
|
+
function chatNothingToContinue(rows) {
|
|
1524
|
+
return !rows.some(function (r) { return chatAutomatic(r) || (r.forkable && FORKABLE[r.id.slice(0, r.id.indexOf(':'))]); });
|
|
1525
|
+
}
|
|
1526
|
+
function chatEmptyCardProvider(offered, usage) {
|
|
1527
|
+
var claudeLow = usage.some(function (u) { return u.provider === 'claude' && u.state === 'low'; });
|
|
1528
|
+
if (offered.indexOf('claude') >= 0 && !(claudeLow && offered.indexOf('codex') >= 0)) return 'claude';
|
|
1529
|
+
return offered.indexOf('codex') >= 0 ? 'codex' : '';
|
|
1530
|
+
}
|
|
1531
|
+
function chatOneNewSession(list) {
|
|
1532
|
+
var seen = false;
|
|
1533
|
+
return list.filter(function (a) { if (a.mode !== 'newSession') return true; var keep = !seen; seen = true; return keep; });
|
|
1534
|
+
}
|
|
1535
|
+
function chatEmptyPhase(p) { return 'No session on this task yet. Planning a new ' + providerName(p) + ' session\u2026'; }
|
|
1536
|
+
function chatEmptyNote(p) { return 'No session on this task yet. COS will start a new ' + providerName(p) + ' session when you press Send.'; }
|
|
1537
|
+
function chatStartNewTitle(p) { return 'Start a new ' + providerName(p) + ' session with this request'; }
|
|
1538
|
+
// Plan again (WorkChatPolicy.canPlanAgain): a round that failed before the planner answered, so nothing was planned or sent.
|
|
1539
|
+
function chatCanPlanAgain(r) { return !!(r && r.state === 'planFailed' && !r.answer && !r.assignments.length && !r.roundId && !r.sentAt); }
|
|
1540
|
+
|
|
1454
1541
|
// The planner input, exactly as the server's parseWorkChatInput reads an orchestrator client's (Control 0.5.333).
|
|
1455
1542
|
function chatPlanBody(it, question, input, c, wid) {
|
|
1456
1543
|
var t = it.task, ids = {};
|
|
@@ -1468,10 +1555,9 @@
|
|
|
1468
1555
|
var st = (last.states || {})[a.id] || {};
|
|
1469
1556
|
return { sessionID: a.sessionID, status: chatClean(CHAT_ASSIGN_WORD[st.state] || 'Not sent', 100), detail: chatClean(st.detail, 2000), result: '' };
|
|
1470
1557
|
}) : [];
|
|
1471
|
-
var exhausted = {}; input.usage.forEach(function (u) { if (u.state === 'exhausted') exhausted[u.provider] = 1; });
|
|
1472
1558
|
var body = { workID: wid, revision: str(t.workRevision), question: question, context: chatContext(t, wid), sessions: sessions, history: history, updates: updates,
|
|
1473
1559
|
mode: 'plan', orchestrator: true, now: new Date().toISOString(), usage: input.usage, leftOut: input.leftOut,
|
|
1474
|
-
newSessionProviders:
|
|
1560
|
+
newSessionProviders: newSessionProviders(WM.models, input.usage) };
|
|
1475
1561
|
var planner = chatPlanner(), effort = chatEffort();
|
|
1476
1562
|
if (planner) body.planner = planner; // only when the server advertised a choice, as Control does
|
|
1477
1563
|
if (effort) body.effort = effort;
|
|
@@ -1536,8 +1622,10 @@
|
|
|
1536
1622
|
if (c.rounds.some(function (r) { return r.state === 'planning'; })) return 'A plan is still being made for this card.';
|
|
1537
1623
|
return '';
|
|
1538
1624
|
}
|
|
1539
|
-
function chatPlan(it) {
|
|
1540
|
-
var c = chatOf(it.wid),
|
|
1625
|
+
function chatPlan(it, replanKey) {
|
|
1626
|
+
var c = chatOf(it.wid), again = replanKey ? chatFindRound(it.wid, replanKey) : null;
|
|
1627
|
+
if (replanKey && !chatCanPlanAgain(again)) return Promise.resolve(false);
|
|
1628
|
+
var question = again ? again.question : c.draft.trim();
|
|
1541
1629
|
if (c.busy || !question || chatProblem(it, c)) return Promise.resolve(false);
|
|
1542
1630
|
var wid = chatWorkId(it.task);
|
|
1543
1631
|
c.busy = true; c.error = ''; c.phase = 'Reading the task and its connections…';
|
|
@@ -1551,28 +1639,43 @@
|
|
|
1551
1639
|
throw chatOwn('This task changed since the page loaded. Check it, then plan again. Your request is still here.');
|
|
1552
1640
|
}
|
|
1553
1641
|
// From here the request belongs to a round, as on the Mac: the composer is cleared and the round keeps the words.
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1642
|
+
// Plan again reuses the failed round in its place (same key, same words), so one request never shows twice, and the
|
|
1643
|
+
// composer's draft is left alone.
|
|
1644
|
+
var base = { key: again ? again.key : 'web-' + uuid(), roundId: '', question: question, answer: '', assignments: [], planner: chatPlanner(), model: '', effort: '', note: '', meter: null,
|
|
1645
|
+
state: 'planning', roundState: '', inHistory: false, states: {}, error: '', at: Date.now(), sentAt: 0, sendUnknown: false, context: '', skipped: {}, offerNewSession: '' };
|
|
1646
|
+
var r = again ? Object.assign(again, base) : base;
|
|
1647
|
+
if (!again) { c.rounds.push(r); c.draft = ''; c.draftAt = Date.now(); chatTouch(c); chatResetComposer(it.wid); }
|
|
1648
|
+
else chatTouch(c);
|
|
1558
1649
|
c.phase = 'Finding this task’s sessions…'; chatRepaint(it.wid);
|
|
1559
1650
|
var meetings = (Array.isArray(it.task.meetingRefs) ? it.task.meetingRefs : []).map(function (m) { return m && typeof m.recordId === 'string' ? 'meeting:' + m.recordId : ''; })
|
|
1560
1651
|
.filter(function (m) { return m && m.length <= 250 && /^meeting:[^\u0000-\u001f]+$/.test(m); }).slice(0, 20);
|
|
1561
|
-
return
|
|
1652
|
+
return loadWorkModels(false).then(function () {
|
|
1653
|
+
return api('/api/work/orchestrator/input', { method: 'POST', body: { workID: wid, meetingWorkIDs: meetings }, timeoutMs: CHAT_TIMEOUT.input });
|
|
1654
|
+
}).then(function (raw) {
|
|
1562
1655
|
var input = chatReadInput(raw);
|
|
1563
1656
|
if (!input || raw.workID !== wid) throw chatOwn('The server’s list of this task’s sessions could not be read. Nothing was sent.');
|
|
1564
1657
|
var body = chatPlanBody(it, question, input, c, wid);
|
|
1658
|
+
// Empty card: the planner is offered exactly one provider for its one New session.
|
|
1659
|
+
var empty = chatNothingToContinue(input.rows) ? chatEmptyCardProvider(body.newSessionProviders, input.usage) : '';
|
|
1660
|
+
if (empty) body.newSessionProviders = [empty];
|
|
1565
1661
|
r.note = chatSessionsNote(input);
|
|
1566
1662
|
r.meter = chatMeter(question, body.context, wid);
|
|
1567
1663
|
r.context = body.context.slice(0, 4000);
|
|
1568
|
-
c.phase = 'Thinking through the request with ' + plannerName(body.planner || 'sonnet') + '…'; chatRepaint(it.wid);
|
|
1664
|
+
c.phase = empty ? chatEmptyPhase(empty) : 'Thinking through the request with ' + plannerName(body.planner || 'sonnet') + '…'; chatRepaint(it.wid);
|
|
1569
1665
|
return api('/api/work/chat', { method: 'POST', body: body, timeoutMs: CHAT_TIMEOUT.plan }).then(function (answer) {
|
|
1570
1666
|
var plan = chatReadPlan(answer, input, body);
|
|
1571
1667
|
if (plan && plan.tooMany) throw chatOwn('The plan has more than 3 assignments, more than COS sends from one plan. Nothing was sent. Plan again.');
|
|
1572
1668
|
if (!plan) throw chatOwn('The plan named a session it may not send to here, or broke a limit. Nothing was sent. Plan again.');
|
|
1573
|
-
|
|
1669
|
+
var planned = plan.assignments;
|
|
1670
|
+
if (empty) {
|
|
1671
|
+
// One New session at most. With it the round stops at Ready to send; without one, the round offers it as one tap.
|
|
1672
|
+
planned = chatOneNewSession(planned);
|
|
1673
|
+
if (planned.some(function (a) { return a.mode === 'newSession'; })) r.note = chatEmptyNote(empty);
|
|
1674
|
+
else if (!planned.length) r.offerNewSession = empty;
|
|
1675
|
+
}
|
|
1676
|
+
r.answer = plan.answer; r.assignments = planned;
|
|
1574
1677
|
r.model = chatClean(answer.model, 100); r.effort = CHAT_EFFORTS.indexOf(answer.effort) >= 0 ? answer.effort : '';
|
|
1575
|
-
r.state =
|
|
1678
|
+
r.state = planned.length ? 'ready' : 'answered';
|
|
1576
1679
|
chatTouch(c);
|
|
1577
1680
|
// The plan is saved to the Mac only on Send. Today's COS Control files every saved round as a plan "from your
|
|
1578
1681
|
// glasses" and posts a notice, so saving on Plan would announce a plan nobody chose to send. Send saves it
|
|
@@ -1771,6 +1874,23 @@
|
|
|
1771
1874
|
chatTouch(c); chatRepaint(it.wid);
|
|
1772
1875
|
return true;
|
|
1773
1876
|
}
|
|
1877
|
+
// "Start a new Claude session with this request" (WorkTaskChat.startNewSession): the tap is the Send. It makes the one New
|
|
1878
|
+
// session assignment (the request as its prompt) and sends it through chatSend, so the server's one-send-per-round rule,
|
|
1879
|
+
// the saved round and COS Control's fences all apply. Once only: the offer is gone as soon as it is tapped.
|
|
1880
|
+
function chatStartNewSession(it, r) {
|
|
1881
|
+
var p = r && r.offerNewSession;
|
|
1882
|
+
if (!p || CHAT_NEW_PROVIDERS.indexOf(p) < 0 || r.state !== 'answered' || r.assignments.length || r.sending || chatOf(it.wid).busy) return Promise.resolve(false);
|
|
1883
|
+
if (!r.question.trim() || r.question.length > 2000 || /COS-WORK/i.test(r.question.replace(/[*_`]/g, ''))) {
|
|
1884
|
+
r.error = 'This request cannot be sent as it is. Edit it and plan again. Nothing was sent.'; chatRepaint(it.wid); return Promise.resolve(false);
|
|
1885
|
+
}
|
|
1886
|
+
r.assignments = [{ id: 'a1', mode: 'newSession', provider: p, prompt: r.question, title: 'New ' + providerName(p) + ' session' }];
|
|
1887
|
+
r.offerNewSession = ''; r.state = 'ready'; r.note = chatEmptyNote(p);
|
|
1888
|
+
chatTouch(chatOf(it.wid));
|
|
1889
|
+
// Lead decision (11/09, Q1 W-D1): two taps on the web, like every send here. This tap opens the usual confirm
|
|
1890
|
+
// ("Send to a new Claude session?"), so the round's record, confirm: two-step, is what happened.
|
|
1891
|
+
chatAskSend(it, r, ['a1']);
|
|
1892
|
+
return Promise.resolve(true);
|
|
1893
|
+
}
|
|
1774
1894
|
function chatAgain(it, r) {
|
|
1775
1895
|
var c = chatOf(it.wid);
|
|
1776
1896
|
if (!r || c.draft.trim() || c.busy) return;
|
|
@@ -1887,6 +2007,13 @@
|
|
|
1887
2007
|
(r.pollStopped ? '<p class="wk-fine">This page stopped checking it. Check again to see whether the Mac sent it.</p><div class="wk-row"><button type="button" class="cx-btn quiet" data-wk="chat-check" data-key="' + esc(r.key) + '">Check again</button></div>' : '');
|
|
1888
2008
|
} else if (r.state === 'quiet') {
|
|
1889
2009
|
actions = '<div class="wk-row"><button type="button" class="cx-btn quiet" data-wk="chat-check" data-key="' + esc(r.key) + '">Check again</button></div>';
|
|
2010
|
+
} else if (r.state === 'answered' && !r.assignments.length && r.offerNewSession) {
|
|
2011
|
+
actions = '<div class="wk-row wk-wrap wk-round-actions"><button type="button" class="cx-btn primary wk-start-new" data-wk="chat-start-new" data-key="' + esc(r.key) + '"' + (r.sending || c.busy ? ' disabled' : '') + '>' +
|
|
2012
|
+
esc(chatStartNewTitle(r.offerNewSession)) + '</button></div>' +
|
|
2013
|
+
'<p class="wk-fine">No session on this task yet, and the plan did not start one. Nothing is sent until you press this.</p>';
|
|
2014
|
+
} else if (chatCanPlanAgain(r)) {
|
|
2015
|
+
actions = '<div class="wk-row"><button type="button" class="cx-btn quiet" data-wk="chat-plan-again" data-key="' + esc(r.key) + '"' + (c.busy ? ' disabled' : '') + '>Plan again</button>' +
|
|
2016
|
+
'<span class="wk-small wk-muted">Plans this request again here. Nothing is sent until you press Send.</span></div>';
|
|
1890
2017
|
} else if (r.state === 'planFailed' || (r.state === 'notDelivered' && !r.roundId)) {
|
|
1891
2018
|
actions = '<div class="wk-row"><button type="button" class="cx-btn quiet" data-wk="chat-again" data-key="' + esc(r.key) + '"' + (c.draft.trim() || c.busy ? ' disabled title="Clear the message box first"' : '') + '>Use this request again</button></div>';
|
|
1892
2019
|
}
|
|
@@ -1899,7 +2026,7 @@
|
|
|
1899
2026
|
var waited = Date.now() - (r.sentAt || Date.now());
|
|
1900
2027
|
var sendingLine = r.state !== 'sending' ? '' : '<p class="wk-small wk-muted">' + esc(r.roundState === 'sending' ? 'COS Control on your Mac is sending it. This page checks every few seconds.'
|
|
1901
2028
|
: r.roundState === 'checking' ? 'COS Control already has a send for this plan. Checking where it is…'
|
|
1902
|
-
: waited < 60000 ? 'Waiting for COS Control on your Mac to send this. Nothing has been sent yet.'
|
|
2029
|
+
: waited < 60000 ? 'Waiting for COS Control on your Mac to send this. COS Control has to be open on the Mac. Nothing has been sent yet.'
|
|
1903
2030
|
: 'Waiting for COS Control on your Mac to send this. It has not picked it up yet: the Mac may be asleep or COS Control closed. The request waits 10 minutes, and nothing is sent until COS Control takes it.') + '</p>';
|
|
1904
2031
|
return '<article class="wk-round" data-key="' + esc(r.key) + '">' +
|
|
1905
2032
|
'<p class="wk-round-q">' + esc(r.question) + '</p>' +
|
|
@@ -2074,14 +2201,28 @@
|
|
|
2074
2201
|
}
|
|
2075
2202
|
return rows;
|
|
2076
2203
|
}
|
|
2204
|
+
// Q1 P2: the list a New session picks from is the Mac's available Claude and Codex models, so what shows as picked is
|
|
2205
|
+
// what runs. A saved destination's model stays listed even when it is not offered now; "Server default" shows only for
|
|
2206
|
+
// a saved destination that chose it.
|
|
2207
|
+
function startModelOptions(st) {
|
|
2208
|
+
var list = WM.models ? WM.models.filter(function (m) { return m.available && (m.provider === 'claude' || m.provider === 'codex'); }).map(function (m) { return [m.id, m.title]; })
|
|
2209
|
+
: MODELS.filter(function (m) { return m[0]; });
|
|
2210
|
+
if (st.model && !list.some(function (m) { return m[0] === st.model; })) list.unshift([st.model, (MODELS.filter(function (m) { return m[0] === st.model; })[0] || [st.model, st.model])[1]]);
|
|
2211
|
+
if (!st.model && st.fromSaved) list.unshift(['', 'Server default']);
|
|
2212
|
+
return list;
|
|
2213
|
+
}
|
|
2077
2214
|
function startState(it) {
|
|
2078
2215
|
var st = S.start[it.wid];
|
|
2079
2216
|
if (st) return st;
|
|
2080
2217
|
var saved = (S.activity[it.wid] || {}).savedDestination, rows = sessionChoices(it);
|
|
2081
|
-
st = { mode: 'newSession', sessionId: '', model:
|
|
2218
|
+
st = { mode: 'newSession', sessionId: '', model: defaultNewModel(WM.models), note: '', sending: false, request: null, error: '', fromSaved: false, modelPicked: false };
|
|
2219
|
+
// As COS Control 0.5.386 (WorkHandoffStore.swift draft(for:) 910-912, defaultDraft 927-937): a saved destination wins;
|
|
2220
|
+
// without one, a card with a session of its own opens on Continue, and a card with none on Start a new session with
|
|
2221
|
+
// Claude's Opus (else Codex's first model).
|
|
2222
|
+
var own = rows.filter(function (c) { return !c.saved; });
|
|
2082
2223
|
if (saved && saved.mode) {
|
|
2083
2224
|
st.mode = saved.mode; st.sessionId = str(saved.sessionId); st.model = str(saved.model); st.fromSaved = true;
|
|
2084
|
-
} else if (
|
|
2225
|
+
} else if (own.length) { st.mode = 'continueSession'; st.sessionId = own[0].sessionId; }
|
|
2085
2226
|
S.start[it.wid] = st;
|
|
2086
2227
|
return st;
|
|
2087
2228
|
}
|
|
@@ -2107,6 +2248,11 @@
|
|
|
2107
2248
|
return;
|
|
2108
2249
|
}
|
|
2109
2250
|
var st = startState(it), blocking = blockingHandoff(it), caps = (S.activity[it.wid] || {}).capabilities || S.openCaps || {};
|
|
2251
|
+
if (!WM.at && !WM.loading) loadWorkModels(false).then(function () {
|
|
2252
|
+
if (!st.fromSaved && !st.modelPicked && st.mode === 'newSession' && !st.request) st.model = defaultNewModel(WM.models) || st.model;
|
|
2253
|
+
var r = W.parseRoute(); if (r.pane === 'work' && r.args[0] === arg && r.args[1] === 'start') renderStart(arg);
|
|
2254
|
+
});
|
|
2255
|
+
if (!WM.models && !WM.failed && !st.model && !st.fromSaved) st.model = 'opus';
|
|
2110
2256
|
var sessions = sessionChoices(it);
|
|
2111
2257
|
var problem = startProblem(it, st), taking = caps.requests === 1;
|
|
2112
2258
|
function choice(mode, title, detail, disabled) {
|
|
@@ -2115,7 +2261,8 @@
|
|
|
2115
2261
|
}
|
|
2116
2262
|
var picker = st.mode === 'newSession'
|
|
2117
2263
|
? '<label class="wk-pick wk-pick-wide"><span>Model</span><select class="field" data-wk-start="model"' + (st.sending || st.request ? ' disabled' : '') + '>' +
|
|
2118
|
-
|
|
2264
|
+
startModelOptions(st).map(function (m) { return '<option value="' + esc(m[0]) + '"' + (m[0] === st.model ? ' selected' : '') + '>' + esc(m[1]) + '</option>'; }).join('') + '</select></label>' +
|
|
2265
|
+
(WM.failed ? '<p class="wk-small wk-muted">COS could not read the models on your Mac. These are the usual ones.</p>' : '')
|
|
2119
2266
|
: '<label class="wk-pick wk-pick-wide"><span>Session</span><select class="field" data-wk-start="session"' + (st.sending || st.request ? ' disabled' : '') + '>' +
|
|
2120
2267
|
(sessions.length ? '' : '<option value="">No session has worked on this card yet</option>') +
|
|
2121
2268
|
sessions.filter(function (c) { return st.mode !== 'fork' || FORKABLE[c.provider]; }).map(function (c) {
|
|
@@ -2125,7 +2272,7 @@
|
|
|
2125
2272
|
if (req) {
|
|
2126
2273
|
var terminal = ['sent', 'refused', 'unresolved', 'unconfirmed', 'expired'].indexOf(req.state) >= 0;
|
|
2127
2274
|
reqHtml = '<div class="wk-handoff" data-tone="' + (req.state === 'sent' ? 'green' : req.state === 'refused' || req.state === 'expired' ? 'danger' : 'accent') + '">' +
|
|
2128
|
-
'<p class="wk-state">' + esc(
|
|
2275
|
+
'<p class="wk-state">' + esc(requestLine(req)) + '</p>' + (req.reason ? '<p class="wk-small">' + esc(req.reason) + '</p>' : '') +
|
|
2129
2276
|
(st.pollStopped ? '<p class="wk-small wk-muted">' + esc(st.pollStopped) + '</p>' : '') +
|
|
2130
2277
|
(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>';
|
|
2131
2278
|
}
|
|
@@ -2313,7 +2460,7 @@
|
|
|
2313
2460
|
if (!S.undoBatch) return;
|
|
2314
2461
|
api('/api/work-intake/undo', { method: 'POST', body: { batchId: S.undoBatch } }).then(function () {
|
|
2315
2462
|
S.undoBatch = null; loadIntake(true); loadBoard(true);
|
|
2316
|
-
}, function (err) { if (err.status === 404) S.loopAvailable = false; S.sortError = err.status === 404 ?
|
|
2463
|
+
}, function (err) { if (err.status === 404) S.loopAvailable = false; S.sortError = err.status === 404 ? UNDO_FAILED : errText(err); render(); });
|
|
2317
2464
|
}
|
|
2318
2465
|
function batchOp(t, action, fields) {
|
|
2319
2466
|
if (S.batchBusy || !(S.caps.workBatch === 1 && S.caps.writable === true)) return;
|
|
@@ -2322,7 +2469,7 @@
|
|
|
2322
2469
|
S.batchBusy = true; S.loopError = ''; render();
|
|
2323
2470
|
api('/api/work-board/batch', { method: 'POST', body: { domain: t.domain, ops: [op] } }).then(function () {
|
|
2324
2471
|
S.batchBusy = false; loadBoard(true); if (S.tab === 'waiting') loadWaiting();
|
|
2325
|
-
}, function (err) { S.batchBusy = false; S.loopError = err.status === 404 ? 'Waiting on
|
|
2472
|
+
}, function (err) { S.batchBusy = false; S.loopError = err.status === 404 ? 'Waiting on is not on this COS server.' : errText(err); render(); });
|
|
2326
2473
|
}
|
|
2327
2474
|
function loadWaiting() {
|
|
2328
2475
|
var mode = S.waitingDropped ? 'dropped' : 'waiting';
|
|
@@ -2330,7 +2477,7 @@
|
|
|
2330
2477
|
var rows = (d && (d.tasks || d.items || d.cards)) || [];
|
|
2331
2478
|
if (mode === 'dropped') S.dropped = rows; else S.waiting = rows;
|
|
2332
2479
|
S.loopError = ''; render();
|
|
2333
|
-
}, function (err) { S.loopError = err.status === 404 ? (mode === 'dropped' ? 'Dropped cards
|
|
2480
|
+
}, function (err) { S.loopError = err.status === 404 ? (mode === 'dropped' ? 'Dropped cards are not on this COS server.' : 'Waiting on is not on this COS server.') : errText(err);
|
|
2334
2481
|
if (mode === 'dropped') S.dropped = []; else S.waiting = []; render(); });
|
|
2335
2482
|
}
|
|
2336
2483
|
// Meaning search: asked only when Return is pressed in the search box (never while typing).
|
|
@@ -2464,6 +2611,10 @@
|
|
|
2464
2611
|
it = current(); if (!it) return;
|
|
2465
2612
|
var draftBox = $('wk-chat-text'); if (draftBox) { chatOf(it.wid).draft = draftBox.value; draftBox.blur(); }
|
|
2466
2613
|
chatPlan(it); return;
|
|
2614
|
+
case 'chat-plan-again': case 'chat-start-new':
|
|
2615
|
+
it = current(); var again = it && chatFindRound(it.wid, t.getAttribute('data-key')); if (!again) return;
|
|
2616
|
+
if (act === 'chat-plan-again') chatPlan(it, again.key); else chatStartNewSession(it, again);
|
|
2617
|
+
return;
|
|
2467
2618
|
case 'chat-send': case 'chat-confirm': case 'chat-cancel': case 'chat-skip': case 'chat-stop': case 'chat-again': case 'chat-check': case 'chat-remove':
|
|
2468
2619
|
it = current(); var round = it && chatFindRound(it.wid, t.getAttribute('data-key')); if (!round) return;
|
|
2469
2620
|
var ids = String(t.getAttribute('data-ids') || '').split(',').filter(Boolean);
|
|
@@ -2555,7 +2706,7 @@
|
|
|
2555
2706
|
var rows = sessionChoices(it).filter(function (c) { return st.mode !== 'fork' || FORKABLE[c.provider]; });
|
|
2556
2707
|
if (st.mode !== 'newSession' && !rows.some(function (c) { return c.sessionId === st.sessionId; })) st.sessionId = rows.length ? rows[0].sessionId : '';
|
|
2557
2708
|
} else if (el.getAttribute('data-wk-start') === 'session') { st.sessionId = el.value; st.fromSaved = false; }
|
|
2558
|
-
else if (el.getAttribute('data-wk-start') === 'model') { st.model = el.value; st.fromSaved = false; }
|
|
2709
|
+
else if (el.getAttribute('data-wk-start') === 'model') { st.model = el.value; st.fromSaved = false; st.modelPicked = true; }
|
|
2559
2710
|
return renderStart(it.id);
|
|
2560
2711
|
}
|
|
2561
2712
|
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); } }
|
|
@@ -2671,9 +2822,9 @@
|
|
|
2671
2822
|
},
|
|
2672
2823
|
leave: function () { stopPoll(); stopHandoffPolls(); chatSave(true); },
|
|
2673
2824
|
// For web-control/work-composer-web.test.ts: Work chat's steps, run against a fake server.
|
|
2674
|
-
_chat: { S: S, of: chatOf, plan: chatPlan, send: chatSend, stop: chatStop, again: chatAgain, poll: chatPoll, caps: loadChatCaps, readCaps: chatReadCaps,
|
|
2825
|
+
_chat: { S: S, WM: WM, catalog: workModelCatalog, of: chatOf, plan: chatPlan, send: chatSend, stop: chatStop, again: chatAgain, poll: chatPoll, caps: loadChatCaps, readCaps: chatReadCaps,
|
|
2675
2826
|
find: chatFindRound, planBody: chatPlanBody, context: chatContext, meter: chatMeter, readPlan: chatReadPlan, readInput: chatReadInput,
|
|
2676
|
-
store: function () { return chatStore; }, save: chatSave, timers: CHAT_TIMERS, render: renderDetail, restore: chatRestored, item: itemFor,
|
|
2827
|
+
store: function () { return chatStore; }, save: chatSave, timers: CHAT_TIMERS, render: renderDetail, renderStart: renderStart, sortView: sortView, restore: chatRestored, item: itemFor,
|
|
2677
2828
|
mergeIn: function () { chatMergeIn(chatReadDisk()); }, limits: { questionChars: CHAT_MAX, pollMaxMs: CHAT_POLL.max } },
|
|
2678
2829
|
// The Ask sheet's "Find a task about this": open the board on this query and ask by meaning.
|
|
2679
2830
|
search: function (q) { S.q = String(q || ''); S.tab = 'board'; render(); searchMeaning(); },
|
|
@@ -77,10 +77,13 @@
|
|
|
77
77
|
}
|
|
78
78
|
// About 16 reads: never two runs at once, never more than one a minute, two at a time through storyApi, and never
|
|
79
79
|
// ahead of the pane the person just opened (Safari keeps 6 connections to the Mac over HTTP/1.1).
|
|
80
|
-
//
|
|
81
|
-
// when the menu opens rather than costing the pane its connections on every visit.
|
|
80
|
+
// Below 600 px wide (the folded Duo) the rail is hidden and the stories show only inside the section menu, so they
|
|
81
|
+
// load when the menu opens rather than costing the pane its connections on every visit. This is the WIDTH test on
|
|
82
|
+
// purpose, not W.isPhone: from 7.0.2 a short touch screen at any width counts as a phone (the unfolded Duo, 951x539),
|
|
83
|
+
// and it still has the rail, its story lines and hot dots.
|
|
84
|
+
function railHidden() { return !window.matchMedia('(min-width: 600px)').matches; }
|
|
82
85
|
function loadStories(force) {
|
|
83
|
-
if (!force &&
|
|
86
|
+
if (!force && railHidden()) return;
|
|
84
87
|
if (Date.now() - storiesAt < 60000) return;
|
|
85
88
|
storiesAt = Date.now();
|
|
86
89
|
var now = new Date();
|
|
@@ -210,17 +213,17 @@
|
|
|
210
213
|
setStory('threads', active + ' active' + (list[0] && list[0].name ? ', newest ' + clip(list[0].name, 40) : ''), false);
|
|
211
214
|
});
|
|
212
215
|
|
|
213
|
-
// Memories (TodayHome.memoriesStory): n is the
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
var n =
|
|
216
|
+
// Memories (TodayHome.memoriesStory): n is the needs-review count (capped, from the bridge's
|
|
217
|
+
// status, which runs the same CLI Control's helper runs, judged by memory-review.js); total is
|
|
218
|
+
// every item that needs review by the same predicate, never the To review list's length (that
|
|
219
|
+
// list also holds suggestions and proposals with no decision waiting). "Next n of total" when
|
|
220
|
+
// more need review than are shown, else "n to review".
|
|
221
|
+
soft(storyApi('/api/web-bridge/memories', { method: 'POST', body: { id: 'rail-status', op: 'status', args: {} } })).then(function (r) {
|
|
222
|
+
var d = r && r.details ? r.details : {};
|
|
223
|
+
var n = d.learningToReview;
|
|
221
224
|
if (typeof n !== 'number') return setStory('memories', FALLBACK.memories, false);
|
|
222
225
|
if (n <= 0) return setStory('memories', 'Nothing to review', false);
|
|
223
|
-
var total =
|
|
226
|
+
var total = typeof d.learningToReviewTotal === 'number' ? d.learningToReviewTotal : null;
|
|
224
227
|
setStory('memories', total != null && total > n ? 'Next ' + n + ' of ' + total : n + ' to review', true);
|
|
225
228
|
});
|
|
226
229
|
// Dictation lives on the Mac (its history and suggestions are local files).
|
|
@@ -260,9 +263,9 @@
|
|
|
260
263
|
}
|
|
261
264
|
crumbs.innerHTML = parts.join('');
|
|
262
265
|
$('nav-back').disabled = onToday;
|
|
263
|
-
// The phone top bar names the detail too, so a reader says where it is.
|
|
266
|
+
// The phone top bar (below 600 px, the same width test) names the detail too, so a reader says where it is.
|
|
264
267
|
var label = $('section-name');
|
|
265
|
-
if (label && detail &&
|
|
268
|
+
if (label && detail && railHidden()) label.textContent = detail;
|
|
266
269
|
}
|
|
267
270
|
|
|
268
271
|
document.addEventListener('click', function (e) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"v": 1,
|
|
3
|
+
"algorithm": "sha256",
|
|
4
|
+
"files": {
|
|
5
|
+
"/index.html": "3bea8ab14118e48c8bbf5f09de2e8b65ca1225b7e7b564982a84477bafc8341e",
|
|
6
|
+
"/signer-proof.js": "a8f8f10608fa1ec0e8da0280fdb27c16c78c011e6ff935477593e25a8e6a26e0",
|
|
7
|
+
"/signer-store.js": "ae123328f922a509eea4d8f9e8440c9ef6972eeb14bf90b9e8bde3c84f7132bc",
|
|
8
|
+
"/signer.css": "e19f5ec70c9dae28347ad40cd08742842e964b3d0b973a5c39f48f9cafdd319b",
|
|
9
|
+
"/signer.js": "c92dae47694a42aec0995a8cb0e70ba54979183711d840b2935528a197b11735"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="referrer" content="no-referrer">
|
|
7
|
+
<title>COS approval</title>
|
|
8
|
+
<link rel="stylesheet" href="/signer.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<main>
|
|
12
|
+
<h1>COS approval</h1>
|
|
13
|
+
|
|
14
|
+
<section id="loading"><p>Loading.</p></section>
|
|
15
|
+
|
|
16
|
+
<section id="framed" hidden>
|
|
17
|
+
<p>This page only works when you open it directly. Nothing was signed.</p>
|
|
18
|
+
</section>
|
|
19
|
+
|
|
20
|
+
<section id="enroll" hidden>
|
|
21
|
+
<h2>Set up this phone</h2>
|
|
22
|
+
<p>This lets you approve messages to your Mac with a user-verified passkey on this phone. Nothing else is needed. The key stays on this phone.</p>
|
|
23
|
+
<p>This phone will show a 6-digit code. Type it in COS Control on your Mac, compare the key fingerprints, then approve with Touch ID.</p>
|
|
24
|
+
<label for="enroll-label">Name for this phone</label>
|
|
25
|
+
<input id="enroll-label" type="text" maxlength="64" autocomplete="off" value="iPhone">
|
|
26
|
+
<button type="button" id="enroll-start">Set up phone approval</button>
|
|
27
|
+
<button type="button" id="enroll-confirm" hidden>Confirm with passkey</button>
|
|
28
|
+
<p class="status" id="enroll-status" role="status"></p>
|
|
29
|
+
</section>
|
|
30
|
+
|
|
31
|
+
<section id="pending" hidden>
|
|
32
|
+
<h2>Type this code on your Mac</h2>
|
|
33
|
+
<p class="code" id="pending-code"></p>
|
|
34
|
+
<p>Key fingerprint <span class="mono" id="pending-fingerprint"></span></p>
|
|
35
|
+
<p>In COS Control on your Mac, type this code and check that its key fingerprint matches this phone. Then approve with Touch ID. This page updates when you do.</p>
|
|
36
|
+
</section>
|
|
37
|
+
|
|
38
|
+
<section id="sign" hidden>
|
|
39
|
+
<h2>You are about to approve</h2>
|
|
40
|
+
<p class="label">Words</p>
|
|
41
|
+
<pre id="sign-words"></pre>
|
|
42
|
+
<dl>
|
|
43
|
+
<dt>Sent to</dt><dd id="sign-provider"></dd>
|
|
44
|
+
<dt>Session</dt><dd class="mono" id="sign-session"></dd>
|
|
45
|
+
<dt>What it allows</dt><dd id="sign-scope"></dd>
|
|
46
|
+
</dl>
|
|
47
|
+
<pre id="sign-packet" hidden></pre>
|
|
48
|
+
<button type="button" id="sign-approve">Approve with phone passkey</button>
|
|
49
|
+
<button type="button" id="sign-cancel" class="quiet">Cancel approval</button>
|
|
50
|
+
<p class="status" id="sign-status" role="status"></p>
|
|
51
|
+
</section>
|
|
52
|
+
|
|
53
|
+
<section id="done" hidden>
|
|
54
|
+
<p id="done-text"></p>
|
|
55
|
+
<p><a id="done-back" href="#">Back to Control</a></p>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<section id="idle" hidden>
|
|
59
|
+
<p id="idle-text">This phone has a key. Start an approval from a session in Control.</p>
|
|
60
|
+
<p><span id="key-label"></span> <span class="mono" id="key-fingerprint"></span></p>
|
|
61
|
+
<button type="button" id="key-revoke" class="quiet">Remove this phone’s key</button>
|
|
62
|
+
</section>
|
|
63
|
+
|
|
64
|
+
<section id="problem" hidden>
|
|
65
|
+
<p id="problem-text"></p>
|
|
66
|
+
</section>
|
|
67
|
+
|
|
68
|
+
<p class="foot"><a id="back" href="#">Back to Control</a></p>
|
|
69
|
+
</main>
|
|
70
|
+
<script src="/signer-proof.js"></script>
|
|
71
|
+
<script src="/signer-store.js"></script>
|
|
72
|
+
<script src="/signer.js"></script>
|
|
73
|
+
</body>
|
|
74
|
+
</html>
|