@gotcos/glasses-server 7.0.7 → 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 +19 -0
- package/CHANGELOG.md +170 -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 +126 -23
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +3 -3
- 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/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/listener-startup.ts +18 -0
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- 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-state-derive.ts +36 -0
- package/server/lib/session-stream-events.ts +25 -1
- 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/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +11 -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/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +16 -9
- 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 +36 -0
- package/server/web-control/app.js +953 -57
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/index.html +14 -1
- 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/settings.js +1 -1
- package/server/web-control/panes/today-actions.css +35 -0
- package/server/web-control/panes/today-actions.js +75 -6
- package/server/web-control/panes/work.css +3 -0
- package/server/web-control/panes/work.js +178 -35
- 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>' +
|
|
@@ -2082,14 +2201,28 @@
|
|
|
2082
2201
|
}
|
|
2083
2202
|
return rows;
|
|
2084
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
|
+
}
|
|
2085
2214
|
function startState(it) {
|
|
2086
2215
|
var st = S.start[it.wid];
|
|
2087
2216
|
if (st) return st;
|
|
2088
2217
|
var saved = (S.activity[it.wid] || {}).savedDestination, rows = sessionChoices(it);
|
|
2089
|
-
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; });
|
|
2090
2223
|
if (saved && saved.mode) {
|
|
2091
2224
|
st.mode = saved.mode; st.sessionId = str(saved.sessionId); st.model = str(saved.model); st.fromSaved = true;
|
|
2092
|
-
} else if (
|
|
2225
|
+
} else if (own.length) { st.mode = 'continueSession'; st.sessionId = own[0].sessionId; }
|
|
2093
2226
|
S.start[it.wid] = st;
|
|
2094
2227
|
return st;
|
|
2095
2228
|
}
|
|
@@ -2115,6 +2248,11 @@
|
|
|
2115
2248
|
return;
|
|
2116
2249
|
}
|
|
2117
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';
|
|
2118
2256
|
var sessions = sessionChoices(it);
|
|
2119
2257
|
var problem = startProblem(it, st), taking = caps.requests === 1;
|
|
2120
2258
|
function choice(mode, title, detail, disabled) {
|
|
@@ -2123,7 +2261,8 @@
|
|
|
2123
2261
|
}
|
|
2124
2262
|
var picker = st.mode === 'newSession'
|
|
2125
2263
|
? '<label class="wk-pick wk-pick-wide"><span>Model</span><select class="field" data-wk-start="model"' + (st.sending || st.request ? ' disabled' : '') + '>' +
|
|
2126
|
-
|
|
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>' : '')
|
|
2127
2266
|
: '<label class="wk-pick wk-pick-wide"><span>Session</span><select class="field" data-wk-start="session"' + (st.sending || st.request ? ' disabled' : '') + '>' +
|
|
2128
2267
|
(sessions.length ? '' : '<option value="">No session has worked on this card yet</option>') +
|
|
2129
2268
|
sessions.filter(function (c) { return st.mode !== 'fork' || FORKABLE[c.provider]; }).map(function (c) {
|
|
@@ -2133,7 +2272,7 @@
|
|
|
2133
2272
|
if (req) {
|
|
2134
2273
|
var terminal = ['sent', 'refused', 'unresolved', 'unconfirmed', 'expired'].indexOf(req.state) >= 0;
|
|
2135
2274
|
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(
|
|
2275
|
+
'<p class="wk-state">' + esc(requestLine(req)) + '</p>' + (req.reason ? '<p class="wk-small">' + esc(req.reason) + '</p>' : '') +
|
|
2137
2276
|
(st.pollStopped ? '<p class="wk-small wk-muted">' + esc(st.pollStopped) + '</p>' : '') +
|
|
2138
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>';
|
|
2139
2278
|
}
|
|
@@ -2321,7 +2460,7 @@
|
|
|
2321
2460
|
if (!S.undoBatch) return;
|
|
2322
2461
|
api('/api/work-intake/undo', { method: 'POST', body: { batchId: S.undoBatch } }).then(function () {
|
|
2323
2462
|
S.undoBatch = null; loadIntake(true); loadBoard(true);
|
|
2324
|
-
}, 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(); });
|
|
2325
2464
|
}
|
|
2326
2465
|
function batchOp(t, action, fields) {
|
|
2327
2466
|
if (S.batchBusy || !(S.caps.workBatch === 1 && S.caps.writable === true)) return;
|
|
@@ -2330,7 +2469,7 @@
|
|
|
2330
2469
|
S.batchBusy = true; S.loopError = ''; render();
|
|
2331
2470
|
api('/api/work-board/batch', { method: 'POST', body: { domain: t.domain, ops: [op] } }).then(function () {
|
|
2332
2471
|
S.batchBusy = false; loadBoard(true); if (S.tab === 'waiting') loadWaiting();
|
|
2333
|
-
}, 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(); });
|
|
2334
2473
|
}
|
|
2335
2474
|
function loadWaiting() {
|
|
2336
2475
|
var mode = S.waitingDropped ? 'dropped' : 'waiting';
|
|
@@ -2338,7 +2477,7 @@
|
|
|
2338
2477
|
var rows = (d && (d.tasks || d.items || d.cards)) || [];
|
|
2339
2478
|
if (mode === 'dropped') S.dropped = rows; else S.waiting = rows;
|
|
2340
2479
|
S.loopError = ''; render();
|
|
2341
|
-
}, 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);
|
|
2342
2481
|
if (mode === 'dropped') S.dropped = []; else S.waiting = []; render(); });
|
|
2343
2482
|
}
|
|
2344
2483
|
// Meaning search: asked only when Return is pressed in the search box (never while typing).
|
|
@@ -2472,6 +2611,10 @@
|
|
|
2472
2611
|
it = current(); if (!it) return;
|
|
2473
2612
|
var draftBox = $('wk-chat-text'); if (draftBox) { chatOf(it.wid).draft = draftBox.value; draftBox.blur(); }
|
|
2474
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;
|
|
2475
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':
|
|
2476
2619
|
it = current(); var round = it && chatFindRound(it.wid, t.getAttribute('data-key')); if (!round) return;
|
|
2477
2620
|
var ids = String(t.getAttribute('data-ids') || '').split(',').filter(Boolean);
|
|
@@ -2563,7 +2706,7 @@
|
|
|
2563
2706
|
var rows = sessionChoices(it).filter(function (c) { return st.mode !== 'fork' || FORKABLE[c.provider]; });
|
|
2564
2707
|
if (st.mode !== 'newSession' && !rows.some(function (c) { return c.sessionId === st.sessionId; })) st.sessionId = rows.length ? rows[0].sessionId : '';
|
|
2565
2708
|
} 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; }
|
|
2709
|
+
else if (el.getAttribute('data-wk-start') === 'model') { st.model = el.value; st.fromSaved = false; st.modelPicked = true; }
|
|
2567
2710
|
return renderStart(it.id);
|
|
2568
2711
|
}
|
|
2569
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); } }
|
|
@@ -2679,9 +2822,9 @@
|
|
|
2679
2822
|
},
|
|
2680
2823
|
leave: function () { stopPoll(); stopHandoffPolls(); chatSave(true); },
|
|
2681
2824
|
// 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,
|
|
2825
|
+
_chat: { S: S, WM: WM, catalog: workModelCatalog, of: chatOf, plan: chatPlan, send: chatSend, stop: chatStop, again: chatAgain, poll: chatPoll, caps: loadChatCaps, readCaps: chatReadCaps,
|
|
2683
2826
|
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,
|
|
2827
|
+
store: function () { return chatStore; }, save: chatSave, timers: CHAT_TIMERS, render: renderDetail, renderStart: renderStart, sortView: sortView, restore: chatRestored, item: itemFor,
|
|
2685
2828
|
mergeIn: function () { chatMergeIn(chatReadDisk()); }, limits: { questionChars: CHAT_MAX, pollMaxMs: CHAT_POLL.max } },
|
|
2686
2829
|
// The Ask sheet's "Find a task about this": open the board on this query and ask by meaning.
|
|
2687
2830
|
search: function (q) { S.q = String(q || ''); S.tab = 'board'; render(); searchMeaning(); },
|
|
@@ -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>
|