kira-wallet 0.1.0
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/LICENSE +21 -0
- package/README.md +116 -0
- package/THIRD_PARTY_NOTICES.md +13 -0
- package/bin/kira.cjs +8 -0
- package/curve_pricing.py +86 -0
- package/docs/AGENT_TOOLS.md +53 -0
- package/docs/CLI.md +144 -0
- package/docs/JOB_CONTRACT.md +111 -0
- package/docs/KIRA_JOBS_BUILD_REPORT.md +92 -0
- package/docs/KIRA_REDESIGN_REPORT.md +125 -0
- package/docs/ONBOARDING_CHAT_CONTRACT.md +36 -0
- package/docs/RELEASE.md +45 -0
- package/docs/WALLET_CAPABILITIES.md +98 -0
- package/docs/character/brand-generation.json +20 -0
- package/docs/character/workspace-generation.json +29 -0
- package/docs/screenshots/kira-jobs-desktop.jpg +0 -0
- package/kira_agent.py +280 -0
- package/kira_cli.py +263 -0
- package/kira_config.py +98 -0
- package/kira_jobs.py +504 -0
- package/kira_tools.py +104 -0
- package/networks.json +214 -0
- package/onchain.cjs +34 -0
- package/package.json +78 -0
- package/pipeline-onchain.cjs +284 -0
- package/research.py +39 -0
- package/sources/job-envelope-v1.schema.json +469 -0
- package/sources/job-request-v1.schema.json +352 -0
- package/sources/mintclub-bond-abi.json +1457 -0
- package/sources/rpc-candidates.json +294 -0
- package/sources/uniswap-deployments.json +18261 -0
- package/viewer/README.md +119 -0
- package/viewer/chain_images.py +21 -0
- package/viewer/details.py +119 -0
- package/viewer/model.py +216 -0
- package/viewer/refresh-curve-prices.cjs +35 -0
- package/viewer/refresh_prices.py +97 -0
- package/viewer/server.py +153 -0
- package/viewer/static/agent.css +100 -0
- package/viewer/static/agent.js +203 -0
- package/viewer/static/app.js +252 -0
- package/viewer/static/favicon.svg +1 -0
- package/viewer/static/index.html +139 -0
- package/viewer/static/jobs.js +113 -0
- package/viewer/static/kira-attention.png +0 -0
- package/viewer/static/kira-explain.png +0 -0
- package/viewer/static/kira-logo.png +0 -0
- package/viewer/static/kira-research.png +0 -0
- package/viewer/static/kira-review.png +0 -0
- package/viewer/static/kira.png +0 -0
- package/viewer/static/style.css +51 -0
- package/viewer/static/watching-model.js +129 -0
- package/viewer/static/watching.js +167 -0
- package/viewer/static/workspace-model.js +43 -0
- package/viewer/static/workspace.css +463 -0
- package/viewer/static/workspace.js +102 -0
- package/viewer/token_images.py +137 -0
- package/wallet.py +389 -0
package/viewer/server.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Loopback viewer with explicitly enabled, session-protected local controls."""
|
|
2
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import threading
|
|
8
|
+
import secrets
|
|
9
|
+
import sys
|
|
10
|
+
import signal
|
|
11
|
+
from urllib.parse import urlparse, parse_qs
|
|
12
|
+
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
|
|
13
|
+
from kira_jobs import JobStore, JobError
|
|
14
|
+
from kira_agent import AgentStore
|
|
15
|
+
from model import ROOT, load_state, within
|
|
16
|
+
from token_images import IMAGE_HOSTS
|
|
17
|
+
|
|
18
|
+
STATIC=Path(__file__).resolve().parent/'static'
|
|
19
|
+
lock=threading.Lock()
|
|
20
|
+
last_good=None
|
|
21
|
+
|
|
22
|
+
class Handler(BaseHTTPRequestHandler):
|
|
23
|
+
def local_request(self):
|
|
24
|
+
expected='127.0.0.1:'+str(self.server.server_port)
|
|
25
|
+
if self.headers.get('Host')!=expected or self.headers.get('Origin', 'http://'+expected)!='http://'+expected or self.headers.get('Sec-Fetch-Site') in ('cross-site','same-site'):
|
|
26
|
+
self.send_error(403,'Local origin required.');return False
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
def authenticated(self):
|
|
30
|
+
if not getattr(self.server,'controls',False):self.send_error(403,'Local controls are disabled.');return False
|
|
31
|
+
if not secrets.compare_digest(self.headers.get('X-Kira-Session','').encode(),self.server.session_token.encode()):self.send_error(403,'Local session required.');return False
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
def json(self,value,status=200):
|
|
35
|
+
self.respond(json.dumps(value).encode(),'application/json; charset=utf-8',status=status)
|
|
36
|
+
|
|
37
|
+
def do_POST(self):
|
|
38
|
+
if not self.local_request():return
|
|
39
|
+
if not getattr(self.server,'controls',False):self.send_error(501,'Local controls are disabled.');return
|
|
40
|
+
if not self.authenticated():return
|
|
41
|
+
expected='http://127.0.0.1:'+str(self.server.server_port)
|
|
42
|
+
if self.headers.get('Origin')!=expected:self.send_error(403,'Same origin required.');return
|
|
43
|
+
if self.headers.get('Content-Type')!='application/json':self.send_error(415);return
|
|
44
|
+
path=urlparse(self.path).path
|
|
45
|
+
if path not in ('/api/operations','/api/agent/test','/api/agent/settings','/api/chat/send','/api/chat/reset','/api/chat/cancel'):self.send_error(404);return
|
|
46
|
+
try:
|
|
47
|
+
self.connection.settimeout(5)
|
|
48
|
+
length=int(self.headers.get('Content-Length','0'))
|
|
49
|
+
if not 0<length<=65536:self.send_error(413);return
|
|
50
|
+
request=json.loads(self.rfile.read(length),parse_constant=lambda _: (_ for _ in ()).throw(ValueError()))
|
|
51
|
+
if not isinstance(request,dict):raise ValueError()
|
|
52
|
+
if path!='/api/operations':
|
|
53
|
+
if path in ('/api/agent/test','/api/chat/send') and json.loads((ROOT/'wallets.json').read_text()).get('demo'):
|
|
54
|
+
raise JobError('demo_read_only','The synthetic demo does not contact model services. Run kira setup with your personal data directory.')
|
|
55
|
+
if path=='/api/agent/test':result=self.server.agent.start(request,test=True)
|
|
56
|
+
elif path=='/api/agent/settings':result=self.server.agent.configure(request)
|
|
57
|
+
elif path=='/api/chat/send':result=self.server.agent.start(request)
|
|
58
|
+
elif path=='/api/chat/reset':
|
|
59
|
+
if request:raise ValueError()
|
|
60
|
+
result=self.server.agent.reset()
|
|
61
|
+
else:
|
|
62
|
+
if set(request)!={'id'}:raise ValueError()
|
|
63
|
+
result=self.server.agent.cancel(request['id'])
|
|
64
|
+
self.json(result,202 if path in ('/api/agent/test','/api/chat/send') else 200);return
|
|
65
|
+
if json.loads((ROOT/'wallets.json').read_text()).get('demo'):
|
|
66
|
+
operation=request.get('operation')
|
|
67
|
+
if operation=='job.resume' and isinstance(request.get('input'),dict):
|
|
68
|
+
operation=self.server.jobs.get(request['input'].get('job_id'))['operation']
|
|
69
|
+
if operation in ('wallet.add','wallet.refresh','prices.refresh'):
|
|
70
|
+
raise JobError('demo_read_only','Sample research cannot contact providers. Use a separate personal data directory.')
|
|
71
|
+
job=self.server.jobs.submit(request)
|
|
72
|
+
if job['state']=='queued':self.server.jobs.launch()
|
|
73
|
+
self.json(job,202)
|
|
74
|
+
except JobError as error:self.json({'error':{'code':error.code,'message':str(error)}},409 if error.code=='idempotency_conflict' else 400)
|
|
75
|
+
except (ValueError,TypeError):self.json({'error':{'code':'invalid_request','message':'Expected a valid operation request.'}},400)
|
|
76
|
+
except OSError:self.json({'error':{'code':'unavailable','message':'Local storage is temporarily unavailable.'}},503)
|
|
77
|
+
|
|
78
|
+
def do_GET(self):
|
|
79
|
+
global last_good
|
|
80
|
+
if not self.local_request():return
|
|
81
|
+
path=urlparse(self.path).path
|
|
82
|
+
try:
|
|
83
|
+
if path=='/api/health':
|
|
84
|
+
self.json({'instance':os.environ.get('KIRA_INSTANCE_ID'),'read_only':not getattr(self.server,'controls',False),'controls':getattr(self.server,'controls',False)});return
|
|
85
|
+
if path=='/api/session':
|
|
86
|
+
self.json({'controls':getattr(self.server,'controls',False),'token':self.server.session_token if getattr(self.server,'controls',False) else None});return
|
|
87
|
+
if path.startswith('/api/jobs') or path.startswith('/api/chat/turn/') or path in ('/api/agent','/api/chat','/api/snapshots','/api/snapshot','/api/compare','/api/settings'):
|
|
88
|
+
if not self.authenticated():return
|
|
89
|
+
query=parse_qs(urlparse(self.path).query)
|
|
90
|
+
if path in ('/api/agent','/api/chat'):
|
|
91
|
+
if json.loads((ROOT/'wallets.json').read_text()).get('demo'):
|
|
92
|
+
self.json({'config':None,'providers':[],'conversation_id':None,'messages':[],'active_turn':None})
|
|
93
|
+
else:self.json(self.server.agent.status(query.get('recheck')==['1']))
|
|
94
|
+
elif path.startswith('/api/chat/turn/'):self.json(self.server.agent.read(path.removeprefix('/api/chat/turn/')))
|
|
95
|
+
elif path=='/api/jobs':self.json(self.server.jobs.list())
|
|
96
|
+
elif path.startswith('/api/jobs/'):self.json(self.server.jobs.get(path.removeprefix('/api/jobs/')))
|
|
97
|
+
elif path=='/api/snapshots':self.json(self.server.jobs.snapshots(query.get('wallet',[''])[0]))
|
|
98
|
+
elif path=='/api/snapshot':self.json(self.server.jobs.snapshot(query.get('id',[''])[0]))
|
|
99
|
+
elif path=='/api/compare':self.json(self.server.jobs.compare(query.get('before',[''])[0],query.get('after',[''])[0]))
|
|
100
|
+
elif path=='/api/settings':
|
|
101
|
+
from kira_config import load_config
|
|
102
|
+
config=load_config();self.json({key:config[key] for key in ('schema_version','rpc','discovery')})
|
|
103
|
+
else:self.send_error(404)
|
|
104
|
+
return
|
|
105
|
+
if path=='/api/state':
|
|
106
|
+
with lock:
|
|
107
|
+
try:
|
|
108
|
+
_,body,etag=load_state();last_good=(body,etag)
|
|
109
|
+
except (json.JSONDecodeError,FileNotFoundError):
|
|
110
|
+
if last_good is None:raise
|
|
111
|
+
body,etag=last_good
|
|
112
|
+
if self.headers.get('If-None-Match')=='"'+etag+'"':
|
|
113
|
+
self.send_response(304);self.send_header('ETag','"'+etag+'"');self.send_header('Cache-Control','no-cache');self.end_headers();return
|
|
114
|
+
self.respond(body,'application/json; charset=utf-8',etag);return
|
|
115
|
+
if path.startswith('/api/report/'):
|
|
116
|
+
key=path.removeprefix('/api/report/')
|
|
117
|
+
registry=json.loads((ROOT/'wallets.json').read_text())
|
|
118
|
+
entry=next((w for w in registry['wallets'] if w['address_key']==key),None)
|
|
119
|
+
if not entry or not entry.get('latest_snapshot',{}).get('report'):self.send_error(404);return
|
|
120
|
+
body=within(ROOT,entry['latest_snapshot']['report']).read_bytes()
|
|
121
|
+
self.respond(body,'text/plain; charset=utf-8');return
|
|
122
|
+
files={'/':'index.html','/index.html':'index.html','/app.js':'app.js','/jobs.js':'jobs.js','/workspace.js':'workspace.js','/workspace-model.js':'workspace-model.js','/workspace.css':'workspace.css','/agent.js':'agent.js','/agent.css':'agent.css','/watching.js':'watching.js','/watching-model.js':'watching-model.js','/style.css':'style.css','/favicon.svg':'favicon.svg','/kira.png':'kira.png','/kira-logo.png':'kira-logo.png',**{f'/kira-{pose}.png':f'kira-{pose}.png' for pose in ('research','explain','review','attention')}}
|
|
123
|
+
if path not in files:self.send_error(404);return
|
|
124
|
+
file=STATIC/files[path]
|
|
125
|
+
content={'html':'text/html; charset=utf-8','js':'text/javascript; charset=utf-8','css':'text/css; charset=utf-8','svg':'image/svg+xml','png':'image/png'}[file.suffix[1:]]
|
|
126
|
+
self.respond(file.read_bytes(),content)
|
|
127
|
+
except JobError as error:self.json({'error':{'code':error.code,'message':str(error)}},400)
|
|
128
|
+
except (ValueError,OSError,json.JSONDecodeError):self.send_error(503,'Research data is being updated. Try again shortly.')
|
|
129
|
+
|
|
130
|
+
def respond(self,body,content,etag=None,status=200):
|
|
131
|
+
self.send_response(status);self.send_header('Content-Type',content);self.send_header('Content-Length',str(len(body)))
|
|
132
|
+
self.send_header('Cache-Control','no-cache');self.send_header('X-Content-Type-Options','nosniff')
|
|
133
|
+
self.send_header('Referrer-Policy','no-referrer');self.send_header('X-Frame-Options','DENY')
|
|
134
|
+
image_origins=' '.join('https://'+host for host in IMAGE_HOSTS)
|
|
135
|
+
self.send_header('Content-Security-Policy',"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: "+image_origins+"; connect-src 'self'; frame-ancestors 'none'")
|
|
136
|
+
if etag:self.send_header('ETag','"'+etag+'"')
|
|
137
|
+
self.end_headers();self.wfile.write(body)
|
|
138
|
+
|
|
139
|
+
def log_message(self,*args): pass
|
|
140
|
+
|
|
141
|
+
if __name__=='__main__':
|
|
142
|
+
parser=argparse.ArgumentParser();parser.add_argument('--port',type=int,default=8765);parser.add_argument('--controls',action='store_true');args=parser.parse_args()
|
|
143
|
+
server=ThreadingHTTPServer(('127.0.0.1',args.port),Handler)
|
|
144
|
+
server.controls=args.controls;server.session_token=secrets.token_urlsafe(32)
|
|
145
|
+
if args.controls:server.jobs=JobStore(ROOT);server.agent=AgentStore(ROOT)
|
|
146
|
+
def stop_viewer(*_):raise KeyboardInterrupt()
|
|
147
|
+
signal.signal(signal.SIGTERM,stop_viewer)
|
|
148
|
+
print(f'Wallet viewer: http://127.0.0.1:{args.port}',flush=True)
|
|
149
|
+
try:server.serve_forever()
|
|
150
|
+
except KeyboardInterrupt:pass
|
|
151
|
+
finally:
|
|
152
|
+
if args.controls:server.agent.shutdown()
|
|
153
|
+
server.server_close()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* A distinct, reachable composer and a deliberate account setup flow. */
|
|
2
|
+
.composer { padding: 16px 18px 14px; background: #f8f6fb; }
|
|
3
|
+
.composer-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 9px; }
|
|
4
|
+
.composer-top label { font-size: 13px; line-height: 20px; font-weight: 600; color: #493254; }
|
|
5
|
+
#chat-status { color: #715c7d; font-size: 11px; line-height: 18px; }
|
|
6
|
+
.composer-box { background: #fff; border: 1px solid #bcb0c8; border-radius: 14px; box-shadow: 0 2px 8px #3a294708; transition: border-color .15s, box-shadow .15s; }
|
|
7
|
+
.composer-box:focus-within { border-color: #79558d; box-shadow: 0 0 0 3px #79558d1a; }
|
|
8
|
+
.composer textarea { resize: none; min-height: 78px; max-height: 160px; padding: 13px 14px 6px; font-size: 14px; line-height: 1.6; outline: none; }
|
|
9
|
+
.composer textarea::placeholder { color: #74677e; }
|
|
10
|
+
.composer-footer { padding: 4px 8px 8px 10px; gap: 8px; }
|
|
11
|
+
.model-button { display: inline-flex; align-items: center; gap: 7px; min-width: 0; min-height: 44px; max-width: calc(100% - 52px); padding: 6px 9px; border: 0; background: transparent; color: #574164; font-size: 12px; line-height: 18px; border-radius: 8px; }
|
|
12
|
+
.model-button:hover { background: #f0edf5; }
|
|
13
|
+
.model-button > span:nth-child(2) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14
|
+
.model-button .ui-icon { width: 16px; height: 16px; }
|
|
15
|
+
.model-dot { width: 6px; height: 6px; flex: none; border-radius: 50%; background: #9e90a7; }
|
|
16
|
+
.model-button.connected .model-dot { background: #52795e; }
|
|
17
|
+
.send-button { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; flex: none; border-radius: 11px; padding: 0; background: #6b477d; color: white; opacity: 1; }
|
|
18
|
+
.send-button:hover:not(:disabled) { background: #50305f; }
|
|
19
|
+
.send-button:disabled { background: #eee8f3; color: #9c8aaa; cursor: default; }
|
|
20
|
+
.send-button .ui-icon { width: 20px; height: 20px; stroke-width: 1.8; }
|
|
21
|
+
.stop-button { background: #574164; }
|
|
22
|
+
.composer-context { display: grid; gap: 4px; margin-top: 9px; color: #766480; font-size: 11px; line-height: 16px; }
|
|
23
|
+
#chat-context { font-weight: 500; overflow-wrap: anywhere; }
|
|
24
|
+
#chat-hint { color: #766480; font-size: 11px; }
|
|
25
|
+
.composer .form-error:empty { display: none; }
|
|
26
|
+
.chat-new { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; background: #ffffff60; border: 1px solid #dcd2e4; color: #654378; border-radius: 10px; flex: none; }
|
|
27
|
+
.chat-new .ui-icon { width: 19px; height: 19px; }
|
|
28
|
+
.chat-messages { display: grid; gap: 18px; }
|
|
29
|
+
.chat-message { font-size: 14px; line-height: 1.7; color: #48374f; overflow-wrap: anywhere; }
|
|
30
|
+
.chat-message p { margin: 6px 0 0; white-space: pre-wrap; }
|
|
31
|
+
.chat-message strong { font-size: 11px; font-weight: 600; color: #745c81; }
|
|
32
|
+
.chat-message.user { margin-left: 24px; padding: 12px 14px; border-radius: 13px 13px 3px 13px; background: #e6deed; }
|
|
33
|
+
.chat-message.assistant { padding: 0 1px; }
|
|
34
|
+
.chat-message.pending { color: #80688c; font-size: 13px; }
|
|
35
|
+
.kira-panel.chatting .kira-scene { height: 72px; }
|
|
36
|
+
.kira-panel.chatting .kira-scene img { width: 76px; height: 86px; bottom: -8px; }
|
|
37
|
+
.kira-panel.chatting .scene-caption { max-width: 70%; padding-top: 16px; gap: 6px; }
|
|
38
|
+
.kira-panel.chatting .scene-caption > span:first-child { font-size: 17px; line-height: 22px; }
|
|
39
|
+
.kira-panel.chatting .scene-rule { display: none; }
|
|
40
|
+
.kira-panel.chatting .scene-caption small { font-size: 11px; max-width: none; }
|
|
41
|
+
.kira-panel.chatting #kira-note, .kira-panel.chatting .kira-quick-actions, .kira-panel.chatting .briefing-meta { display: none; }
|
|
42
|
+
.kira-panel.chatting .kira-conversation { padding-top: 18px; }
|
|
43
|
+
.agent-dialog { width: min(590px, calc(100% - 32px)); max-height: calc(100dvh - 40px); padding: 28px; border-radius: 20px; }
|
|
44
|
+
.agent-dialog .dialog-heading { margin-bottom: 22px; align-items: flex-start; }
|
|
45
|
+
.agent-dialog h2 { font-size: 27px; line-height: 1.25; letter-spacing: -.03em; font-weight: 600; }
|
|
46
|
+
.agent-dialog .eyebrow { font-size: 10px; letter-spacing: .08em; color: #785e85; }
|
|
47
|
+
.agent-dialog .dialog-heading { justify-content: space-between; }
|
|
48
|
+
.agent-dialog h3 { font-size: 19px; line-height: 1.4; letter-spacing: -.02em; font-weight: 600; margin: 6px 0 10px; }
|
|
49
|
+
.setup-progress { list-style: none; padding: 0; display: flex; justify-content: space-between; gap: 12px; margin: 0 0 24px; }
|
|
50
|
+
.setup-progress li { display: flex; align-items: center; gap: 7px; font-size: 12px; color: #715c7d; }
|
|
51
|
+
.setup-progress span { display: grid; place-items: center; width: 24px; height: 24px; border: 1px solid #d7cedd; border-radius: 50%; font-size: 11px; }
|
|
52
|
+
.setup-progress [aria-current] { color: #5b356e; font-weight: 600; }
|
|
53
|
+
.setup-progress [aria-current] span { color: white; background: #6b477d; border-color: #6b477d; }
|
|
54
|
+
.setup-intro { font-size: 14px; color: #6a5d71; line-height: 1.65; margin: 0 0 18px; }
|
|
55
|
+
.provider-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
56
|
+
.provider-card { padding: 18px 15px; min-height: 116px; border: 1px solid #ded5e5; border-radius: 13px; background: #fcfafc; text-align: left; color: #4f3c59; display: grid; gap: 7px; }
|
|
57
|
+
.provider-card[aria-pressed=true] { border-color: #856497; background: #f0eaf5; box-shadow: 0 0 0 1px #856497; }
|
|
58
|
+
.provider-card strong { font-size: 15px; line-height: 22px; font-weight: 600; }
|
|
59
|
+
.provider-card small { color: #796a82; font-size: 11px; line-height: 17px; }
|
|
60
|
+
.provider-card .provider-status { font-size: 12px; line-height: 18px; }
|
|
61
|
+
.provider-status.ready { color: #3e7050; }
|
|
62
|
+
.cli-help { margin-top: 12px; font-size: 12px; line-height: 1.6; color: #796a82; }
|
|
63
|
+
.cli-command { display: flex; align-items: center; gap: 8px; margin-top: 8px; padding: 9px 12px; background: #f4f0f7; border-radius: 8px; }
|
|
64
|
+
.cli-command code { flex: 1; min-width: 0; overflow-wrap: anywhere; font-size: 11px; }
|
|
65
|
+
.cli-command button { min-height: 44px; border: 0; background: none; color: #654378; font-size: 12px; }
|
|
66
|
+
.agent-dialog label { font-size: 13px; line-height: 1.5; }
|
|
67
|
+
.model-field { margin-top: 12px; }
|
|
68
|
+
.model-field span { font-weight: 400; color: #715c7d; }
|
|
69
|
+
.scope-choices { border: 0; margin: 0; padding: 0; display: grid; gap: 9px; }
|
|
70
|
+
.scope-choices label { display: flex; gap: 11px; align-items: flex-start; margin: 0; padding: 13px 14px; border: 1px solid #dfd6e6; border-radius: 11px; background: #fcfafc; }
|
|
71
|
+
.scope-choices label:has(input:checked) { background: #f1ebf6; border-color: #9478a3; }
|
|
72
|
+
.scope-choices input, .permission-choice input { width: 16px; height: 16px; flex: none; margin: 3px 0 0; accent-color: #6b477d; }
|
|
73
|
+
.agent-dialog .scope-choices input[type=radio] { width: 16px; height: 16px; min-height: 16px; flex: 0 0 16px; padding: 0; }
|
|
74
|
+
.agent-dialog .permission-choice input[type=checkbox] { width: 16px; height: 16px; min-height: 16px; flex: 0 0 16px; padding: 0; }
|
|
75
|
+
.scope-choices strong, .permission-choice strong { display: block; font-size: 13px; font-weight: 500; line-height: 20px; }
|
|
76
|
+
.scope-choices small, .permission-choice small { display: block; font-size: 12px; color: #796a82; line-height: 1.6; margin-top: 3px; }
|
|
77
|
+
.agent-dialog .permission-choice { display: flex; align-items: flex-start; gap: 11px; margin: 17px 0 0; }
|
|
78
|
+
.native-trust { padding-top: 16px; border-top: 1px solid #e8e0ed; }
|
|
79
|
+
.setup-footer { border-top: 1px solid #e8e0ed; margin-top: 22px; padding-top: 18px; display: flex; gap: 10px; justify-content: flex-end; }
|
|
80
|
+
.setup-footer button { min-height: 44px; }
|
|
81
|
+
.ready-art { margin: 5px 0 10px; }
|
|
82
|
+
.ready-art img { display: block; border-radius: 16px; }
|
|
83
|
+
.agent-review { display: grid; grid-template-columns: 90px 1fr; gap: 10px 16px; padding: 16px; background: #f6f2f8; border-radius: 12px; font-size: 13px; line-height: 20px; margin: 16px 0; }
|
|
84
|
+
.agent-review dt { color: #766480; }
|
|
85
|
+
.agent-review dd { margin: 0; color: #543d60; overflow-wrap: anywhere; }
|
|
86
|
+
#agent-test-status { font-size: 13px; line-height: 1.6; color: #4e7359; }
|
|
87
|
+
@media (max-width: 600px) {
|
|
88
|
+
.agent-dialog { padding: 22px 18px; }
|
|
89
|
+
.agent-dialog h2 { font-size: 25px; }
|
|
90
|
+
.provider-grid { gap: 10px; }
|
|
91
|
+
.provider-card { padding: 14px 12px; }
|
|
92
|
+
.composer textarea { font-size: 16px; }
|
|
93
|
+
#chat-hint { display: none; }
|
|
94
|
+
.setup-progress { gap: 8px; }
|
|
95
|
+
}
|
|
96
|
+
@media (prefers-reduced-motion: reduce) { .composer-box { transition: none; } }
|
|
97
|
+
@media (min-width: 981px) and (max-height: 760px) {
|
|
98
|
+
.kira-panel { position: static; height: auto; min-height: 620px; max-height: none; }
|
|
99
|
+
.kira-conversation { flex: none; overflow: visible; }
|
|
100
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
let agentState = null, agentProvider = 'codex', agentStep = 1, setupRevision = 0, agentVerified = false;
|
|
3
|
+
let chatTurn = null, chatEpoch = 0, chatRequest = null, chatRetry = null, agentLoading = false, setupTesting = false, setupTurn = null, draftRevision = 0;
|
|
4
|
+
let chatSignature = '', agentReady = false, chatDraftRevision = 0;
|
|
5
|
+
const scopeNames = {none: 'No wallet context', wallet: 'One approved wallet', portfolio: 'Whole portfolio'};
|
|
6
|
+
const agentPost = (path, body) => localAPI(path, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)});
|
|
7
|
+
function currentSetup() {
|
|
8
|
+
return {provider: agentProvider, model: $('agent-model').value.trim(), scope: document.querySelector('[name="agent-scope"]:checked').value,
|
|
9
|
+
wallet: $('agent-wallet').value || null, retain_history: $('agent-retain').checked, trust_native_cli: $('agent-trust').checked};
|
|
10
|
+
}
|
|
11
|
+
function invalidateSetup() { setupRevision++; agentVerified = false; $('agent-save').disabled = true; $('agent-test-status').textContent = ''; }
|
|
12
|
+
function scopeLabel(config) {
|
|
13
|
+
if (!config) return 'No wallet context shared';
|
|
14
|
+
if (config.scope === 'wallet') return 'Context: ' + (state?.wallets.find(w => w.key === config.wallet)?.name || 'approved wallet');
|
|
15
|
+
return config.scope === 'portfolio' ? 'Context: whole portfolio' : 'No wallet context shared';
|
|
16
|
+
}
|
|
17
|
+
function renderAgent() {
|
|
18
|
+
const config = agentState?.config;
|
|
19
|
+
$('chat-model-label').textContent = config ? (config.provider === 'codex' ? 'Codex' : 'Claude') + ' · ' + (config.model || 'CLI default') : 'Connect a model';
|
|
20
|
+
$('chat-setup').classList.toggle('connected', Boolean(config));
|
|
21
|
+
$('chat-context').textContent = scopeLabel(config);
|
|
22
|
+
$('chat-status').textContent = chatTurn ? 'Kira is thinking…' : config ? (config.retain_history ? 'History saved locally' : 'History in memory') : 'Connect your account';
|
|
23
|
+
$('chat-send').hidden = Boolean(chatTurn); $('chat-stop').hidden = !chatTurn;
|
|
24
|
+
$('chat-send').disabled = !config || !localSession?.controls || !$('kira-draft').value.trim() || Boolean(chatRequest);
|
|
25
|
+
const messages = agentState?.messages || [];
|
|
26
|
+
$('kira-panel').classList.toggle('chatting', Boolean(messages.length || chatTurn));
|
|
27
|
+
const signature = JSON.stringify([messages, chatTurn, chatRequest?.message]);
|
|
28
|
+
if (signature !== chatSignature) {
|
|
29
|
+
chatSignature = signature;
|
|
30
|
+
$('chat-messages').innerHTML = messages.map(message => `<article class="chat-message ${message.role === 'user' ? 'user' : 'assistant'}"><strong>${message.role === 'user' ? 'YOU' : 'KIRA'}</strong><p>${escapeHTML(message.text)}</p></article>`).join('') + (chatTurn && chatRequest?.message ? `<article class="chat-message user"><strong>YOU</strong><p>${escapeHTML(chatRequest.message)}</p></article>` : '') + (chatTurn ? '<div class="chat-message pending"><span class="spinner" aria-hidden="true"></span> Reading the context you approved…</div>' : '');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function renderProviders() {
|
|
34
|
+
$('agent-providers').innerHTML = (agentState?.providers || []).map(row => {
|
|
35
|
+
const ready = row.installed && row.supported && row.logged_in;
|
|
36
|
+
const label = !row.installed ? 'Install CLI' : !row.supported ? 'Unsupported version' : !row.logged_in ? 'Login needed' : 'Account detected';
|
|
37
|
+
return `<button class="provider-card" type="button" data-provider="${row.id}" aria-pressed="${agentProvider === row.id}"><strong>${escapeHTML(row.name)}</strong><small>${row.id === 'codex' ? 'Your ChatGPT account' : 'Your Claude account'}${row.version ? ' · ' + escapeHTML(row.version) : ''}</small><span class="provider-status ${ready ? 'ready' : ''}">${label}</span></button>`;
|
|
38
|
+
}).join('');
|
|
39
|
+
const selected = agentState?.providers.find(p => p.id === agentProvider);
|
|
40
|
+
$('agent-cli-help').innerHTML = !selected ? '' : selected.installed && selected.supported && selected.logged_in ? 'Your native account is ready for a response check. Kira never copies its credentials.' :
|
|
41
|
+
`<p>${!selected.installed ? 'Install the official CLI in your terminal, then sign in.' : !selected.supported ? 'Use a verified version: Codex 0.158.x or Claude Code 2.1.259–2.1.x.' : 'Sign in through the official CLI in your terminal.'} <a href="${selected.url}" target="_blank" rel="noreferrer">Official setup ↗</a></p>${(!selected.installed ? [selected.install, selected.login] : [selected.login]).map(command => `<div class="cli-command"><code>${escapeHTML(command)}</code><button type="button" data-copy-cli="${escapeHTML(command)}">Copy</button></div>`).join('')}`;
|
|
42
|
+
updateStep();
|
|
43
|
+
}
|
|
44
|
+
function updateStep() {
|
|
45
|
+
for (let step = 1; step <= 3; step++) {
|
|
46
|
+
$('agent-step-' + step).hidden = step !== agentStep;
|
|
47
|
+
const node = document.querySelector('.setup-progress [data-step="' + step + '"]');
|
|
48
|
+
if (step === agentStep) node.setAttribute('aria-current', 'step'); else node.removeAttribute('aria-current');
|
|
49
|
+
}
|
|
50
|
+
$('agent-back').hidden = agentStep === 1; $('agent-next').hidden = agentStep === 3; $('agent-save').hidden = agentStep !== 3;
|
|
51
|
+
const selected = agentState?.providers.find(p => p.id === agentProvider);
|
|
52
|
+
$('agent-next').disabled = agentStep === 1 ? !(selected?.installed && selected.supported && selected.logged_in) : !$('agent-trust').checked || (currentSetup().scope === 'wallet' && !$('agent-wallet').value);
|
|
53
|
+
$('agent-wallet-field').hidden = currentSetup().scope !== 'wallet';
|
|
54
|
+
if (agentStep === 3) {
|
|
55
|
+
const config = currentSetup();
|
|
56
|
+
$('agent-review').innerHTML = '<dt>Account</dt><dd>' + escapeHTML(selected?.name || '') + '</dd><dt>Model</dt><dd>' + escapeHTML(config.model || 'CLI default') + '</dd><dt>Context</dt><dd>' + escapeHTML(scopeLabel(config)) + '</dd><dt>History</dt><dd>' + (config.retain_history ? 'Saved on this computer' : 'Memory only') + '</dd>';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function loadAgent(recheck = false) {
|
|
60
|
+
if (!localSession?.controls) return;
|
|
61
|
+
const epoch = chatEpoch;
|
|
62
|
+
const status = await localAPI('/api/agent' + (recheck ? '?recheck=1' : ''));
|
|
63
|
+
if (epoch !== chatEpoch) return;
|
|
64
|
+
agentState = status; renderAgent();
|
|
65
|
+
if (agentState.active_turn && !chatTurn && !chatRequest && !setupTesting) {
|
|
66
|
+
const id = agentState.active_turn;
|
|
67
|
+
// Keep Stop available even when the first recovery lookup loses its response.
|
|
68
|
+
chatTurn = id; chatDraftRevision = draftRevision; renderAgent();
|
|
69
|
+
let turn;
|
|
70
|
+
try { turn = await localAPI('/api/chat/turn/' + encodeURIComponent(id)); }
|
|
71
|
+
catch { if (epoch === chatEpoch) { recoverResponse(id, epoch); agentReady = true; } return; }
|
|
72
|
+
if (epoch !== chatEpoch) return;
|
|
73
|
+
if (turn.state === 'running') {
|
|
74
|
+
chatTurn = turn.id; chatRequest = turn.test ? null : {message: turn.message, conversation_id: turn.conversation_id, idempotency_key: turn.id}; renderAgent();
|
|
75
|
+
recoverResponse(turn.id, epoch);
|
|
76
|
+
} else {
|
|
77
|
+
await recoverResponse(id, epoch);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
agentReady = true;
|
|
81
|
+
}
|
|
82
|
+
async function openAgent() {
|
|
83
|
+
if (state?.demo) { toast('The sample workspace does not contact model services. Run kira setup for your personal workspace.'); return; }
|
|
84
|
+
if (!localSession?.controls) { toast('Start your local workspace with kira setup to enable connection settings.'); return; }
|
|
85
|
+
$('agent-error').textContent = ''; $('agent-providers').innerHTML = '<p>Checking installed accounts…</p>';
|
|
86
|
+
$('agent-dialog').showModal(); agentStep = 1; invalidateSetup();
|
|
87
|
+
try {
|
|
88
|
+
await loadAgent();
|
|
89
|
+
const config = agentState.config;
|
|
90
|
+
agentProvider = config?.provider || agentState.providers.find(p => p.installed && p.supported && p.logged_in)?.id || 'codex';
|
|
91
|
+
$('agent-model').value = config?.model || '';
|
|
92
|
+
document.querySelector('[name="agent-scope"][value="' + (config?.scope || 'none') + '"]').checked = true;
|
|
93
|
+
$('agent-wallet').innerHTML = '<option value="">Choose a wallet</option>' + (state?.wallets || []).map(w => `<option value="${escapeHTML(w.key)}">${escapeHTML(w.name)}</option>`).join('');
|
|
94
|
+
$('agent-wallet').value = config?.wallet || ''; $('agent-retain').checked = config?.retain_history || false; $('agent-trust').checked = config?.trust_native_cli || false;
|
|
95
|
+
renderProviders();
|
|
96
|
+
} catch (error) { $('agent-error').textContent = error.message; }
|
|
97
|
+
}
|
|
98
|
+
$('chat-setup').addEventListener('click', openAgent);
|
|
99
|
+
$('settings-agent').addEventListener('click', () => { $('settings-dialog').close(); openAgent(); });
|
|
100
|
+
$('agent-providers').addEventListener('click', event => {
|
|
101
|
+
const button = event.target.closest('[data-provider]'); if (!button || setupTesting) return;
|
|
102
|
+
if (agentProvider !== button.dataset.provider) { agentProvider = button.dataset.provider; $('agent-model').value = ''; invalidateSetup(); renderProviders(); }
|
|
103
|
+
});
|
|
104
|
+
$('agent-cli-help').addEventListener('click', async event => {
|
|
105
|
+
const button = event.target.closest('[data-copy-cli]'); if (!button) return;
|
|
106
|
+
try { await navigator.clipboard.writeText(button.dataset.copyCli); button.textContent = 'Copied'; } catch { toast('Select and copy the command in your terminal.'); }
|
|
107
|
+
});
|
|
108
|
+
for (const id of ['agent-model', 'agent-wallet', 'agent-retain', 'agent-trust']) $(id).addEventListener('input', () => { invalidateSetup(); updateStep(); });
|
|
109
|
+
document.querySelectorAll('[name="agent-scope"]').forEach(input => input.addEventListener('change', () => { invalidateSetup(); updateStep(); }));
|
|
110
|
+
$('agent-next').addEventListener('click', () => { if (!$('agent-next').disabled) { agentStep++; updateStep(); $('agent-dialog').scrollTop = 0; } });
|
|
111
|
+
$('agent-back').addEventListener('click', () => { agentStep--; updateStep(); });
|
|
112
|
+
$('agent-recheck').addEventListener('click', async () => {
|
|
113
|
+
$('agent-recheck').disabled = true; $('agent-error').textContent = '';
|
|
114
|
+
try { await loadAgent(true); invalidateSetup(); renderProviders(); } catch (error) { $('agent-error').textContent = error.message; }
|
|
115
|
+
finally { $('agent-recheck').disabled = false; }
|
|
116
|
+
});
|
|
117
|
+
async function waitTurn(id, valid) {
|
|
118
|
+
while (valid()) {
|
|
119
|
+
const turn = await localAPI('/api/chat/turn/' + encodeURIComponent(id));
|
|
120
|
+
if (turn.state !== 'running') return turn;
|
|
121
|
+
await new Promise(resolve => setTimeout(resolve, 700));
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
$('agent-test').addEventListener('click', async () => {
|
|
126
|
+
if (setupTesting) return;
|
|
127
|
+
setupTesting = true; const revision = setupRevision; $('agent-test').disabled = true; $('agent-back').disabled = true; $('agent-error').textContent = ''; $('agent-test-status').textContent = 'Checking a response with no wallet data…';
|
|
128
|
+
const id = crypto.randomUUID(); setupTurn = id;
|
|
129
|
+
try {
|
|
130
|
+
const turn = await agentPost('/api/agent/test', {config: currentSetup(), idempotency_key: id});
|
|
131
|
+
if (revision !== setupRevision) { await agentPost('/api/chat/cancel', {id: turn.id}); return; }
|
|
132
|
+
const result = await waitTurn(turn.id, () => revision === setupRevision);
|
|
133
|
+
if (!result) return;
|
|
134
|
+
if (result.state !== 'succeeded') throw new Error(result.error?.message || 'Connection check stopped. Retry before saving.');
|
|
135
|
+
if (revision === setupRevision) { agentVerified = true; $('agent-save').disabled = false; $('agent-test-status').textContent = 'Connected. Kira received a real response.'; }
|
|
136
|
+
} catch (error) { await agentPost('/api/chat/cancel', {id}).catch(() => {}); if (revision === setupRevision) { $('agent-test-status').textContent = ''; $('agent-error').textContent = error.message; } }
|
|
137
|
+
finally { setupTesting = false; setupTurn = null; $('agent-test').disabled = false; $('agent-back').disabled = false; }
|
|
138
|
+
});
|
|
139
|
+
$('agent-dialog').addEventListener('close', () => {
|
|
140
|
+
invalidateSetup(); if (setupTurn) agentPost('/api/chat/cancel', {id: setupTurn}).catch(() => {});
|
|
141
|
+
});
|
|
142
|
+
$('agent-save').addEventListener('click', async () => {
|
|
143
|
+
if (!agentVerified) return; $('agent-save').disabled = true; $('agent-error').textContent = '';
|
|
144
|
+
try {
|
|
145
|
+
await agentPost('/api/agent/settings', currentSetup()); chatEpoch++; chatTurn = null; chatRequest = null;
|
|
146
|
+
await loadAgent(); $('agent-dialog').close(); $('kira-draft').focus();
|
|
147
|
+
} catch (error) { $('agent-error').textContent = error.message; $('agent-save').disabled = false; }
|
|
148
|
+
});
|
|
149
|
+
$('kira-draft').addEventListener('input', () => { draftRevision++; $('draft-status').textContent = 'Draft stays in memory until sent.'; renderAgent(); });
|
|
150
|
+
$('kira-draft').addEventListener('keydown', event => {
|
|
151
|
+
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing && event.keyCode !== 229) { event.preventDefault(); if (!$('chat-send').disabled) $('chat-form').requestSubmit(); }
|
|
152
|
+
});
|
|
153
|
+
async function sendChat(event) {
|
|
154
|
+
event.preventDefault(); if (!agentState?.config || chatTurn || chatRequest) return;
|
|
155
|
+
const message = $('kira-draft').value.trim(); if (!message) return;
|
|
156
|
+
const epoch = chatEpoch, draftAtSend = draftRevision;
|
|
157
|
+
chatDraftRevision = draftAtSend;
|
|
158
|
+
chatRequest = chatRetry?.message === message && chatRetry.conversation_id === agentState.conversation_id ? chatRetry : {message, conversation_id: agentState.conversation_id, idempotency_key: crypto.randomUUID()}; chatRetry = chatRequest; renderAgent(); $('chat-error').textContent = '';
|
|
159
|
+
try {
|
|
160
|
+
const turn = await agentPost('/api/chat/send', chatRequest);
|
|
161
|
+
if (epoch !== chatEpoch) return;
|
|
162
|
+
chatTurn = turn.id; if (draftRevision === draftAtSend) $('kira-draft').value = ''; renderAgent();
|
|
163
|
+
const result = await waitTurn(turn.id, () => epoch === chatEpoch);
|
|
164
|
+
if (!result || epoch !== chatEpoch) return;
|
|
165
|
+
if (result.state !== 'succeeded') { if (!$('kira-draft').value && draftRevision === draftAtSend) $('kira-draft').value = message; chatRetry = null; throw new Error(result.error?.message || 'Response stopped. You can edit and send your message again.'); }
|
|
166
|
+
await loadAgent(); chatRetry = null; $('conversation-scroll').scrollTop = $('conversation-scroll').scrollHeight;
|
|
167
|
+
} catch (error) { if (epoch === chatEpoch) { $('chat-error').textContent = error.message; if (chatTurn && chatRetry) { recoverResponse(chatTurn, epoch); return; } } }
|
|
168
|
+
finally { if (epoch === chatEpoch && !(chatTurn && chatRetry)) { chatTurn = null; chatRequest = null; renderAgent(); if (chatRetry) loadAgent().catch(() => {}); } }
|
|
169
|
+
}
|
|
170
|
+
async function recoverResponse(id, epoch) {
|
|
171
|
+
try {
|
|
172
|
+
const result = await waitTurn(id, () => epoch === chatEpoch);
|
|
173
|
+
if (epoch !== chatEpoch || !result) return;
|
|
174
|
+
if (result.state !== 'succeeded') {
|
|
175
|
+
if (!$('kira-draft').value && !result.test && draftRevision === chatDraftRevision) $('kira-draft').value = chatRequest?.message || result.message || '';
|
|
176
|
+
$('chat-error').textContent = result.error?.message || 'Response stopped. You can edit and resend your message.';
|
|
177
|
+
} else $('chat-error').textContent = '';
|
|
178
|
+
const status = await localAPI('/api/agent');
|
|
179
|
+
if (epoch !== chatEpoch || chatTurn !== id) return;
|
|
180
|
+
agentState = status; chatTurn = null; chatRequest = null; chatRetry = null;
|
|
181
|
+
} catch (error) { if (epoch === chatEpoch) { $('chat-error').textContent = 'Reconnecting. ' + error.message; setTimeout(() => { if (epoch === chatEpoch && chatTurn === id) recoverResponse(id, epoch); }, 1500); } }
|
|
182
|
+
finally { if (epoch === chatEpoch) renderAgent(); }
|
|
183
|
+
}
|
|
184
|
+
$('chat-form').addEventListener('submit', sendChat);
|
|
185
|
+
$('chat-stop').addEventListener('click', async () => {
|
|
186
|
+
const id = chatTurn; if (!id) return;
|
|
187
|
+
$('chat-stop').disabled = true;
|
|
188
|
+
try { const result = await agentPost('/api/chat/cancel', {id}); chatEpoch++; chatTurn = null; if (result.state !== 'succeeded' && chatRequest && !$('kira-draft').value) $('kira-draft').value = chatRequest.message; chatRequest = null; chatRetry = null; await loadAgent(); $('chat-error').textContent = result.state === 'succeeded' ? '' : 'Response stopped. You can edit and resend your message.'; }
|
|
189
|
+
catch (error) { $('chat-error').textContent = error.message; }
|
|
190
|
+
finally { $('chat-stop').disabled = false; renderAgent(); }
|
|
191
|
+
});
|
|
192
|
+
$('chat-new').addEventListener('click', async () => {
|
|
193
|
+
if (!localSession?.controls || chatRequest && !chatTurn) return;
|
|
194
|
+
try { await agentPost('/api/chat/reset', {}); chatEpoch++; chatTurn = null; chatRequest = null; chatRetry = null; await loadAgent(); $('chat-error').textContent = ''; $('kira-draft').focus(); }
|
|
195
|
+
catch (error) { $('chat-error').textContent = error.message; }
|
|
196
|
+
});
|
|
197
|
+
async function initialAgent() {
|
|
198
|
+
if (agentLoading || agentReady || !localSession?.controls) return; agentLoading = true;
|
|
199
|
+
try { await loadAgent(); if (new URLSearchParams(location.search).get('setup') === '1') { history.replaceState(null, '', '/#/home'); await openAgent(); } }
|
|
200
|
+
catch { /* The normal local session polling can reconnect. */ }
|
|
201
|
+
finally { agentLoading = false; }
|
|
202
|
+
}
|
|
203
|
+
renderAgent(); initialAgent(); const agentStartup = setInterval(() => { initialAgent(); if (agentReady) clearInterval(agentStartup); }, 1000);
|