@gotcos/glasses-server 6.98.1 → 6.99.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/package.json +2 -1
  3. package/server/index.ts +170 -11
  4. package/server/lib/api-auth.ts +31 -14
  5. package/server/lib/claude-bridge.ts +21 -2
  6. package/server/lib/codex-bridge.ts +21 -2
  7. package/server/lib/cos-thread.ts +582 -0
  8. package/server/lib/display-bus.ts +3 -0
  9. package/server/lib/drop-attachments.ts +55 -0
  10. package/server/lib/drop-destinations.ts +134 -0
  11. package/server/lib/drop-history-search.ts +97 -0
  12. package/server/lib/drop-p2-boot.ts +60 -0
  13. package/server/lib/drop-p2-runtime.ts +237 -0
  14. package/server/lib/drop-p2-settings.ts +174 -0
  15. package/server/lib/drop-resolution.ts +168 -0
  16. package/server/lib/drop-router.ts +325 -0
  17. package/server/lib/drop-service.ts +830 -0
  18. package/server/lib/drop-settings.ts +56 -0
  19. package/server/lib/drop-store.ts +450 -0
  20. package/server/lib/drop-when.ts +265 -0
  21. package/server/lib/glasses-pairing.ts +15 -0
  22. package/server/lib/learning-needs-you.ts +111 -0
  23. package/server/lib/live-cues-v2-settings.ts +2 -1
  24. package/server/lib/live-cues-v2.ts +6 -4
  25. package/server/lib/media-store.ts +80 -0
  26. package/server/lib/meeting-batch-transcribe.ts +15 -2
  27. package/server/lib/meeting-consolidation-run.ts +169 -0
  28. package/server/lib/meeting-store.ts +61 -0
  29. package/server/lib/meeting-title-steer.ts +200 -0
  30. package/server/lib/network-policy.ts +84 -6
  31. package/server/lib/permission-broker.ts +13 -0
  32. package/server/lib/phone-recording.ts +1 -1
  33. package/server/lib/ping-budget.ts +149 -0
  34. package/server/lib/ping-engine.ts +245 -0
  35. package/server/lib/ping-feed-sink.ts +33 -0
  36. package/server/lib/ping-sources.ts +192 -0
  37. package/server/lib/query-job-coordinator.ts +5 -2
  38. package/server/lib/query-job-runtime.ts +45 -4
  39. package/server/lib/query-job-types.ts +10 -1
  40. package/server/lib/reminder-scheduler.ts +153 -0
  41. package/server/lib/tailnet-self-name.ts +103 -0
  42. package/server/lib/tailscale-nudge.ts +40 -2
  43. package/server/lib/task-store.ts +32 -0
  44. package/server/lib/web-ask-admission.ts +56 -0
  45. package/server/lib/web-live-chunks.ts +273 -0
  46. package/server/lib/web-memories-bridge.ts +323 -0
  47. package/server/lib/web-session-auth.ts +492 -0
  48. package/server/lib/web-usage-bridge.ts +154 -0
  49. package/server/lib/work-search.ts +14 -1
  50. package/server/lib/your-move-count.ts +58 -0
  51. package/server/routes/cos-thread.ts +79 -0
  52. package/server/routes/display.ts +11 -1
  53. package/server/routes/drops.ts +262 -0
  54. package/server/routes/health.ts +16 -0
  55. package/server/routes/live-cues.ts +5 -4
  56. package/server/routes/meeting.ts +61 -5
  57. package/server/routes/permission-broker.ts +23 -3
  58. package/server/routes/transcribe-stream.ts +43 -4
  59. package/server/routes/web-control.ts +569 -0
  60. package/server/routes/welcome-context.ts +2 -1
  61. package/server/scripts/web-live-cls/cdp.mjs +57 -0
  62. package/server/scripts/web-live-cls/measure.mjs +80 -0
  63. package/server/scripts/web-live-cls/mock.mjs +97 -0
  64. package/server/scripts/web-live-cls/render.mjs +26 -0
  65. package/server/web-control/app.css +519 -0
  66. package/server/web-control/app.js +3643 -0
  67. package/server/web-control/control.css +325 -0
  68. package/server/web-control/fonts/DMSans.ttf +0 -0
  69. package/server/web-control/fonts/Fraunces.ttf +0 -0
  70. package/server/web-control/fonts/JetBrainsMono.ttf +0 -0
  71. package/server/web-control/index.html +275 -0
  72. package/server/web-control/mark.svg +1 -0
  73. package/server/web-control/memories/d3.min.js +2 -0
  74. package/server/web-control/memories/graph-explorer.css +200 -0
  75. package/server/web-control/memories/graph-explorer.js +748 -0
  76. package/server/web-control/memories/memories-app.js +1561 -0
  77. package/server/web-control/memories/memories-theme.css +73 -0
  78. package/server/web-control/memories/memories.html +72 -0
  79. package/server/web-control/memories/memory-stewardship.js +63 -0
  80. package/server/web-control/memories/memory-workspace.js +216 -0
  81. package/server/web-control/panes/cos-chat.css +174 -0
  82. package/server/web-control/panes/cos-chat.js +1118 -0
  83. package/server/web-control/panes/dictation.js +173 -0
  84. package/server/web-control/panes/meetings.css +314 -0
  85. package/server/web-control/panes/meetings.js +2568 -0
  86. package/server/web-control/panes/messages.js +718 -0
  87. package/server/web-control/panes/questions.css +32 -0
  88. package/server/web-control/panes/questions.js +275 -0
  89. package/server/web-control/panes/sessions.css +452 -0
  90. package/server/web-control/panes/settings.js +292 -0
  91. package/server/web-control/panes/speakers.css +269 -0
  92. package/server/web-control/panes/speakers.js +2755 -0
  93. package/server/web-control/panes/threads.js +200 -0
  94. package/server/web-control/panes/work.css +261 -0
  95. package/server/web-control/panes/work.js +1667 -0
  96. package/server/web-control/shell.js +294 -0
  97. package/server/web-control/theme.js +25 -0
  98. package/server/web-control/vendor/qrcode-encoder.js +10 -0
