@gotcos/glasses-server 7.0.7 → 7.0.27
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 +19 -0
- package/CHANGELOG.md +305 -0
- package/docs/web-key-verifier-contract.md +378 -0
- package/managed-runtime-contract.json +9 -1
- package/package.json +10 -3
- package/server/bootstrap.ts +10 -0
- package/server/index.ts +129 -23
- package/server/lib/agent-session-older.ts +218 -0
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +102 -20
- package/server/lib/api-auth.ts +37 -1
- package/server/lib/calendar-meetings.ts +70 -0
- package/server/lib/calendar-source.ts +54 -9
- package/server/lib/claude-hooks-installer.ts +111 -12
- package/server/lib/conversation.ts +56 -28
- package/server/lib/cors-reject-log.ts +51 -0
- package/server/lib/cos-started-sessions.ts +101 -0
- package/server/lib/disk-io-meter.ts +202 -0
- package/server/lib/drop-service.ts +47 -12
- package/server/lib/drop-store.ts +21 -1
- 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/list-model-memo.ts +85 -0
- package/server/lib/listener-startup.ts +18 -0
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- package/server/lib/model-efforts.ts +40 -0
- package/server/lib/permission-broker.ts +443 -11
- package/server/lib/secret-path-guard.ts +4 -1
- package/server/lib/session-cache-writer.ts +8 -1
- package/server/lib/session-hook-events.ts +23 -1
- package/server/lib/session-hooks-runtime.ts +34 -0
- package/server/lib/session-log.ts +313 -4
- package/server/lib/session-organizer.ts +305 -0
- package/server/lib/session-state-derive.ts +36 -0
- package/server/lib/session-stream-events.ts +39 -3
- package/server/lib/thread-turn-queue.ts +24 -0
- package/server/lib/today-brief.ts +522 -58
- 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-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-orchestrator/snapshot.ts +17 -3
- package/server/lib/work-review-runtime.ts +17 -4
- package/server/routes/agent-session-bindings.ts +16 -1
- package/server/routes/agent-session-stream.ts +7 -0
- package/server/routes/agent-sessions.ts +106 -2
- package/server/routes/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +14 -1
- package/server/routes/live-events.ts +6 -1
- package/server/routes/permission-broker.ts +26 -0
- package/server/routes/query-jobs.ts +3 -0
- package/server/routes/session-organizer.ts +149 -0
- package/server/routes/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +50 -14
- package/server/routes/today-brief.ts +91 -36
- package/server/routes/web-control.ts +37 -4
- package/server/routes/web-key.ts +284 -0
- package/server/routes/welcome-context.ts +6 -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 +45 -0
- package/server/web-control/app.js +1680 -108
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/index.html +54 -17
- package/server/web-control/panes/cos-chat.css +52 -6
- package/server/web-control/panes/cos-chat.js +364 -32
- package/server/web-control/panes/questions.js +135 -14
- package/server/web-control/panes/sessions.css +280 -0
- package/server/web-control/panes/settings.js +1 -1
- package/server/web-control/panes/today-actions.css +35 -0
- package/server/web-control/panes/today-actions.js +121 -6
- package/server/web-control/panes/work.css +3 -0
- package/server/web-control/panes/work.js +185 -38
- package/server/web-control/theme.js +33 -0
- 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
|
}
|
|
@@ -1266,9 +1320,10 @@
|
|
|
1266
1320
|
state: CHAT_ROUND_WORD[r.state] ? r.state : 'planFailed', roundState: chatClean(r.roundState, 40), inHistory: r.inHistory === true,
|
|
1267
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,
|
|
1268
1322
|
sendUnknown: r.sendUnknown === true, context: chatClean(r.context, 4000), skipped: r.skipped && typeof r.skipped === 'object' ? r.skipped : {},
|
|
1269
|
-
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 : '' };
|
|
1270
1325
|
// A plan in flight lives only in this page. After a reload it is gone, and nothing was saved or sent for it.
|
|
1271
|
-
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.'; }
|
|
1272
1327
|
return out;
|
|
1273
1328
|
}
|
|
1274
1329
|
function chatOf(wid) {
|
|
@@ -1336,7 +1391,8 @@
|
|
|
1336
1391
|
function chatStored(r) {
|
|
1337
1392
|
return { key: r.key, roundId: r.roundId, question: r.question, answer: r.answer, assignments: r.assignments, planner: r.planner, model: r.model,
|
|
1338
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,
|
|
1339
|
-
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 || '' };
|
|
1340
1396
|
}
|
|
1341
1397
|
function chatTrim(rounds) {
|
|
1342
1398
|
var newest = rounds.slice(-CHAT_KEEP.rounds);
|
|
@@ -1459,6 +1515,29 @@
|
|
|
1459
1515
|
return 'COS found ' + total + ' session' + (total === 1 ? '' : 's') + ' for this task, none can take a Continue now' +
|
|
1460
1516
|
(named.length ? ': ' + named.join('; ') + (more > 0 ? '; and ' + more + ' more' : '') : '') + '. The planner can fork one or start a new session.';
|
|
1461
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
|
+
|
|
1462
1541
|
// The planner input, exactly as the server's parseWorkChatInput reads an orchestrator client's (Control 0.5.333).
|
|
1463
1542
|
function chatPlanBody(it, question, input, c, wid) {
|
|
1464
1543
|
var t = it.task, ids = {};
|
|
@@ -1476,10 +1555,9 @@
|
|
|
1476
1555
|
var st = (last.states || {})[a.id] || {};
|
|
1477
1556
|
return { sessionID: a.sessionID, status: chatClean(CHAT_ASSIGN_WORD[st.state] || 'Not sent', 100), detail: chatClean(st.detail, 2000), result: '' };
|
|
1478
1557
|
}) : [];
|
|
1479
|
-
var exhausted = {}; input.usage.forEach(function (u) { if (u.state === 'exhausted') exhausted[u.provider] = 1; });
|
|
1480
1558
|
var body = { workID: wid, revision: str(t.workRevision), question: question, context: chatContext(t, wid), sessions: sessions, history: history, updates: updates,
|
|
1481
1559
|
mode: 'plan', orchestrator: true, now: new Date().toISOString(), usage: input.usage, leftOut: input.leftOut,
|
|
1482
|
-
newSessionProviders:
|
|
1560
|
+
newSessionProviders: newSessionProviders(WM.models, input.usage) };
|
|
1483
1561
|
var planner = chatPlanner(), effort = chatEffort();
|
|
1484
1562
|
if (planner) body.planner = planner; // only when the server advertised a choice, as Control does
|
|
1485
1563
|
if (effort) body.effort = effort;
|
|
@@ -1544,8 +1622,10 @@
|
|
|
1544
1622
|
if (c.rounds.some(function (r) { return r.state === 'planning'; })) return 'A plan is still being made for this card.';
|
|
1545
1623
|
return '';
|
|
1546
1624
|
}
|
|
1547
|
-
function chatPlan(it) {
|
|
1548
|
-
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();
|
|
1549
1629
|
if (c.busy || !question || chatProblem(it, c)) return Promise.resolve(false);
|
|
1550
1630
|
var wid = chatWorkId(it.task);
|
|
1551
1631
|
c.busy = true; c.error = ''; c.phase = 'Reading the task and its connections…';
|
|
@@ -1559,28 +1639,43 @@
|
|
|
1559
1639
|
throw chatOwn('This task changed since the page loaded. Check it, then plan again. Your request is still here.');
|
|
1560
1640
|
}
|
|
1561
1641
|
// From here the request belongs to a round, as on the Mac: the composer is cleared and the round keeps the words.
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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);
|
|
1566
1649
|
c.phase = 'Finding this task’s sessions…'; chatRepaint(it.wid);
|
|
1567
1650
|
var meetings = (Array.isArray(it.task.meetingRefs) ? it.task.meetingRefs : []).map(function (m) { return m && typeof m.recordId === 'string' ? 'meeting:' + m.recordId : ''; })
|
|
1568
1651
|
.filter(function (m) { return m && m.length <= 250 && /^meeting:[^\u0000-\u001f]+$/.test(m); }).slice(0, 20);
|
|
1569
|
-
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) {
|
|
1570
1655
|
var input = chatReadInput(raw);
|
|
1571
1656
|
if (!input || raw.workID !== wid) throw chatOwn('The server’s list of this task’s sessions could not be read. Nothing was sent.');
|
|
1572
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];
|
|
1573
1661
|
r.note = chatSessionsNote(input);
|
|
1574
1662
|
r.meter = chatMeter(question, body.context, wid);
|
|
1575
1663
|
r.context = body.context.slice(0, 4000);
|
|
1576
|
-
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);
|
|
1577
1665
|
return api('/api/work/chat', { method: 'POST', body: body, timeoutMs: CHAT_TIMEOUT.plan }).then(function (answer) {
|
|
1578
1666
|
var plan = chatReadPlan(answer, input, body);
|
|
1579
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.');
|
|
1580
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.');
|
|
1581
|
-
|
|
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;
|
|
1582
1677
|
r.model = chatClean(answer.model, 100); r.effort = CHAT_EFFORTS.indexOf(answer.effort) >= 0 ? answer.effort : '';
|
|
1583
|
-
r.state =
|
|
1678
|
+
r.state = planned.length ? 'ready' : 'answered';
|
|
1584
1679
|
chatTouch(c);
|
|
1585
1680
|
// The plan is saved to the Mac only on Send. Today's COS Control files every saved round as a plan "from your
|
|
1586
1681
|
// glasses" and posts a notice, so saving on Plan would announce a plan nobody chose to send. Send saves it
|
|
@@ -1779,6 +1874,23 @@
|
|
|
1779
1874
|
chatTouch(c); chatRepaint(it.wid);
|
|
1780
1875
|
return true;
|
|
1781
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
|
+
}
|
|
1782
1894
|
function chatAgain(it, r) {
|
|
1783
1895
|
var c = chatOf(it.wid);
|
|
1784
1896
|
if (!r || c.draft.trim() || c.busy) return;
|
|
@@ -1895,6 +2007,13 @@
|
|
|
1895
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>' : '');
|
|
1896
2008
|
} else if (r.state === 'quiet') {
|
|
1897
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>';
|
|
1898
2017
|
} else if (r.state === 'planFailed' || (r.state === 'notDelivered' && !r.roundId)) {
|
|
1899
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>';
|
|
1900
2019
|
}
|
|
@@ -1907,7 +2026,7 @@
|
|
|
1907
2026
|
var waited = Date.now() - (r.sentAt || Date.now());
|
|
1908
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.'
|
|
1909
2028
|
: r.roundState === 'checking' ? 'COS Control already has a send for this plan. Checking where it is…'
|
|
1910
|
-
: 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.'
|
|
1911
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>';
|
|
1912
2031
|
return '<article class="wk-round" data-key="' + esc(r.key) + '">' +
|
|
1913
2032
|
'<p class="wk-round-q">' + esc(r.question) + '</p>' +
|
|
@@ -1979,7 +2098,9 @@
|
|
|
1979
2098
|
chatGrow(box);
|
|
1980
2099
|
}
|
|
1981
2100
|
function chatGrow(box) {
|
|
1982
|
-
|
|
2101
|
+
// The visible height the one way Control Web reads it (COSWeb.viewportFit): a visible height of 0 (the unfolded Duo
|
|
2102
|
+
// after a keyboard closed, 11/10) reads as innerHeight, never as a 96 px box.
|
|
2103
|
+
var fit = W.viewportFit ? W.viewportFit() : null, h = fit ? fit.height : window.innerHeight || 600;
|
|
1983
2104
|
var max = Math.max(96, Math.min(320, Math.round(h * 0.42)));
|
|
1984
2105
|
box.style.height = 'auto';
|
|
1985
2106
|
box.style.height = Math.min(box.scrollHeight + 2, max) + 'px';
|
|
@@ -1992,9 +2113,11 @@
|
|
|
1992
2113
|
// With the on-screen keyboard up, keep the text box and the Plan row inside what is visible: scroll only as much as
|
|
1993
2114
|
// needed, never past the top of the box. On a folded or unfolded Duo the visual viewport tells us what is visible.
|
|
1994
2115
|
function chatKeepInView() {
|
|
1995
|
-
var box = $('wk-chat-text'), row = $('wk-cmp-actions'),
|
|
2116
|
+
var box = $('wk-chat-text'), row = $('wk-cmp-actions'), fit = W.viewportFit ? W.viewportFit() : null;
|
|
1996
2117
|
if (!box || !row || document.activeElement !== box) return;
|
|
1997
|
-
|
|
2118
|
+
// Through viewportFit, so a visible height of 0 (11/10) never puts the visible bottom above its top and scrolls the
|
|
2119
|
+
// page away; it reads as innerHeight from offset 0.
|
|
2120
|
+
var top = fit ? fit.top : 0, height = fit ? fit.height : window.innerHeight;
|
|
1998
2121
|
var bar = document.querySelector('.topbar'), barBottom = bar ? bar.getBoundingClientRect().bottom : 0;
|
|
1999
2122
|
var visTop = Math.max(top, barBottom > top ? barBottom : top) + 6, visBottom = top + height - 8;
|
|
2000
2123
|
var boxTop = box.getBoundingClientRect().top, rowBottom = row.getBoundingClientRect().bottom, delta = 0;
|
|
@@ -2082,14 +2205,28 @@
|
|
|
2082
2205
|
}
|
|
2083
2206
|
return rows;
|
|
2084
2207
|
}
|
|
2208
|
+
// Q1 P2: the list a New session picks from is the Mac's available Claude and Codex models, so what shows as picked is
|
|
2209
|
+
// what runs. A saved destination's model stays listed even when it is not offered now; "Server default" shows only for
|
|
2210
|
+
// a saved destination that chose it.
|
|
2211
|
+
function startModelOptions(st) {
|
|
2212
|
+
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]; })
|
|
2213
|
+
: MODELS.filter(function (m) { return m[0]; });
|
|
2214
|
+
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]]);
|
|
2215
|
+
if (!st.model && st.fromSaved) list.unshift(['', 'Server default']);
|
|
2216
|
+
return list;
|
|
2217
|
+
}
|
|
2085
2218
|
function startState(it) {
|
|
2086
2219
|
var st = S.start[it.wid];
|
|
2087
2220
|
if (st) return st;
|
|
2088
2221
|
var saved = (S.activity[it.wid] || {}).savedDestination, rows = sessionChoices(it);
|
|
2089
|
-
st = { mode: 'newSession', sessionId: '', model:
|
|
2222
|
+
st = { mode: 'newSession', sessionId: '', model: defaultNewModel(WM.models), note: '', sending: false, request: null, error: '', fromSaved: false, modelPicked: false };
|
|
2223
|
+
// As COS Control 0.5.386 (WorkHandoffStore.swift draft(for:) 910-912, defaultDraft 927-937): a saved destination wins;
|
|
2224
|
+
// without one, a card with a session of its own opens on Continue, and a card with none on Start a new session with
|
|
2225
|
+
// Claude's Opus (else Codex's first model).
|
|
2226
|
+
var own = rows.filter(function (c) { return !c.saved; });
|
|
2090
2227
|
if (saved && saved.mode) {
|
|
2091
2228
|
st.mode = saved.mode; st.sessionId = str(saved.sessionId); st.model = str(saved.model); st.fromSaved = true;
|
|
2092
|
-
} else if (
|
|
2229
|
+
} else if (own.length) { st.mode = 'continueSession'; st.sessionId = own[0].sessionId; }
|
|
2093
2230
|
S.start[it.wid] = st;
|
|
2094
2231
|
return st;
|
|
2095
2232
|
}
|
|
@@ -2115,6 +2252,11 @@
|
|
|
2115
2252
|
return;
|
|
2116
2253
|
}
|
|
2117
2254
|
var st = startState(it), blocking = blockingHandoff(it), caps = (S.activity[it.wid] || {}).capabilities || S.openCaps || {};
|
|
2255
|
+
if (!WM.at && !WM.loading) loadWorkModels(false).then(function () {
|
|
2256
|
+
if (!st.fromSaved && !st.modelPicked && st.mode === 'newSession' && !st.request) st.model = defaultNewModel(WM.models) || st.model;
|
|
2257
|
+
var r = W.parseRoute(); if (r.pane === 'work' && r.args[0] === arg && r.args[1] === 'start') renderStart(arg);
|
|
2258
|
+
});
|
|
2259
|
+
if (!WM.models && !WM.failed && !st.model && !st.fromSaved) st.model = 'opus';
|
|
2118
2260
|
var sessions = sessionChoices(it);
|
|
2119
2261
|
var problem = startProblem(it, st), taking = caps.requests === 1;
|
|
2120
2262
|
function choice(mode, title, detail, disabled) {
|
|
@@ -2123,7 +2265,8 @@
|
|
|
2123
2265
|
}
|
|
2124
2266
|
var picker = st.mode === 'newSession'
|
|
2125
2267
|
? '<label class="wk-pick wk-pick-wide"><span>Model</span><select class="field" data-wk-start="model"' + (st.sending || st.request ? ' disabled' : '') + '>' +
|
|
2126
|
-
|
|
2268
|
+
startModelOptions(st).map(function (m) { return '<option value="' + esc(m[0]) + '"' + (m[0] === st.model ? ' selected' : '') + '>' + esc(m[1]) + '</option>'; }).join('') + '</select></label>' +
|
|
2269
|
+
(WM.failed ? '<p class="wk-small wk-muted">COS could not read the models on your Mac. These are the usual ones.</p>' : '')
|
|
2127
2270
|
: '<label class="wk-pick wk-pick-wide"><span>Session</span><select class="field" data-wk-start="session"' + (st.sending || st.request ? ' disabled' : '') + '>' +
|
|
2128
2271
|
(sessions.length ? '' : '<option value="">No session has worked on this card yet</option>') +
|
|
2129
2272
|
sessions.filter(function (c) { return st.mode !== 'fork' || FORKABLE[c.provider]; }).map(function (c) {
|
|
@@ -2133,7 +2276,7 @@
|
|
|
2133
2276
|
if (req) {
|
|
2134
2277
|
var terminal = ['sent', 'refused', 'unresolved', 'unconfirmed', 'expired'].indexOf(req.state) >= 0;
|
|
2135
2278
|
reqHtml = '<div class="wk-handoff" data-tone="' + (req.state === 'sent' ? 'green' : req.state === 'refused' || req.state === 'expired' ? 'danger' : 'accent') + '">' +
|
|
2136
|
-
'<p class="wk-state">' + esc(
|
|
2279
|
+
'<p class="wk-state">' + esc(requestLine(req)) + '</p>' + (req.reason ? '<p class="wk-small">' + esc(req.reason) + '</p>' : '') +
|
|
2137
2280
|
(st.pollStopped ? '<p class="wk-small wk-muted">' + esc(st.pollStopped) + '</p>' : '') +
|
|
2138
2281
|
(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>';
|
|
2139
2282
|
}
|
|
@@ -2321,7 +2464,7 @@
|
|
|
2321
2464
|
if (!S.undoBatch) return;
|
|
2322
2465
|
api('/api/work-intake/undo', { method: 'POST', body: { batchId: S.undoBatch } }).then(function () {
|
|
2323
2466
|
S.undoBatch = null; loadIntake(true); loadBoard(true);
|
|
2324
|
-
}, function (err) { if (err.status === 404) S.loopAvailable = false; S.sortError = err.status === 404 ?
|
|
2467
|
+
}, function (err) { if (err.status === 404) S.loopAvailable = false; S.sortError = err.status === 404 ? UNDO_FAILED : errText(err); render(); });
|
|
2325
2468
|
}
|
|
2326
2469
|
function batchOp(t, action, fields) {
|
|
2327
2470
|
if (S.batchBusy || !(S.caps.workBatch === 1 && S.caps.writable === true)) return;
|
|
@@ -2330,7 +2473,7 @@
|
|
|
2330
2473
|
S.batchBusy = true; S.loopError = ''; render();
|
|
2331
2474
|
api('/api/work-board/batch', { method: 'POST', body: { domain: t.domain, ops: [op] } }).then(function () {
|
|
2332
2475
|
S.batchBusy = false; loadBoard(true); if (S.tab === 'waiting') loadWaiting();
|
|
2333
|
-
}, function (err) { S.batchBusy = false; S.loopError = err.status === 404 ? 'Waiting on
|
|
2476
|
+
}, function (err) { S.batchBusy = false; S.loopError = err.status === 404 ? 'Waiting on is not on this COS server.' : errText(err); render(); });
|
|
2334
2477
|
}
|
|
2335
2478
|
function loadWaiting() {
|
|
2336
2479
|
var mode = S.waitingDropped ? 'dropped' : 'waiting';
|
|
@@ -2338,7 +2481,7 @@
|
|
|
2338
2481
|
var rows = (d && (d.tasks || d.items || d.cards)) || [];
|
|
2339
2482
|
if (mode === 'dropped') S.dropped = rows; else S.waiting = rows;
|
|
2340
2483
|
S.loopError = ''; render();
|
|
2341
|
-
}, function (err) { S.loopError = err.status === 404 ? (mode === 'dropped' ? 'Dropped cards
|
|
2484
|
+
}, 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);
|
|
2342
2485
|
if (mode === 'dropped') S.dropped = []; else S.waiting = []; render(); });
|
|
2343
2486
|
}
|
|
2344
2487
|
// Meaning search: asked only when Return is pressed in the search box (never while typing).
|
|
@@ -2472,6 +2615,10 @@
|
|
|
2472
2615
|
it = current(); if (!it) return;
|
|
2473
2616
|
var draftBox = $('wk-chat-text'); if (draftBox) { chatOf(it.wid).draft = draftBox.value; draftBox.blur(); }
|
|
2474
2617
|
chatPlan(it); return;
|
|
2618
|
+
case 'chat-plan-again': case 'chat-start-new':
|
|
2619
|
+
it = current(); var again = it && chatFindRound(it.wid, t.getAttribute('data-key')); if (!again) return;
|
|
2620
|
+
if (act === 'chat-plan-again') chatPlan(it, again.key); else chatStartNewSession(it, again);
|
|
2621
|
+
return;
|
|
2475
2622
|
case 'chat-send': case 'chat-confirm': case 'chat-cancel': case 'chat-skip': case 'chat-stop': case 'chat-again': case 'chat-check': case 'chat-remove':
|
|
2476
2623
|
it = current(); var round = it && chatFindRound(it.wid, t.getAttribute('data-key')); if (!round) return;
|
|
2477
2624
|
var ids = String(t.getAttribute('data-ids') || '').split(',').filter(Boolean);
|
|
@@ -2563,7 +2710,7 @@
|
|
|
2563
2710
|
var rows = sessionChoices(it).filter(function (c) { return st.mode !== 'fork' || FORKABLE[c.provider]; });
|
|
2564
2711
|
if (st.mode !== 'newSession' && !rows.some(function (c) { return c.sessionId === st.sessionId; })) st.sessionId = rows.length ? rows[0].sessionId : '';
|
|
2565
2712
|
} else if (el.getAttribute('data-wk-start') === 'session') { st.sessionId = el.value; st.fromSaved = false; }
|
|
2566
|
-
else if (el.getAttribute('data-wk-start') === 'model') { st.model = el.value; st.fromSaved = false; }
|
|
2713
|
+
else if (el.getAttribute('data-wk-start') === 'model') { st.model = el.value; st.fromSaved = false; st.modelPicked = true; }
|
|
2567
2714
|
return renderStart(it.id);
|
|
2568
2715
|
}
|
|
2569
2716
|
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); } }
|
|
@@ -2679,9 +2826,9 @@
|
|
|
2679
2826
|
},
|
|
2680
2827
|
leave: function () { stopPoll(); stopHandoffPolls(); chatSave(true); },
|
|
2681
2828
|
// For web-control/work-composer-web.test.ts: Work chat's steps, run against a fake server.
|
|
2682
|
-
_chat: { S: S, of: chatOf, plan: chatPlan, send: chatSend, stop: chatStop, again: chatAgain, poll: chatPoll, caps: loadChatCaps, readCaps: chatReadCaps,
|
|
2829
|
+
_chat: { S: S, WM: WM, catalog: workModelCatalog, of: chatOf, plan: chatPlan, send: chatSend, stop: chatStop, again: chatAgain, poll: chatPoll, caps: loadChatCaps, readCaps: chatReadCaps,
|
|
2683
2830
|
find: chatFindRound, planBody: chatPlanBody, context: chatContext, meter: chatMeter, readPlan: chatReadPlan, readInput: chatReadInput,
|
|
2684
|
-
store: function () { return chatStore; }, save: chatSave, timers: CHAT_TIMERS, render: renderDetail, restore: chatRestored, item: itemFor,
|
|
2831
|
+
store: function () { return chatStore; }, save: chatSave, timers: CHAT_TIMERS, render: renderDetail, renderStart: renderStart, sortView: sortView, restore: chatRestored, item: itemFor,
|
|
2685
2832
|
mergeIn: function () { chatMergeIn(chatReadDisk()); }, limits: { questionChars: CHAT_MAX, pollMaxMs: CHAT_POLL.max } },
|
|
2686
2833
|
// The Ask sheet's "Find a task about this": open the board on this query and ask by meaning.
|
|
2687
2834
|
search: function (q) { S.q = String(q || ''); S.tab = 'board'; render(); searchMeaning(); },
|
|
@@ -19,5 +19,38 @@
|
|
|
19
19
|
apply: function () { return apply(read()); }
|
|
20
20
|
};
|
|
21
21
|
apply(read());
|
|
22
|
+
// 7.0.19: the first paint (index.html #first-paint) shows only when this script ran, so with JavaScript off the page
|
|
23
|
+
// never shows a mark that nothing would take away. app.js removes the class when the sign-in card or the app shows.
|
|
24
|
+
document.documentElement.classList.add('fp-wait');
|
|
22
25
|
if (media && media.addEventListener) media.addEventListener('change', function () { if (read() === 'system') apply('system'); });
|
|
23
26
|
})();
|
|
27
|
+
|
|
28
|
+
/* The app's scripts (index.html #boot-scripts), loaded one frame after the page is drawn so the first paint never waits
|
|
29
|
+
for them. Same files, same order (async=false keeps insertion order), and app.js's boot runs only once every one of
|
|
30
|
+
them has run, as when they were in the page: window.CosBoot.after(boot). The frame wait has a 50 ms fallback, because
|
|
31
|
+
a page opened in the background gets no animation frames. A script that fails to load counts as settled, as a failed
|
|
32
|
+
<script> tag did; nothing waits for it forever. */
|
|
33
|
+
(function () {
|
|
34
|
+
var waiting = [], settled = false;
|
|
35
|
+
function settle() { settled = true; var fns = waiting; waiting = []; fns.forEach(function (fn) { try { fn(); } catch (_e) { setTimeout(function () { throw _e; }, 0); } }); }
|
|
36
|
+
window.CosBoot = { after: function (fn) { if (typeof fn !== 'function') return; if (settled) fn(); else waiting.push(fn); } };
|
|
37
|
+
function load() {
|
|
38
|
+
var list = [];
|
|
39
|
+
try { list = JSON.parse((document.getElementById('boot-scripts') || {}).textContent || '[]'); } catch (_e) { list = []; }
|
|
40
|
+
list = Array.isArray(list) ? list.filter(function (u) { return typeof u === 'string' && u.indexOf('/control/') === 0; }) : [];
|
|
41
|
+
var left = list.length;
|
|
42
|
+
if (!left) { settle(); return; }
|
|
43
|
+
list.forEach(function (src) {
|
|
44
|
+
var el = document.createElement('script');
|
|
45
|
+
el.src = src; el.async = false;
|
|
46
|
+
el.onload = el.onerror = function () { if (--left === 0) settle(); };
|
|
47
|
+
document.body.appendChild(el);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function afterFrame() {
|
|
51
|
+
var done = false, go = function () { if (done) return; done = true; setTimeout(load, 0); };
|
|
52
|
+
if (window.requestAnimationFrame) window.requestAnimationFrame(go);
|
|
53
|
+
setTimeout(go, 50);
|
|
54
|
+
}
|
|
55
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', afterFrame); else afterFrame();
|
|
56
|
+
})();
|
|
@@ -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>
|