@@ -0,0 +1,63 @@
1
+ /* Owner review, source refresh and outcome evidence use the same versioned RPC. */
2
+ (function(){
3
+ 'use strict';
4
+ window.createMemoryStewardship=function(api){
5
+ var state={tab:'rules',rows:[],cursor:null,busy:false,error:null,intent:null,serial:0,open:false,draft:{}};
6
+ var esc=api.esc;
7
+ function request(body){return api.call('workspace.request',body);}
8
+ function field(id){var e=document.getElementById(id);return e?e.value.trim():'';}
9
+ function button(label,action,id){return '<button '+(state.busy||(state.intent&&action!=='retry')?'disabled':'')+' data-steward-action="'+action+'" data-steward-id="'+esc(id||'')+'">'+label+'</button>';}
10
+ function saveDraft(){['stewardText','stewardTrigger','stewardExclusions','stewardEvidence','stewardEvaluation'].forEach(function(id){var e=document.getElementById(id);if(e)state.draft[id]=e.value;});}
11
+ function render(){
12
+ if(!state.open)return;saveDraft();
13
+ var body='<p>Evidence and owner decisions govern changes. A saved rule stays in shadow until reviewed and independently checked.</p><div class="actions">'+['rules','sources','outcomes'].map(function(t){return button(t==='rules'?'Standing rules':t==='sources'?'Current sources':'Outcome evidence','tab',t);}).join('')+'</div>';
14
+ if(state.error)body+='<p class="bad">'+esc(state.error)+'</p>'+(state.intent?button('Retry same action','retry'):button('Reload','reload'));
15
+ if(state.busy)body+='<p>Working…</p>';
16
+ if(state.tab==='rules'){
17
+ body+=state.rows.map(function(r){var p=r.payload||{};
18
+ return '<section class="logrow"><h3>'+esc(p.text)+'</h3><p>'+esc(r.state)+' · revision '+esc(r.revision)+' · '+esc(p.review)+'</p><p>Applies when: '+esc(p.trigger||'not specified')+'</p><p>Exclusions: '+esc((p.exclusions||[]).join('; ')||'none recorded')+'</p><small>Evidence: '+esc((p.evidence_refs||[]).join(', '))+'</small><div class="actions">'+(r.state==='candidate'&&p.review!=='approved_by_owner'?button('Review this version','review',r.id):'')+(r.state==='candidate'&&p.review==='approved_by_owner'?button('Check evaluation and activate','activate',r.id):'')+(r.state==='active'?button('Roll back this rule','rollback',r.id):'')+'</div></section>';
19
+ }).join('')||'<p>No standing-rule versions recorded.</p>';
20
+ if(state.cursor)body+=button('Load more','more');
21
+ body+='<div class="settings-block"><h3>Propose a rule</h3><textarea class="editor" id="stewardText" placeholder="The proposed behavior"></textarea><input id="stewardTrigger" placeholder="When this rule applies"><input id="stewardExclusions" placeholder="Exclusions, one per line"><input id="stewardEvidence" placeholder="Source or memory IDs, separated by commas">'+button('Save shadow proposal','propose')+'</div><div class="settings-block"><label>Independent evaluation ID <input id="stewardEvaluation" placeholder="Evaluation artifact ID"></label><p class="muted">Checks must cover this exact revision, trigger, exclusions, counterexamples and regressions. An unverified artifact cannot activate a rule.</p></div>';
22
+ }else if(state.tab==='sources'){
23
+ body+=state.rows.map(function(r){return '<section class="logrow"><h3>'+esc(r.title||r.id)+'</h3><p>Revision '+esc(r.revision)+' · '+esc(r.captured_at)+' · '+esc(r.projection)+'</p><small>'+esc(r.content_hash)+'</small>'+button('Refresh granted document','refresh',r.id)+'</section>';}).join('')||'<p>No current granted source snapshots. Source grants are configured by the instance owner.</p>';
24
+ }else{
25
+ var d=state.outcomes||{};body+='<p>'+esc(d.unique_runs||0)+' observed runs. Evidence threshold '+(d.evidence_gate_met?'met':'not yet met')+'.</p><pre>'+esc(JSON.stringify(d,null,2))+'</pre><p>Retrieval, context inclusion, model-reported application and independently checked outcomes are distinct. Insufficient evidence does not imply improvement.</p>';
26
+ }
27
+ api.modal('Memory stewardship',body);
28
+ Object.keys(state.draft).forEach(function(id){var e=document.getElementById(id);if(e)e.value=state.draft[id];});
29
+ document.querySelectorAll('[data-steward-action]').forEach(function(el){el.addEventListener('click',function(){act(el.dataset.stewardAction,el.dataset.stewardId);});});
30
+ }
31
+ function load(more){
32
+ if(state.busy)return;state.busy=true;state.error=null;var serial=++state.serial;
33
+ var action=state.tab==='rules'?'rule_page':state.tab==='sources'?'source_status':'trace_summary';var body={action:action};
34
+ if(action==='rule_page'){body.limit=25;body.cursor=more?state.cursor:null;}
35
+ render();request(body).then(function(d){if(serial!==state.serial||!state.open)return;state.busy=false;
36
+ if(action==='trace_summary')state.outcomes=d;else{state.rows=more?state.rows.concat(d.items||[]):d.items||[];state.cursor=d.next_cursor||null;}
37
+ render();},function(e){if(serial!==state.serial||!state.open)return;state.busy=false;state.error=e.message;render();});
38
+ }
39
+ function mutate(body){
40
+ if(state.busy||!state.open)return;if(state.intent&&body!==state.intent)return;if(!state.intent)state.intent=JSON.parse(JSON.stringify(body));
41
+ state.busy=true;state.error=null;render();request(state.intent).then(function(d){
42
+ state.busy=false;state.intent=null;if(state.open){api.toast(d.status||'Saved');load(false);}
43
+ },function(e){state.busy=false;state.error=/evaluation_not_found/.test(e.message)?'No passing evaluation was found for this ID. The rule is still in shadow. Check the evaluation result and enter a valid ID.':/evaluation_unreadable/.test(e.message)?'This evaluation could not be verified. The rule is still in shadow. Regenerate the evaluation before retrying.':e.message;
44
+ if(/revision_conflict|idempotency_conflict|invalid_|rule_not_|evaluation_|owner_review_required|not_memory_authority/.test(e.message))state.intent=null;
45
+ render();});
46
+ }
47
+ function act(action,id){
48
+ if(state.busy||!state.open)return;
49
+ if(state.intent&&action!=='retry'){state.error='Retry the pending action before starting another.';render();return;}
50
+ if(action==='retry'){mutate(state.intent);return;}
51
+ if(action==='tab'){if(state.intent){state.error='Resolve the pending action before changing sections.';render();return;}state.tab=id;state.rows=[];state.cursor=null;load(false);return;}
52
+ if(action==='reload'){load(false);return;}if(action==='more'){load(true);return;}
53
+ if(action==='propose'){
54
+ mutate({action:'propose_rule',payload:{lesson_id:'owner_proposal',text:field('stewardText'),trigger:field('stewardTrigger'),exclusions:field('stewardExclusions').split('\n').filter(Boolean),evidence_refs:field('stewardEvidence').split(',').map(function(x){return x.trim();}).filter(Boolean)}});return;
55
+ }
56
+ var row=state.rows.find(function(r){return r.id===id;});if(!row)return;
57
+ var body={action:action==='refresh'?'refresh_source':action==='review'?'review_rule':action==='rollback'?'rollback_rule':'activate_rule',id:id,expected_revision:row.revision,idempotency_key:'steward_'+crypto.randomUUID()};
58
+ if(action==='activate'){body.evaluation_id=field('stewardEvaluation');if(!body.evaluation_id){state.error='Enter the independent evaluation ID for this reviewed revision.';render();return;}}
59
+ mutate(body);
60
+ }
61
+ return {open:function(){state.open=true;if(state.intent){render();return;}load(false);},close:function(){saveDraft();state.open=false;++state.serial;if(!state.intent)state.busy=false;}};
62
+ };
63
+ })();
@@ -0,0 +1,216 @@
1
+ /* Owner-only investigations. Canvas edits are drafts; assertions have their own explicit save. */
2
+ (function () {
3
+ 'use strict';
4
+ var MAX_NODES=200,MAX_EDGES=500;
5
+ function id(){return crypto.randomUUID();}
6
+ function copy(value){return JSON.parse(JSON.stringify(value));}
7
+ function endpoint(value){return typeof value==='object'?value.id:value;}
8
+ function mergeCanvas(old, incoming){
9
+ var nodes=new Map((old.nodes||[]).map(function(n){return [n.id,n];}));
10
+ var links=new Map((old.links||[]).map(function(l){return [l.id||endpoint(l.source)+'|'+endpoint(l.target),l];}));
11
+ (incoming.nodes||[]).forEach(function(n){nodes.set(n.id,Object.assign({},n,nodes.has(n.id)?{x:nodes.get(n.id).x,y:nodes.get(n.id).y,fx:nodes.get(n.id).fx,fy:nodes.get(n.id).fy}:{}));});
12
+ (incoming.links||[]).forEach(function(l){links.set(l.id||endpoint(l.source)+'|'+endpoint(l.target),Object.assign({},l,{source:endpoint(l.source),target:endpoint(l.target)}));});
13
+ if(nodes.size>MAX_NODES||links.size>MAX_EDGES)throw new Error('Canvas limit reached (200 points / 500 links). Hide a branch or start another investigation.');
14
+ return {nodes:Array.from(nodes.values()),links:Array.from(links.values())};
15
+ }
16
+ function create(options){
17
+ var el=document.createElement('section');el.className='memory-investigation';
18
+ var state={id:id(),revision:0,title:'Untitled investigation',nodes:[],links:[],anchors:[],waypoints:[],filters:{hops:3,direction:'undirected',avoid:[]},generation:null,paths:[]};
19
+ var queuedFocus=null,editEpoch=0,lastFocus=null,selected=null,offsets={},history=[],controller=null,started=false,busy=false,requestSerial=0,saveIntents={},assertionIntent=null,separationIntent=null;
20
+ el.innerHTML='<div data-role="canvas" class="cgx-host"></div><p data-role="overview-status" class="muted" role="status"></p><details class="investigation-tools" data-role="advanced"><summary>Advanced investigation</summary><div class="graph-search row"><input type="search" id="graphQuery" aria-label="Find an entity" placeholder="Find a person or idea…"><button onclick="cosApp.graphSearch()">Find points</button><span id="graphSearchStatus" class="muted"></span></div><div class="source-card"><div class="row"><input data-role="title" aria-label="Investigation title" placeholder="Investigation title"><button data-action="save">Save investigation</button><button data-action="open">Open saved</button><button data-action="new">New</button></div>'+
21
+ '<p data-role="receipt" class="muted">Draft · saved separately from standing knowledge.</p><div data-role="matches" class="row"></div>'+
22
+ '<div class="row"><b data-role="selected">Select a point</b><button data-action="start">Set start</button><button data-action="target">Set target</button><button data-action="waypoint">Add waypoint</button><button data-action="expand">Expand point</button><button data-action="avoid">Avoid point</button><button data-action="hide">Hide point</button><button data-action="undo">Undo canvas edit</button></div>'+
23
+ '<p data-role="anchors"></p><div data-role="waypoints" class="row"></div><div class="row"><label>Total hops <select data-role="hops" aria-label="Total hop limit"><option>3</option><option>4</option><option>5</option><option>6</option><option>7</option></select></label><label>Traversal <select data-role="direction" aria-label="Relationship direction"><option value="undirected">Undirected discovery</option><option value="directed">Known direction only</option></select></label><label>Known valid on <input data-role="valid" aria-label="Known valid on" type="date"></label><button data-action="paths">Find connections</button><button data-action="refresh">Review generation changes</button><button data-action="separate">Keep anchors separate</button></div>'+
24
+ '<p class="muted">Searches the full index. Up to 3 alternatives, 25 neighbors per expansion. Legacy connections have unknown direction and validity. Paths show associations; they do not establish correlation or causation.</p>'+
25
+ '<div class="row"><input data-role="point" aria-label="New idea name" placeholder="Add your own idea"><button data-action="point">Add idea</button><input data-role="rationale" aria-label="Association rationale" placeholder="Why might start and target relate?"><button data-action="hypothesis">Add draft hypothesis</button><button data-action="assertion">Save as my assertion</button></div>'+
26
+ '<p data-role="status" role="status" aria-live="polite"></p><div data-role="paths"></div><div data-role="saved"></div></div></details>';
27
+ function role(name){return el.querySelector('[data-role="'+name+'"]');}
28
+ function userIntent(){editEpoch++;if(options.onUserIntent)options.onUserIntent();}
29
+ var legacyMode=false;
30
+ function status(text){role('status').textContent=text;}
31
+ function snapshot(){
32
+ if(controller){var snap=controller.snapshot();state.nodes=snap.nodes;state.links=snap.links;state.positions=snap.positions;}
33
+ state.title=role('title').value||'Untitled investigation';
34
+ state.filters.hops=Number(role('hops').value);state.filters.direction=role('direction').value;state.filters.valid_at=role('valid').value||null;
35
+ return copy(state);
36
+ }
37
+ function remember(){editEpoch++;history.push(snapshot());if(history.length>30)history.shift();}
38
+ function selectedChanged(value){selected=value;role('selected').textContent=value;}
39
+ function draw(){
40
+ if(selected && !state.nodes.some(function(n){return n.id===selected;}))selected=null;
41
+ role('selected').textContent=selected||'Select a point';
42
+ role('title').value=state.title;
43
+ role('hops').value=String(state.filters.hops);role('direction').value=state.filters.direction;role('valid').value=state.filters.valid_at||'';
44
+ role('anchors').textContent='Start: '+(state.anchors[0]||'—')+' → Target: '+(state.anchors[1]||'—')+' · Avoid: '+(state.filters.avoid.join(', ')||'none');
45
+ role('waypoints').replaceChildren();
46
+ state.waypoints.forEach(function(point,index){
47
+ var label=document.createElement('span');label.textContent=(index+1)+'. '+point+' ';role('waypoints').appendChild(label);
48
+ [['↑',-1],['↓',1],['Remove',0]].forEach(function(pair){var b=document.createElement('button');b.textContent=pair[0];b.setAttribute('aria-label',pair[0]+' waypoint '+point);b.onclick=function(){remember();if(!pair[1])state.waypoints.splice(index,1);else{var target=index+pair[1];if(target<0||target>=state.waypoints.length)return;var temp=state.waypoints[target];state.waypoints[target]=point;state.waypoints[index]=temp;}draw();};label.appendChild(b);});
49
+ });
50
+ if(controller)controller.replaceData(state);
51
+ else controller=COSGraphExplorer.mount(role('canvas'),Object.assign({},options.graphOptions||{},{scope:'actual',data:state,status:'ready',focus:state.focus||state.anchors[0],onUserIntent:userIntent,onSelect:function(value){userIntent();selectedChanged(value);},
52
+ onRecenter:function(value){userIntent();selectedChanged(value);run(function(){return expand(value);});},
53
+ onPassages:function(value){run(async function(){options.openPassages(value,await options.call('graph.passages',{entity:value,limit:5}));});},
54
+ labels:Object.assign({},options.graphOptions&&options.graphOptions.labels,{sourceStatus:'Draft ideas and hypotheses stay in this investigation until explicitly saved as assertions.',emptyTitle:'No points to show yet',emptyMessage:'Ask a question or open Advanced investigation to find a point. An empty knowledge index needs documents first.'})}));
55
+ if(options.onController)options.onController(controller);
56
+ drawPaths();
57
+ }
58
+ function drawPaths(){
59
+ role('paths').replaceChildren();
60
+ state.paths.forEach(function(path,index){
61
+ var box=document.createElement('details'),title=document.createElement('summary');title.textContent='Path '+(index+1)+': '+path.nodes.join(' → ');box.appendChild(title);
62
+ path.edges.forEach(function(edge){
63
+ var p=document.createElement('p');p.textContent=endpoint(edge.source)+' → '+endpoint(edge.target)+' · '+(edge.direction||'unknown')+' · '+(edge.evidence_status||'unknown')+' · '+(edge.description||edge.desc||'No relationship description');
64
+ var button=document.createElement('button');button.textContent='Inspect hop evidence';button.onclick=function(){
65
+ if(edge.evidence_status==='user_authored'||edge.evidence_status==='hypothesis'){status('User rationale: '+(edge.description||edge.desc||'None supplied')+' · '+(edge.evidence_refs||[]).length+' cited references.');return;}
66
+ run(async function(){options.openPassages(endpoint(edge.source)+' / '+endpoint(edge.target),await options.call('graph.passages',{source:endpoint(edge.source),target:endpoint(edge.target),limit:5}));});
67
+ };p.appendChild(button);box.appendChild(p);
68
+ });role('paths').appendChild(box);
69
+ });
70
+ }
71
+ async function rpc(action,args){
72
+ var result=await options.call('workspace.request',Object.assign({action:action,request_id:id()},args||{}));
73
+ if(result.error){var code=typeof result.error==='object'?result.error.code:result.error;var message=code==='generation_changed'?'Knowledge changed. Choose Review generation changes before continuing.':code==='revision_conflict'?'This record changed. Refresh and review the latest revision before saving.':typeof result.error==='object'?(result.error.message||result.error.code):result.error;var error=new Error(message);error.code=code;throw error;}
74
+ if(result.protocol!==1)throw new Error('This workspace needs the matching server and memory pipeline protocol.');
75
+ return result;
76
+ }
77
+ async function run(work){
78
+ if(busy){status('The current operation is still running.');return;}
79
+ busy=true;el.setAttribute('aria-busy','true');el.querySelectorAll('input,select').forEach(function(n){n.disabled=true;});status('Working…');
80
+ try{await work();}catch(error){status(error.message);options.toast(error.message);}finally{busy=false;if(role('status').textContent==='Working…')status('Ready.');el.removeAttribute('aria-busy');el.querySelectorAll('input,select').forEach(function(n){n.disabled=false;});if(queuedFocus){var focus=queuedFocus;queuedFocus=null;lastFocus=focus;selectedChanged(focus);run(function(){return expand(focus);});}}
81
+ }
82
+ async function expand(value){
83
+ if(legacyMode)return expandLegacy(value);
84
+ var response=await rpc('graph_expand',{id:value,offset:offsets[value]||0,generation:state.generation});
85
+ var canvas=mergeCanvas(snapshot(),response);remember();state.nodes=canvas.nodes;state.links=canvas.links;state.generation=response.generation;offsets[value]=response.next_offset;
86
+ draw();status(response.next_offset!=null?'Added neighbors. Expand again for the next page.':'All neighbors loaded for this point.');
87
+ }
88
+ async function expandLegacy(value){
89
+ var legacy=await options.call('graph.entity',{id:value,limit:25});
90
+ if(!legacy.found)throw new Error('Entity not found in the legacy graph. Choose another point.');
91
+ var nodes=[{id:legacy.id,group:legacy.type||'unknown',description:legacy.description,descs:legacy.descriptions||[],totalDegree:legacy.degree}].concat((legacy.neighbors||[]).map(function(n){return {id:n.id,group:n.type||'unknown',totalDegree:n.degree};}));
92
+ var canvas=mergeCanvas(snapshot(),{nodes:nodes,links:legacy.edges||[]});remember();state.nodes=canvas.nodes;state.links=canvas.links;
93
+ draw();status('Legacy graph browsing. Reload after updating the server and memory pipeline to use advanced actions.');
94
+ }
95
+ function showMatches(items){
96
+ role('advanced').open=true;
97
+ role('matches').replaceChildren();
98
+ items.forEach(function(item){var b=document.createElement('button');b.textContent=item.id+' · '+(item.type||'unknown');b.onclick=function(){selectedChanged(item.id);run(function(){return expand(item.id);});};role('matches').appendChild(b);});
99
+ }
100
+ function point(){
101
+ var name=role('point').value.trim();if(!name)throw new Error('Enter an idea name.');if(state.nodes.some(function(n){return n.id===name;}))throw new Error('That point is already on the canvas.');
102
+ var data=mergeCanvas(snapshot(),{nodes:[{id:name,group:'concept',description:'User-authored draft idea.',evidence_status:'hypothesis',direction:'unknown',validity:'unknown'}]});remember();state.nodes=data.nodes;selectedChanged(name);draw();status('Idea added to this draft investigation.');
103
+ }
104
+ async function association(persist){
105
+ var source=state.anchors[0],target=state.anchors[1],description=role('rationale').value.trim();
106
+ if(!source||!target||source===target||!description)throw new Error('Choose different start and target points and supply a rationale.');
107
+ var intentBody=JSON.stringify({source:source,target:target,description:description,direction:role('direction').value});
108
+ if(persist&&(!assertionIntent||assertionIntent.body!==intentBody))assertionIntent={body:intentBody,id:id()};
109
+ var edge={id:persist?assertionIntent.id:id(),source:source,target:target,description:description,direction:role('direction').value,validity:'unknown',evidence_status:persist?'user_authored':'hypothesis'};
110
+ mergeCanvas(snapshot(),{links:[edge]});
111
+ if(persist){var receipt=await rpc('save_assertion',{id:edge.id,expected_revision:0,idempotency_key:edge.id,payload:{source:source,target:target,description:description,direction:edge.direction}});role('receipt').textContent='Assertion saved · operation '+receipt.operation_id;/* Keep the prior generation pinned until the user reviews all concurrent overlay changes. */assertionIntent=null;}
112
+ remember();state.links=mergeCanvas(snapshot(),{links:[edge]}).links;draw();status(persist?'Saved as your assertion. Review generation changes before extending this investigation.':'Hypothesis added to the draft; standing knowledge is unchanged.');
113
+ }
114
+ async function save(){
115
+ var current=snapshot(),payload={};['title','anchors','waypoints','nodes','links','positions','filters','generation','paths'].forEach(function(k){if(current[k]!==undefined)payload[k]=current[k];});
116
+ var pending=saveIntents[state.id];var recovering=!!pending;if(!pending)pending=saveIntents[state.id]={body:JSON.stringify(payload),key:id(),revision:state.revision};
117
+ var receipt=await rpc('save_exploration',{id:state.id,expected_revision:pending.revision,idempotency_key:pending.key,payload:JSON.parse(pending.body)});
118
+ state.revision=receipt.revision;delete saveIntents[state.id];role('receipt').textContent='Saved revision '+receipt.revision+' · operation '+receipt.operation_id;status(recovering?'Previous save recovered. Save again to include any subsequent edits.':'Investigation saved. Reopen it after restart.');
119
+ }
120
+ async function open(cursor){
121
+ var response=await rpc('list_explorations',{cursor:cursor||null});if(!cursor)role('saved').replaceChildren();
122
+ if(!response.items.length){status('No saved investigations yet.');return;}
123
+ response.items.forEach(function(row){var button=document.createElement('button');button.textContent=row.title+' · revision '+row.revision;button.onclick=function(){run(async function(){var loaded=await rpc('get_exploration',{id:row.id});row=loaded.item;remember();history=[];role('saved').replaceChildren();state=Object.assign({filters:{hops:3,direction:'undirected',avoid:[]},paths:[],anchors:[],waypoints:[]},copy(row.payload),{id:row.id,revision:row.revision});state.filters.avoid=state.filters.avoid||[];offsets={};selected=null;draw();role('receipt').textContent='Opened revision '+row.revision+' · '+row.operation_id;status('Saved evidence generation retained. Review generation changes before extending.');});};role('saved').appendChild(button);});if(response.next_cursor){var more=document.createElement('button');more.textContent='More investigations';more.onclick=function(){run(function(){return open(response.next_cursor);});};role('saved').appendChild(more);}status('Choose an investigation to reopen.');
124
+ }
125
+ async function paths(){
126
+ if(!state.anchors[0]||!state.anchors[1]||state.anchors[0]===state.anchors[1])throw new Error('Choose different start and target points first.');
127
+ var filters={hops:Number(role('hops').value),direction:role('direction').value,avoid:state.filters.avoid,valid_at:role('valid').value||null};
128
+ var response=await rpc('graph_paths',Object.assign({start:state.anchors[0],target:state.anchors[1],waypoints:state.waypoints,generation:state.generation},filters));
129
+ var canvas=mergeCanvas(snapshot(),response);remember();state.nodes=canvas.nodes;state.links=canvas.links;state.paths=response.paths;state.generation=response.generation;state.filters=filters;draw();status(response.message+(response.truncated?' · Incomplete: '+response.truncation_reason:' · Complete within the selected limits.'));
130
+ }
131
+ async function refresh(){
132
+ var before=snapshot(),previewEpoch=editEpoch;var response=await rpc('graph_resolve',{nodes:before.nodes,links:before.links});
133
+ var message=document.createElement('p');message.textContent='Generation '+JSON.stringify(state.generation)+' → '+JSON.stringify(response.generation)+'. '+response.changes.length+' changes: '+(response.changes.map(function(c){return c.kind+' '+c.id;}).join('; ')||'No changed visible evidence.');
134
+ var apply=document.createElement('button');apply.textContent='Apply reviewed generation';apply.onclick=function(){if(state.id!==before.id||editEpoch!==previewEpoch){role('saved').replaceChildren();status('Investigation changed. Review generation changes again.');return;}var current=snapshot();remember();state.nodes=response.nodes.map(function(n){var old=current.nodes.find(function(x){return x.id===n.id;});return Object.assign({},n,old?{x:old.x,y:old.y,fx:old.fx,fy:old.fy}:{});});state.links=response.links;state.generation=response.generation;state.paths=[];offsets={};draw();role('saved').replaceChildren();status('Current evidence applied. Withdrawn links removed; missing endpoints labeled. Anchors retained.');};role('saved').replaceChildren(message,apply);status('Review evidence changes below before applying.');
135
+ }
136
+ async function separate(){
137
+ var source=state.anchors[0],target=state.anchors[1],before=state.id,epoch=editEpoch;
138
+ if(!source||!target||source===target)throw new Error('Choose different start and target points first.');
139
+ var body=JSON.stringify([source,target]),response=await rpc('identity_status',{source:source,target:target});
140
+ if(before!==state.id||epoch!==editEpoch)return;
141
+ role('saved').replaceChildren();
142
+ if(response.same_identity){status('These names already share one identity. Review a split before keeping them separate.');return;}
143
+ if(response.blocked){status('These identities are already protected from merging: '+response.blocked);return;}
144
+ if(!response.writable){status('Save identity constraints on the memory owner Mac after memory setup.');return;}
145
+ if(!separationIntent||separationIntent.body!==body)separationIntent={body:body,key:id(),revision:response.revision};
146
+ var intent=separationIntent,message=document.createElement('p'),apply=document.createElement('button');
147
+ message.textContent='Keep “'+source+'” and “'+target+'” as distinct identities, including their known aliases. Connections between them remain available.';
148
+ apply.textContent='Save separation rule';apply.onclick=function(){run(async function(){
149
+ if(before!==state.id||epoch!==editEpoch||body!==JSON.stringify(state.anchors.slice(0,2))){role('saved').replaceChildren();status('The anchors changed. Review the separation rule again.');return;}
150
+ try{
151
+ var receipt=await rpc('identity_keep_apart',{source:source,target:target,expected_revision:intent.revision,idempotency_key:intent.key});
152
+ separationIntent=null;role('saved').replaceChildren();role('receipt').textContent='Separation rule saved · operation '+receipt.operation_id;
153
+ status('These identities and their aliases are now protected from merging. Review generation changes before finding connections.');
154
+ }catch(error){
155
+ if(['revision_conflict','identity_split_required','idempotency_conflict'].includes(error.code)){separationIntent=null;role('saved').replaceChildren();}
156
+ throw error;
157
+ }
158
+ });};role('saved').replaceChildren(message,apply);status('Review the two identities before saving.');
159
+ }
160
+ el.addEventListener('click',function(event){if(event.target.closest('button'))userIntent();if(busy&&event.target.closest('button')){event.preventDefault();event.stopImmediatePropagation();status('The current operation is still running.');}},true);
161
+ el.addEventListener('click',function(event){var button=event.target.closest('[data-action]');if(!button||!el.contains(button))return;var action=button.dataset.action;
162
+ run(async function(){
163
+ if(['start','target','waypoint','expand','avoid','hide'].includes(action)&&!selected)throw new Error('Select a point on the canvas first.');
164
+ if(action==='save')return save();if(action==='open')return open();if(action==='paths')return paths();if(action==='refresh')return refresh();if(action==='separate')return separate();if(action==='point')return point();if(action==='hypothesis'||action==='assertion')return association(action==='assertion');
165
+ if(action==='expand'){if(offsets[selected]===null){status('All neighbors for this point are already loaded.');return;}return expand(selected);}
166
+ if(action==='undo'){if(!history.length){status('No canvas edits to undo.');return;}editEpoch++;role('saved').replaceChildren();var previous=history.pop();['nodes','links','positions','anchors','waypoints','filters','generation','paths'].forEach(function(key){state[key]=previous[key];});draw();status('Canvas edit undone. Saved assertions are unchanged.');return;}
167
+ remember();
168
+ if(action==='start')state.anchors[0]=selected;
169
+ if(action==='target')state.anchors[1]=selected;
170
+ if(action==='waypoint'){if(state.waypoints.includes(selected)||state.anchors.includes(selected))throw new Error('That point is already an anchor or waypoint.');if(state.waypoints.length>=6)throw new Error('At most six waypoints fit the seven-hop budget.');state.waypoints.push(selected);}
171
+ if(action==='avoid'&&!state.filters.avoid.includes(selected))state.filters.avoid.push(selected);
172
+ if(action==='hide'){state.nodes=state.nodes.filter(function(n){return n.id!==selected;});state.links=state.links.filter(function(l){return endpoint(l.source)!==selected&&endpoint(l.target)!==selected;});state.waypoints=state.waypoints.filter(function(x){return x!==selected;});state.anchors=state.anchors.map(function(x){return x===selected?null:x;});}
173
+ if(action==='new'){role('matches').replaceChildren();role('point').value='';role('rationale').value='';history=[];role('saved').replaceChildren();state={id:id(),revision:0,title:'Untitled investigation',nodes:[],links:[],anchors:[],waypoints:[],filters:{hops:3,direction:'undirected',avoid:[]},generation:null,paths:[]};offsets={};selected=null;role('receipt').textContent='New draft';}
174
+ draw();status('Draft updated.');
175
+ });
176
+ });
177
+ el.addEventListener('input',userIntent);
178
+ el.addEventListener('change',function(event){if(event.target.matches('input,select')){userIntent();editEpoch++;}});
179
+ role('title').addEventListener('input',function(){state.title=role('title').value;});
180
+ function attach(host,focus){
181
+ host.appendChild(el);
182
+ if(!started){
183
+ started=true;lastFocus=focus;draw();
184
+ run(async function(){
185
+ var capabilities;
186
+ try{capabilities=await rpc('status');}
187
+ catch(error){legacyMode=true;role('overview-status').textContent='Advanced workspace unavailable. Update the server to enable automatic overview.';if(focus)return expandLegacy(focus);return;}
188
+ if((capabilities.capabilities||[]).includes('graph_overview')){
189
+ var epoch=editEpoch;
190
+ role('overview-status').textContent='Loading your knowledge…';
191
+ try{
192
+ var overview=await rpc('graph_overview',{focus:focus||null});
193
+ if(epoch!==editEpoch)return;
194
+ state.nodes=overview.nodes;state.links=overview.links;state.generation=overview.generation;state.focus=overview.focus;
195
+ draw();role('overview-status').textContent=state.nodes.length?'Overview · '+state.nodes.length+' people and ideas. Click a point to inspect it.':'No indexed points yet. Open Knowledge setup to add documents.';
196
+ }catch(error){role('overview-status').textContent='Could not load the overview: '+error.message;throw error;}
197
+ }else if(focus)await expand(focus);
198
+ else role('overview-status').textContent='Update the server to preload your graph. Advanced investigation remains available.';
199
+ });
200
+ }else if(focus&&focus!==lastFocus){
201
+ if(busy){queuedFocus=focus;return;}
202
+ lastFocus=focus;selectedChanged(focus);run(function(){return expand(focus);});
203
+ }
204
+ }
205
+ return {attach:attach,showMatches:showMatches,snapshot:snapshot,
206
+ questionToken:function(){return {id:state.id,epoch:editEpoch};},
207
+ applyQuestion:function(result,token){
208
+ if(!token||token.id!==state.id||token.epoch!==editEpoch||busy)return false;
209
+ if(!Array.isArray(result.nodes)||!Array.isArray(result.links))return false;
210
+ var canvas=mergeCanvas({nodes:[],links:[]},result);remember();
211
+ state.nodes=canvas.nodes;state.links=canvas.links;state.anchors=result.anchors||[];state.waypoints=result.waypoints||[];state.filters=result.filters||{hops:3,direction:'undirected',avoid:[]};state.generation=result.generation;state.paths=result.paths||[];state.focus=state.anchors[0];state.positions={};offsets={};selected=null;
212
+ role('overview-status').textContent=result.message||'Question investigation';draw();return true;
213
+ }};
214
+ }
215
+ window.COSMemoryWorkspace={create:create,mergeCanvas:mergeCanvas,limits:{nodes:MAX_NODES,edges:MAX_EDGES}};
216
+ })();
@@ -0,0 +1,174 @@
1
+ /* COS Control Web: the pinned COS chat (panes/cos-chat.js), server 6.99.10. Used while Drop it is on, and from 6.99.12
2
+ while durable query jobs are on (Ask COS), plus the composer's attachments and the reader's Reply. */
3
+
4
+ /* ── the split: list and chat ─────────────────────────── */
5
+ .msg-split[data-show='list'] > .msg-right { display: none; }
6
+ .msg-split[data-show='chat'] > .msg-left { display: none; }
7
+ @media (min-width: 860px) {
8
+ /* Two columns from 860 px (the unfolded Duo reads like the Mac): the list left, the chat right. */
9
+ .msg-split { display: grid; grid-template-columns: minmax(300px, 40%) minmax(0, 1fr); column-gap: 24px; align-items: start; }
10
+ .msg-split[data-show] > .msg-left, .msg-split[data-show] > .msg-right { display: block; }
11
+ /* The list column is narrower than the pane: its header's buttons go under the title instead of over it. */
12
+ .msg-left .cx-hero .cx-actions { grid-column: 1 / -1; justify-content: flex-start; }
13
+ .msg-right { position: sticky; top: calc(56px + var(--safe-top, 0px)); height: calc(100vh - 72px - var(--safe-top, 0px)); min-height: 420px; }
14
+ .msg-right .cc { height: 100%; display: flex; flex-direction: column; background: var(--panel); border: 1px solid var(--panel-border); border-radius: 16px; overflow: hidden; }
15
+ .msg-right .cc-head { padding: 14px 16px 12px; }
16
+ .msg-right .cc-feed { flex: 1; overflow-y: auto; padding: 12px 16px 8px; overscroll-behavior: contain; }
17
+ .msg-right .cc-composer { position: static; padding: 10px 16px 12px; background: var(--panel); }
18
+ }
19
+
20
+ /* Below 860 px the chat fills the screen, so the composer sits at the bottom even when the chat is short. */
21
+ @media (max-width: 859px) {
22
+ .msg-split[data-show='chat'] .cc { min-height: calc(100vh - 64px - var(--safe-top, 0px)); display: flex; flex-direction: column; }
23
+ .msg-split[data-show='chat'] .cc-feed { flex: 1; }
24
+ }
25
+
26
+ /* ── the pinned row ───────────────────────────────────── */
27
+ .cc-pin { background: color-mix(in srgb, var(--accent) 5%, transparent); }
28
+ .cc-pin[aria-current='true'] { background: var(--qopen); }
29
+ .cc-pin-glyph { background: #2b211a; color: var(--gold); }
30
+ .cc-pinned { font: 500 11px/1.3 var(--body); color: var(--tertiary); }
31
+ .cc-pin-status { font: 600 11.5px/1.3 var(--body); color: var(--sub); }
32
+ .cc-pin-status.hot { color: var(--hot); }
33
+ .cc-pin-status:empty { display: none; }
34
+
35
+ /* ── the chat ─────────────────────────────────────────── */
36
+ .cc { --cc-you: color-mix(in srgb, var(--gold) 22%, var(--panel)); }
37
+ .cc-head { display: flex; align-items: center; gap: 12px; padding: 14px 0 12px; border-bottom: 1px solid var(--hairline); flex-wrap: wrap; }
38
+ .cc-head-text { min-width: 0; flex: 1; }
39
+ .cc-title { font: 500 24px/1.1 var(--display); color: var(--ink); margin: 0; }
40
+ .cc-sub { margin: 3px 0 0; font-size: 13px; line-height: 1.35; color: var(--sub); }
41
+ .cc-head-acts { display: flex; gap: 14px; }
42
+ .cc-head-acts .link-btn { min-height: 36px; font-size: 14px; font-weight: 500; }
43
+ .cc-avatar { flex: none; width: 30px; height: 30px; border-radius: 8px; background: #2b211a; color: var(--gold); display: grid; place-items: center; }
44
+ .cc-avatar .icon { width: 17px; height: 17px; }
45
+ .cc-avatar.big { width: 42px; height: 42px; border-radius: 11px; }
46
+ .cc-avatar.big .icon { width: 24px; height: 24px; }
47
+
48
+ .cc-feed { padding: 14px 0 8px; min-height: 160px; }
49
+ .cc-empty { color: var(--sub); font-size: 14px; text-align: center; padding: 28px 8px; margin: 0; }
50
+ .cc-line { margin: 0 0 14px; }
51
+ .cc-line.you { display: flex; flex-direction: column; align-items: flex-end; }
52
+ .cc-bubble { max-width: min(80%, 560px); background: var(--cc-you); color: var(--ink); border: 1px solid color-mix(in srgb, var(--gold) 35%, transparent);
53
+ border-radius: 18px; padding: 9px 14px; font-size: 15px; line-height: 1.4; overflow-wrap: anywhere; white-space: pre-wrap; }
54
+ .cc-line.answer .cc-bubble { font-weight: 600; }
55
+ .cc-src { font-size: 11.5px; color: var(--tertiary); margin: 4px 6px 0; }
56
+ .cc-line.cos { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 10px; align-items: end; }
57
+ .cc-card { max-width: 560px; background: var(--panel); border: 1px solid var(--panel-border); border-radius: 16px; padding: 11px 14px 12px; min-width: 0; }
58
+ .cc-line.done .cc-card { opacity: 0.86; }
59
+ .cc-line.waits .cc-card { border-color: color-mix(in srgb, var(--hot) 45%, var(--panel-border)); }
60
+ .cc-card-title { margin: 0; font: 600 15px/1.35 var(--body); color: var(--ink); display: flex; gap: 10px; align-items: baseline; justify-content: space-between; flex-wrap: wrap; overflow-wrap: anywhere; }
61
+ .cc-card-line { margin: 3px 0 0; font-size: 14px; line-height: 1.45; color: var(--sub); overflow-wrap: anywhere; white-space: pre-wrap; }
62
+ .cc-quote { display: block; width: 100%; text-align: left; margin: 0 0 8px; padding: 6px 10px; border-left: 3px solid var(--gold); background: var(--qopen); border-radius: 6px;
63
+ font-size: 13px; color: var(--sub); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
64
+ .cc-answer { margin-top: 6px; font-size: 15px; line-height: 1.5; color: var(--ink); overflow-wrap: anywhere; }
65
+ .cc-answer p { margin: 0 0 0.6em; }
66
+ .cc-answer p:last-child { margin-bottom: 0; }
67
+ .cc-wait { color: var(--sub); font-size: 14px; }
68
+ .cc-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; margin-top: 10px; }
69
+ .cc-acts:empty { display: none; }
70
+ .cc-text-btn { min-height: 36px; font-size: 14px; font-weight: 500; color: var(--ink); }
71
+ .cc-choices { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 2px; }
72
+ .cc-choice { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 40px; padding: 4px 0; text-align: left; font: 600 14.5px/1.3 var(--body); color: var(--ink); }
73
+ .cc-choice-n { flex: none; width: 24px; height: 24px; border-radius: 6px; border: 1px solid var(--panel-border); display: grid; place-items: center; font: 500 12px/1 var(--mono); color: var(--sub); }
74
+ .cc-move { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--hairline); }
75
+ .cc-move .cc-choice { font-weight: 500; }
76
+ .cc-fine { margin: 8px 0 0; font-size: 12.5px; color: var(--tertiary); }
77
+ .cc-fine.warn { color: var(--hot); }
78
+ .cc-steer { display: flex; gap: 8px; margin-top: 10px; }
79
+ .cc-steer .field { flex: 1; min-width: 0; height: 36px; }
80
+
81
+ /* ── the composer: two visible buttons, no key needed ─── */
82
+ .cc-composer { position: sticky; bottom: 0; z-index: 5; background: var(--page); border-top: 1px solid var(--hairline); padding: 10px 0 calc(10px + var(--safe-bottom, env(safe-area-inset-bottom, 0px)));
83
+ display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 10px; align-items: end; }
84
+ .cc-input { min-height: 44px; max-height: 140px; padding: 11px 12px; font-size: 16px; border-radius: 12px; }
85
+ .cc-buttons { display: flex; gap: 8px; }
86
+ .cc-buttons .cx-btn { height: 44px; padding: 0 16px; font-size: 15px; border-radius: 12px; }
87
+ .cc-drop { background: transparent; border: 1.5px solid var(--accent); color: var(--accent); }
88
+ .cc-foot { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; font-size: 12px; color: var(--tertiary); }
89
+ .cc-model { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--sub); }
90
+ .cc-model .field { width: auto; height: 34px; min-height: 34px; padding: 0 30px 0 10px; font-size: 13px; background-position: calc(100% - 17px) 14px, calc(100% - 12px) 14px; }
91
+ .cc-hint { flex: 1; min-width: 180px; }
92
+ .cc-note { grid-column: 1 / -1; margin: 0; font-size: 13px; color: var(--hot); }
93
+ /* A phone at 390 px: the box on its own row, the buttons under it, the menus under those. */
94
+ @media (max-width: 480px) {
95
+ .cc-composer { grid-template-columns: minmax(0, 1fr); }
96
+ .cc-buttons { justify-content: flex-end; }
97
+ .cc-buttons .cx-btn { flex: 1; justify-content: center; }
98
+ .cc-buttons .cc-attach { flex: none; }
99
+ }
100
+
101
+ /* ── 6.99.12: the menus, the clip and the chips ───────── */
102
+ .cc-menus { display: inline-flex; flex-wrap: wrap; gap: 6px 12px; }
103
+ .cc-menus:empty { display: none; }
104
+ .cc-attach { position: relative; width: 44px; padding: 0; justify-content: center; cursor: pointer; }
105
+ .cc-buttons .cc-attach { height: 44px; border-radius: 12px; }
106
+ .cc-attach .icon { width: 19px; height: 19px; }
107
+ .cc-attach:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
108
+ .cc-attach.full { opacity: 0.45; cursor: default; }
109
+ /* The real file input fills the clip, invisible, so a tap on iOS reaches it (Camera, Photo Library, Files). */
110
+ .cc-file { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; font-size: 0; }
111
+ .cc-file:disabled { cursor: default; }
112
+ .cc-chips { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 8px; }
113
+ .cc-chips.hidden { display: none; }
114
+ .cc-chip { position: relative; display: flex; align-items: center; gap: 8px; max-width: 100%; min-width: 0; flex: 0 1 236px; padding: 6px 4px 6px 6px;
115
+ border: 1px solid var(--panel-border); border-radius: 12px; background: var(--panel); overflow: hidden; }
116
+ .cc-chip.failed { border-color: color-mix(in srgb, var(--hot) 55%, var(--panel-border)); }
117
+ .cc-chip-thumb { flex: none; width: 40px; height: 40px; border-radius: 8px; overflow: hidden; display: grid; place-items: center; background: var(--qopen); color: var(--sub); }
118
+ .cc-chip-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
119
+ .cc-chip-thumb .icon { width: 20px; height: 20px; }
120
+ .cc-chip-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
121
+ .cc-chip-name { font: 500 13px/1.25 var(--body); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
122
+ .cc-chip-meta { font-size: 12px; line-height: 1.2; color: var(--tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
123
+ .cc-chip.failed .cc-chip-meta { color: var(--hot); }
124
+ .cc-chip-retry { flex: none; min-height: 36px; padding: 0 4px; font-size: 13px; font-weight: 600; }
125
+ .cc-chip-x { flex: none; width: 36px; height: 36px; border-radius: 9px; display: grid; place-items: center; color: var(--sub); font-size: 20px; line-height: 1; }
126
+ .cc-chip-x:hover { background: var(--qopen); color: var(--ink); }
127
+ .cc-chip-bar { position: absolute; left: 0; bottom: 0; height: 3px; background: var(--accent); transition: width 160ms ease-out; }
128
+ .cc-composer.dragging { outline: 2px dashed var(--accent); outline-offset: 4px; border-radius: 12px; }
129
+
130
+ /* What went with an Ask: the photo itself, or the file's kind and name. */
131
+ .cc-sent-atts { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; margin-top: 6px; max-width: min(80%, 560px); }
132
+ .cc-sent-att { display: inline-flex; align-items: center; gap: 6px; height: 56px; min-width: 56px; padding: 0; border-radius: 10px; overflow: hidden;
133
+ border: 1px solid var(--panel-border); background: var(--panel); color: var(--sub); font-size: 12px; }
134
+ .cc-sent-att img { width: 56px; height: 56px; object-fit: cover; display: block; }
135
+ .cc-sent-att .icon { width: 18px; height: 18px; margin-left: 10px; flex: none; }
136
+ .cc-sent-name { padding-right: 10px; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }
137
+
138
+ /* ── 6.99.12: Reply in the message reader ─────────────── */
139
+ .cx-reply { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--hairline); }
140
+ .cx-reply-title { margin: 0; font: 600 16px/1.3 var(--body); color: var(--ink); }
141
+ .cx-reply-sub { margin: 2px 0 0; font-size: 13px; line-height: 1.4; color: var(--sub); }
142
+ .cx-reply-feed { min-height: 0; padding: 10px 0 0; }
143
+ .cx-reply-feed:empty { display: none; }
144
+ .cx-reply .cc-composer { position: static; background: transparent; border-top: 0; padding: 10px 0 0; }
145
+
146
+ /* ── the Messages badge in the rail and the section menu ─ */
147
+ .inbox-count { min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--hot); color: var(--panel); font: 600 11px/18px var(--body); text-align: center; }
148
+ .inbox-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
149
+ .inbox-count.hidden, .inbox-dot.hidden { display: none; }
150
+ #section-menu .inbox-count, #section-menu .inbox-dot { flex: none; margin-left: 8px; }
151
+ @media (min-width: 600px) {
152
+ .rail-btn .inbox-count { position: absolute; top: 3px; right: 8px; }
153
+ .rail-btn .inbox-dot { position: absolute; top: 7px; right: 15px; }
154
+ }
155
+ @media (min-width: 1100px) {
156
+ .rail-btn .inbox-count, .rail-btn .inbox-dot { position: static; margin: 2px 0 0 auto; flex: none; }
157
+ }
158
+
159
+ /* ── 6.99.13: a drop's files, under your words and on its receipt ── */
160
+ .cc-dfiles { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
161
+ .cc-dfiles.mine { align-items: flex-end; max-width: min(80%, 560px); margin-top: 6px; }
162
+ .cc-dfile { display: flex; align-items: center; gap: 10px; min-height: 44px; max-width: 100%; min-width: 0; padding: 4px 12px 4px 4px;
163
+ border: 1px solid var(--panel-border); border-radius: 12px; background: var(--panel); color: var(--ink); text-decoration: none; }
164
+ .cc-dfile:hover { background: var(--qopen); }
165
+ .cc-dfile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
166
+ .cc-dfile-thumb { flex: none; width: 36px; height: 36px; border-radius: 8px; overflow: hidden; display: grid; place-items: center; background: var(--qopen); color: var(--sub); }
167
+ .cc-dfile-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
168
+ .cc-dfile-thumb .icon { width: 18px; height: 18px; }
169
+ .cc-dfile-name { min-width: 0; flex: 1; font: 500 13.5px/1.3 var(--body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
170
+ .cc-dfile-open { flex: none; font: 600 13px/1 var(--body); color: var(--accent); }
171
+
172
+ /* 6.99.16: the COS thread's one quiet line, under the title row. */
173
+ .cc-thread { flex-basis: 100%; margin: 0; font-size: 12.5px; line-height: 1.35; color: var(--tertiary); }
174
+ .cc-thread:empty { display: none; }