kira-wallet 0.1.4 → 0.1.5
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/docs/CLI.md +18 -11
- package/docs/JOB_CONTRACT.md +13 -1
- package/docs/ONBOARDING_CHAT_CONTRACT.md +34 -1
- package/docs/RELEASE.md +17 -9
- package/kira_agent.py +14 -0
- package/kira_cli.py +3 -4
- package/kira_config.py +13 -0
- package/kira_jobs.py +14 -4
- package/kira_models.py +86 -0
- package/package.json +3 -2
- package/sources/job-envelope-v1.schema.json +2 -1
- package/sources/job-request-v1.schema.json +43 -0
- package/viewer/onboarding.py +4 -2
- package/viewer/server.py +3 -1
- package/viewer/static/agent.js +65 -14
- package/viewer/static/app.js +1 -1
- package/viewer/static/index.html +29 -6
- package/viewer/static/jobs.js +105 -8
- package/viewer/static/report.js +2 -2
- package/viewer/static/watching.js +1 -1
- package/viewer/static/workspace-model.js +2 -2
- package/viewer/static/workspace.css +11 -3
package/docs/CLI.md
CHANGED
|
@@ -10,7 +10,7 @@ npm install -g kira-wallet
|
|
|
10
10
|
kira setup
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The operator published npm version 0.1.
|
|
13
|
+
The operator published npm version 0.1.4. Version 0.1.5 is a preparation candidate. GitHub v0.1.3 remains
|
|
14
14
|
a draft; the previous archive and checksum are available from the
|
|
15
15
|
[0.1.2 GitHub release](https://github.com/realproject7/kira-wallet/releases/tag/v0.1.2).
|
|
16
16
|
Registry publication remains an operator action. The code and original character assets use the MIT license.
|
|
@@ -42,14 +42,14 @@ address or use the browser wallet picker. Registration starts one explicit
|
|
|
42
42
|
research job; follow it in Activity and read the saved portfolio report.
|
|
43
43
|
No signature, seed phrase or private key is needed.
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
holdings. Missing coverage keeps the portfolio estimate partial.
|
|
47
|
-
published to npm. Existing snapshots require a holdings refresh after connecting
|
|
45
|
+
Published 0.1.4 explains limited discovery before wallet registration and beside
|
|
46
|
+
holdings. Missing coverage keeps the portfolio estimate partial. Existing snapshots require a holdings refresh after connecting
|
|
48
47
|
an indexer; a price refresh does not discover missing tokens.
|
|
49
48
|
|
|
50
49
|
Public RPC checks known assets and Mint Club. For broader ERC20 discovery,
|
|
51
|
-
open
|
|
52
|
-
|
|
50
|
+
open Data provider connection, select Alchemy and follow its local key guide.
|
|
51
|
+
The 0.1.5 candidate uses one save for Alchemy RPC and discovery; chain overrides
|
|
52
|
+
remain under Advanced. Never paste the key into the browser. `kira doctor`
|
|
53
53
|
checks whether the configured key is available. Configuration alone does not
|
|
54
54
|
prove complete coverage; inspect the recorded results and gaps.
|
|
55
55
|
|
|
@@ -68,8 +68,12 @@ authentication. Verified adapter versions are Codex 0.158.x and Claude Code
|
|
|
68
68
|
verified. This is a native CLI connection, with no Kira account, API-key proxy or
|
|
69
69
|
hosted portfolio backend.
|
|
70
70
|
|
|
71
|
-
In
|
|
72
|
-
|
|
71
|
+
In the 0.1.5 candidate, setup opens the existing Account / Permissions / Ready
|
|
72
|
+
modal directly. Choose Codex or Claude, then Use default model or a readable
|
|
73
|
+
model choice. Codex choices come from bounded native metadata; Claude offers
|
|
74
|
+
Sonnet, Opus and Haiku aliases. A catalog is not proof of account access.
|
|
75
|
+
Advanced accepts a custom identifier and preserves saved identifiers absent
|
|
76
|
+
from the catalog. Workspace settings reopens the same selector and permissions. Choose no automatic wallet context, one registered wallet,
|
|
73
77
|
or the whole recorded portfolio. A generic response check sends no wallet data
|
|
74
78
|
and must pass before these settings are saved.
|
|
75
79
|
|
|
@@ -79,7 +83,10 @@ and contract identities, timestamps, unknown prices and coverage. Credentials,
|
|
|
79
83
|
RPC endpoints, raw provider errors and local paths are excluded. Changing the
|
|
80
84
|
provider, model, context or selected wallet creates a new conversation.
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
New setup in 0.1.5 recommends saving history and preselects it. Existing saved
|
|
87
|
+
true and false choices are restored exactly. Saving lets you reopen chats after
|
|
88
|
+
restart; it does not expand model memory or wallet access. The choice and
|
|
89
|
+
research permission remain editable in Workspace settings. Kira-local history uses private `conversations/` files. With history
|
|
83
90
|
off, conversations and drafts stay in memory. A new server process starts a fresh
|
|
84
91
|
conversation; saved files remain available locally and are not automatically
|
|
85
92
|
replayed. Native CLI, administrator policy and provider data policies remain a
|
|
@@ -135,8 +142,8 @@ kira config import-env --file /absolute/private/rpc.env --key-env ALCHEMY_API_KE
|
|
|
135
142
|
```
|
|
136
143
|
|
|
137
144
|
The import references the file instead of copying keys. It configures Alchemy
|
|
138
|
-
RPC and portfolio discovery
|
|
139
|
-
|
|
145
|
+
RPC and portfolio discovery together, preserving existing custom chain overrides
|
|
146
|
+
and explicit fallback preferences. New configurations allow public fallback. Environment overrides the file. Missing files or keys
|
|
140
147
|
produce limited coverage; malformed configuration fails with safe errors.
|
|
141
148
|
The import does not contact providers. Private settings are never served.
|
|
142
149
|
|
package/docs/JOB_CONTRACT.md
CHANGED
|
@@ -8,7 +8,8 @@ this contract. Chat interruption never cancels an engine job.
|
|
|
8
8
|
Mutations use `{schema_version: 1, operation, input, idempotency_key}`.
|
|
9
9
|
Supported operations are `wallet.add` (address, exact tag), `wallet.refresh`
|
|
10
10
|
(wallet address or exact tag), `prices.refresh` (same), `wallet.setTags`
|
|
11
|
-
(wallet, exact tags), `
|
|
11
|
+
(wallet, exact tags), `settings.rpc`, `settings.discovery`, `settings.provider`,
|
|
12
|
+
`job.resume` and `job.cancel` (job_id).
|
|
12
13
|
Read operations are `job.read`, `job.list`, `snapshot.read`, and
|
|
13
14
|
`snapshots.compare`. The portfolio projection retains its existing schema.
|
|
14
15
|
|
|
@@ -21,6 +22,17 @@ the same job without rerunning providers. Resume requeues the same job ID and
|
|
|
21
22
|
increments its attempt on execution. Retain the key until the job is removed
|
|
22
23
|
by a future explicit retention policy. Version mismatches fail closed.
|
|
23
24
|
|
|
25
|
+
`settings.provider` accepts exactly `{provider: "public" | "alchemy", key_env}`.
|
|
26
|
+
The key reference is a bounded environment variable name, never a credential.
|
|
27
|
+
One writer-locked atomic replacement changes RPC mode and discovery together.
|
|
28
|
+
Alchemy updates generated network references while preserving explicit custom
|
|
29
|
+
chain overrides, unrelated chains, explorer choice and public fallback policy.
|
|
30
|
+
A missing local key fails before replacement. Public mode keeps inactive custom
|
|
31
|
+
references for later use. A detected key is not a validated provider or complete
|
|
32
|
+
holdings result. Existing settings.rpc and settings.discovery requests remain
|
|
33
|
+
compatible. The browser waits for terminal settings results; retrying a lost
|
|
34
|
+
result checks the same accepted job instead of submitting another change.
|
|
35
|
+
|
|
24
36
|
## Envelope
|
|
25
37
|
|
|
26
38
|
Each private `jobs/<uuid>.json` records schema_version, job_id, operation,
|
|
@@ -4,6 +4,27 @@ Kira connects the user's installed, unmodified Codex or Claude Code CLI. It does
|
|
|
4
4
|
|
|
5
5
|
The setup flow separates account readiness, model choice, context permission, and confirmation. The conversation composer remains visible. It shows the connected CLI, automatic context scope, response state, cancellation, and a new conversation action. Enter sends; Shift+Enter adds a line; composition events never send.
|
|
6
6
|
|
|
7
|
+
`kira setup` opens the existing Account, Permissions and Ready modal directly
|
|
8
|
+
after the local session and workspace are available. Opening setup does not run
|
|
9
|
+
a model response check, save permissions or start wallet research. Existing
|
|
10
|
+
settings are loaded when setup is reopened. Closing the modal consumes that
|
|
11
|
+
page's setup request, so background polling does not reopen it. Ordinary
|
|
12
|
+
`kira start` keeps the normal workspace entry behavior.
|
|
13
|
+
|
|
14
|
+
The 0.1.5 selector defaults to Use default model. Codex uses a bounded stdio
|
|
15
|
+
app-server handshake and model/list request; it never starts a thread or turn.
|
|
16
|
+
The subprocess has an eight-second metadata deadline and 256 KB output bound.
|
|
17
|
+
Metadata failures leave the default and Advanced identifier entry available.
|
|
18
|
+
Only installed supported CLI versions are queried. Claude uses its documented
|
|
19
|
+
Sonnet, Opus and Haiku aliases. Every chosen model still requires a deliberate
|
|
20
|
+
generic response test. Vendor account and managed policies may restrict access.
|
|
21
|
+
Workspace settings reuses this modal for both providers and all permissions.
|
|
22
|
+
Fresh setup preselects local history; existing false values remain false.
|
|
23
|
+
History permits reopening chats, without extending model memory or scope.
|
|
24
|
+
No wallet context permits general chat and manual wallet management while
|
|
25
|
+
wallet context and research tools remain unavailable. Any effective model or
|
|
26
|
+
permission change starts a fresh conversation.
|
|
27
|
+
|
|
7
28
|
Automatic context is none, one explicitly selected registered wallet, or the whole recorded portfolio. An allowlisted projection preserves exact balances, contract and chain identities, timestamps, unknown values, and coverage gaps. It excludes credentials, RPC endpoints, paths, raw errors, jobs, and provider payloads. User-entered text is also sent to the selected provider. Scope, wallet, provider, or model changes start a new conversation and never replay old messages. Only a bounded current conversation is replayed.
|
|
8
29
|
|
|
9
30
|
Approved wallet context can include validated historical full-balance Mint Club
|
|
@@ -53,7 +74,7 @@ in Activity and also appear in the chat tray. Unanalysed wallets show a sidebar
|
|
|
53
74
|
spinner and open Activity while their first holdings job is active.
|
|
54
75
|
|
|
55
76
|
History is capped at 100 memory sessions and reads up to 200 validated local
|
|
56
|
-
files. Saving remains
|
|
77
|
+
files. Saving is recommended for fresh setup and remains an editable choice. Memory sessions disappear when the process stops.
|
|
57
78
|
A previous session can continue only with exactly the same model and context
|
|
58
79
|
configuration. Reading history does not expand model access. Old saved files
|
|
59
80
|
remain readable. No missing memory-only conversations are reconstructed.
|
|
@@ -65,3 +86,15 @@ The receipt is consumed only on success. This does not enable history retention.
|
|
|
65
86
|
The Kira system voice is concise, composed and practical. It leads with findings
|
|
66
87
|
and preserves evidence, unknown values and signing boundaries. Personality is
|
|
67
88
|
prompt guidance; response quality still depends on the selected model.
|
|
89
|
+
|
|
90
|
+
## 0.1.5 preparation evidence
|
|
91
|
+
|
|
92
|
+
The combined settings and model-selection tests cover catalog failures, stale
|
|
93
|
+
responses, custom identifiers, fresh retention defaults, saved opt-outs,
|
|
94
|
+
atomic provider changes, preference preservation, missing keys, cancellation
|
|
95
|
+
and ambiguous receipts. Desktop 1440 × 900 and mobile 390 × 844 checks used an
|
|
96
|
+
empty disposable workspace. Codex 0.158.0 and Claude Code 2.1.293 completed
|
|
97
|
+
native default-model response checks with no wallet data. The provider save
|
|
98
|
+
used a synthetic local key reference and made no provider research request.
|
|
99
|
+
Real extension approval, personal OWS passphrases and live holdings coverage
|
|
100
|
+
are separate operator acceptance. Published 0.1.4 snapshots are not rewritten.
|
package/docs/RELEASE.md
CHANGED
|
@@ -32,7 +32,7 @@ creation now identifies Open Wallet Standard with its official locally served
|
|
|
32
32
|
logo and explains encrypted local storage and public-account research. The
|
|
33
33
|
OWS integration and wallet-operation permissions are unchanged.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Published **0.1.4** makes incomplete token discovery visible
|
|
36
36
|
before registration and beside wallet holdings, separates disabled discovery
|
|
37
37
|
from missing credentials and provider failures, and offers a connection or
|
|
38
38
|
holdings-refresh action. Registry failures retain bounded redacted examples
|
|
@@ -44,10 +44,19 @@ RPC failures show affected networks, retry guidance and a settings action.
|
|
|
44
44
|
Failed candidate balance reads keep the saved result and valuation partial
|
|
45
45
|
even when general token discovery succeeded.
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
The next candidate is **0.1.5**. It opens the existing setup modal directly,
|
|
48
|
+
adds readable Codex and Claude model choices, and exposes model and permission
|
|
49
|
+
changes in Workspace settings. New setup recommends local history; upgrades
|
|
50
|
+
preserve explicit opt-outs. Watching wallet methods use individual buttons.
|
|
51
|
+
Data provider connection offers one atomic Alchemy RPC and discovery save,
|
|
52
|
+
with custom overrides under Advanced. A missing key keeps prior settings.
|
|
53
|
+
The wallet draft survives a connection detour. Connecting still requires an
|
|
54
|
+
explicit holdings refresh before missing assets can appear in saved evidence.
|
|
55
|
+
|
|
56
|
+
Version 0.1.5 requires independent review and operator registry publication.
|
|
57
|
+
It is not yet available from the registry. Keep published 0.1.4 and earlier
|
|
58
|
+
package bytes immutable. The showcase remains a separate deployment.
|
|
59
|
+
See [discovery acceptance](KIRA_DISCOVERY_ACCEPTANCE.md).
|
|
51
60
|
|
|
52
61
|
The operator published npm **0.1.2** on 2026-10-05. Its registry archive was
|
|
53
62
|
downloaded and matched the reviewed GitHub archive above, including SHA-256,
|
|
@@ -69,8 +78,7 @@ The earlier unpublished 0.1.2 candidate is preserved in the ignored
|
|
|
69
78
|
`dist/preserved-0.1.2-bad8f02/` directory. Use the current `dist/release.json`
|
|
70
79
|
and its matching archive for publication; do not mix manifests from candidates.
|
|
71
80
|
|
|
72
|
-
Keep GitHub v0.1.1, its tag and its released archive immutable. Versions 0.1.2
|
|
73
|
-
and 0.1.3 are also published and immutable. Do not rebuild them from later documentation
|
|
81
|
+
Keep GitHub v0.1.1, its tag and its released archive immutable. Versions 0.1.2, 0.1.3 and 0.1.4 are also published and immutable. Do not rebuild them from later documentation
|
|
74
82
|
commits. Future package changes need a new version. Release preparation does not
|
|
75
83
|
publish a GitHub release, merge PR 19, promote the website or publish to npm.
|
|
76
84
|
Registry publication remains operator-only. The coding agent never publishes.
|
|
@@ -85,12 +93,12 @@ python3 scripts/prepare-release.py
|
|
|
85
93
|
```
|
|
86
94
|
|
|
87
95
|
`prepare-release.py` scans the package boundary and creates
|
|
88
|
-
`dist/kira-wallet-0.1.
|
|
96
|
+
`dist/kira-wallet-0.1.5.tgz`. The ignored `dist/release.json` records its source
|
|
89
97
|
commit, SHA-256 and member count. It never publishes. After reviewing that
|
|
90
98
|
manifest, the operator's final command from the project root is:
|
|
91
99
|
|
|
92
100
|
```sh
|
|
93
|
-
npm publish ./dist/kira-wallet-0.1.
|
|
101
|
+
npm publish ./dist/kira-wallet-0.1.5.tgz --access public
|
|
94
102
|
```
|
|
95
103
|
|
|
96
104
|
Use the operator's npm account and complete authentication or OTP personally.
|
package/kira_agent.py
CHANGED
|
@@ -251,6 +251,7 @@ class AgentStore:
|
|
|
251
251
|
self.root = Path(root);self.path = self.root/'.kira-agent.json';self.runner=runner;self.detector=detector
|
|
252
252
|
self.lock=threading.RLock();self.turns={};self.active=None;self.epoch=0;self.messages=[];self.conversation=str(uuid.uuid4());self.tested=set();self.config=None;self.workers=[];self.tool_status=None
|
|
253
253
|
self._capabilities=None;self._checked=0
|
|
254
|
+
self._model_lock=threading.Lock();self._model_cache={}
|
|
254
255
|
self.sessions={};self.started_at=datetime.now(timezone.utc).isoformat();self.updated_at=self.started_at
|
|
255
256
|
if self.path.exists():
|
|
256
257
|
try: self.config=config_input(json.loads(self.path.read_text()),self.root)
|
|
@@ -344,6 +345,19 @@ class AgentStore:
|
|
|
344
345
|
self._capabilities=self.detector();self._checked=time.monotonic()
|
|
345
346
|
with self.lock:return {'config':self.config,'providers':self._capabilities,'conversation_id':self.conversation,'messages':self.messages.copy(),'active_turn':self.active,'tool_status':self.tool_status}
|
|
346
347
|
|
|
348
|
+
def models(self, provider, recheck=False):
|
|
349
|
+
if provider not in PROVIDERS:raise JobError('invalid_provider','Choose Codex or Claude.')
|
|
350
|
+
with self._model_lock:
|
|
351
|
+
row=next((row for row in self.status(recheck)['providers'] if row['id']==provider),None)
|
|
352
|
+
if not row or not row['installed'] or not row['supported']:
|
|
353
|
+
return {'models':[], 'note':'Install a supported CLI to load model choices. The default model remains available.'}
|
|
354
|
+
cached=self._model_cache.get(provider)
|
|
355
|
+
if not recheck and cached and time.monotonic()-cached[0]<60:return cached[1]
|
|
356
|
+
from kira_models import catalog
|
|
357
|
+
result=catalog(provider)
|
|
358
|
+
self._model_cache[provider]=(time.monotonic(),result)
|
|
359
|
+
return result
|
|
360
|
+
|
|
347
361
|
def fingerprint(self,config):return hashlib.sha256(json.dumps(config,sort_keys=True).encode()).hexdigest()
|
|
348
362
|
|
|
349
363
|
def stop(self):
|
package/kira_cli.py
CHANGED
|
@@ -232,10 +232,9 @@ def main():
|
|
|
232
232
|
if args.command=='config':
|
|
233
233
|
file=Path(args.file).expanduser().resolve()
|
|
234
234
|
if not file.is_file():raise ValueError('Secret environment file does not exist.')
|
|
235
|
-
from
|
|
236
|
-
cfg
|
|
237
|
-
cfg['
|
|
238
|
-
'chains':{str(cid):{'url_env':args.key_env,'alchemy_network':name} for cid,name in ALCHEMY.items()}}
|
|
235
|
+
from kira_config import provider_config
|
|
236
|
+
cfg=provider_config(cfg,'alchemy',args.key_env)
|
|
237
|
+
cfg['secret_env_file']=str(file);cfg['discovery']['explorers']=True
|
|
239
238
|
elif args.command=='discovery':cfg['discovery']={'provider':args.provider,'key_env':args.key_env,'explorers':args.explorers}
|
|
240
239
|
elif args.setting=='public':cfg['rpc']['mode']='public'
|
|
241
240
|
else:
|
package/kira_config.py
CHANGED
|
@@ -19,6 +19,19 @@ def default_config():
|
|
|
19
19
|
return {'schema_version':1,'rpc':{'mode':'public','allow_public_fallback':True,'chains':{}},
|
|
20
20
|
'discovery':{'provider':'none','key_env':'ALCHEMY_API_KEY','explorers':False}}
|
|
21
21
|
|
|
22
|
+
def provider_config(config, provider, key_env):
|
|
23
|
+
"""One settings transition. Preserve explicit fallback and custom overrides."""
|
|
24
|
+
result={**config,'rpc':{**config['rpc'],'chains':{cid:dict(row) for cid,row in config['rpc']['chains'].items()}},
|
|
25
|
+
'discovery':{**config['discovery'],'provider':'alchemy' if provider=='alchemy' else 'none','key_env':key_env}}
|
|
26
|
+
result['rpc']['mode']='custom' if provider=='alchemy' else 'public'
|
|
27
|
+
if provider=='alchemy':
|
|
28
|
+
from wallet import ALCHEMY
|
|
29
|
+
for cid,network in ALCHEMY.items():
|
|
30
|
+
previous=result['rpc']['chains'].get(str(cid))
|
|
31
|
+
if not previous or previous.get('alchemy_network'):
|
|
32
|
+
result['rpc']['chains'][str(cid)]={'url_env':key_env,'alchemy_network':network}
|
|
33
|
+
return result
|
|
34
|
+
|
|
22
35
|
def load_config():
|
|
23
36
|
path=config_path()
|
|
24
37
|
if not path.exists():return default_config()
|
package/kira_jobs.py
CHANGED
|
@@ -19,7 +19,7 @@ import uuid
|
|
|
19
19
|
|
|
20
20
|
ASSETS = Path(__file__).resolve().parent
|
|
21
21
|
State = Literal['queued', 'running', 'interrupted', 'succeeded', 'partial', 'failed', 'cancelled']
|
|
22
|
-
Operation = Literal['wallet.add','wallet.refresh','prices.refresh','wallet.setTags','settings.rpc','settings.discovery']
|
|
22
|
+
Operation = Literal['wallet.add','wallet.refresh','prices.refresh','wallet.setTags','settings.rpc','settings.discovery','settings.provider']
|
|
23
23
|
TERMINAL = {'succeeded', 'partial', 'failed', 'cancelled', 'interrupted'}
|
|
24
24
|
ADDRESS = re.compile(r'0x[0-9a-fA-F]{40}')
|
|
25
25
|
|
|
@@ -124,10 +124,10 @@ class JobStore:
|
|
|
124
124
|
if not isinstance(job,dict) or type(job.get('schema_version')) is not int or job.get('schema_version') != 1: raise JobError('unsupported_version', 'Unsupported persisted job version.')
|
|
125
125
|
required={'schema_version','job_id','operation','input','state','attempt','sequence','result','previous_result','key_hash','request_hash','created_at','updated_at','cancel_requested','checkpoint','stage','events','errors','chains'}
|
|
126
126
|
optional={'raw_hash','control_receipts','control_raw','provider_configuration','started_at'}
|
|
127
|
-
if required-set(job) or set(job)-required-optional or job['job_id']!=job_id or job['checkpoint']!='snapshots/jobs/'+job_id or not isinstance(job['operation'],str) or job['operation'] not in {'wallet.add','wallet.refresh','prices.refresh','wallet.setTags','settings.rpc','settings.discovery'} or not isinstance(job['state'],str) or job['state'] not in TERMINAL|{'queued','running'} or any(type(job[k]) is not int or job[k]<0 for k in ('attempt','sequence')) or type(job['cancel_requested']) is not bool or not isinstance(job['input'],dict) or any(not isinstance(job[k],list) for k in ('events','errors')) or not isinstance(job['chains'],dict):
|
|
127
|
+
if required-set(job) or set(job)-required-optional or job['job_id']!=job_id or job['checkpoint']!='snapshots/jobs/'+job_id or not isinstance(job['operation'],str) or job['operation'] not in {'wallet.add','wallet.refresh','prices.refresh','wallet.setTags','settings.rpc','settings.discovery','settings.provider'} or not isinstance(job['state'],str) or job['state'] not in TERMINAL|{'queued','running'} or any(type(job[k]) is not int or job[k]<0 for k in ('attempt','sequence')) or type(job['cancel_requested']) is not bool or not isinstance(job['input'],dict) or any(not isinstance(job[k],list) for k in ('events','errors')) or not isinstance(job['chains'],dict):
|
|
128
128
|
raise JobError('invalid_envelope','Persisted job validation failed. Preserve the file for recovery.')
|
|
129
129
|
names={'wallet.add':['address','tag'],'wallet.refresh':['wallet'],'prices.refresh':['wallet'],'wallet.setTags':['wallet','tags'],
|
|
130
|
-
'settings.rpc':['mode','chains','allow_public_fallback'],'settings.discovery':['provider','key_env']}[job['operation']]
|
|
130
|
+
'settings.rpc':['mode','chains','allow_public_fallback'],'settings.discovery':['provider','key_env'],'settings.provider':['provider','key_env']}[job['operation']]
|
|
131
131
|
fields(job['input'],names)
|
|
132
132
|
wallet=job['input'].get('wallet') or job['input'].get('address')
|
|
133
133
|
if wallet is not None and (not isinstance(wallet,str) or not ADDRESS.fullmatch(wallet)):raise JobError('invalid_envelope','Persisted wallet identity is invalid.')
|
|
@@ -154,6 +154,11 @@ class JobStore:
|
|
|
154
154
|
return matches[0]
|
|
155
155
|
|
|
156
156
|
def normalize(self, operation, value):
|
|
157
|
+
if operation == 'settings.provider':
|
|
158
|
+
fields(value,['provider','key_env'])
|
|
159
|
+
if value['provider'] not in ('public','alchemy') or not isinstance(value['key_env'],str) or not re.fullmatch(r'[A-Z_][A-Z0-9_]{0,99}',value['key_env']):
|
|
160
|
+
raise JobError('invalid_settings','Choose a data provider and a local credential reference.')
|
|
161
|
+
return value.copy()
|
|
157
162
|
if operation == 'settings.rpc':
|
|
158
163
|
fields(value,['mode','chains','allow_public_fallback'])
|
|
159
164
|
if value['mode'] not in ('public','custom') or type(value['allow_public_fallback']) is not bool or not isinstance(value['chains'],list) or len(value['chains'])>30:
|
|
@@ -403,7 +408,12 @@ class JobStore:
|
|
|
403
408
|
previous=config['rpc']['chains'].get(key,{})
|
|
404
409
|
if row['url_env']==previous.get('url_env') and previous.get('alchemy_network'):row['alchemy_network']=previous['alchemy_network']
|
|
405
410
|
config['rpc']=updated
|
|
406
|
-
|
|
411
|
+
elif job['operation']=='settings.discovery':config['discovery']={**job['input'],'explorers':config['discovery'].get('explorers',False)}
|
|
412
|
+
else:
|
|
413
|
+
from kira_config import provider_config,secret_values
|
|
414
|
+
config=provider_config(config,**job['input'])
|
|
415
|
+
if job['input']['provider']=='alchemy' and not secret_values(config).get(job['input']['key_env']):
|
|
416
|
+
raise JobError('connection_key_missing','Kira cannot find the local Alchemy key. Follow the connection guide, then retry. Existing settings are unchanged.')
|
|
407
417
|
atomic(config_path(),config)
|
|
408
418
|
result={'status':'completed','setting':job['operation']};atomic(self.jobs/'results'/(job['job_id']+'.json'),result);return result
|
|
409
419
|
env = {**os.environ, 'KIRA_DATA_DIR': str(self.root), 'KIRA_JOB_ID': job['job_id'], 'KIRA_JOB_SNAPSHOT': job['checkpoint'], 'KIRA_ANALYSIS_FD': str(writer.fileno()), 'PYTHONUNBUFFERED': '1'}
|
package/kira_models.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Bounded native model metadata. No inference, threads or credential copying."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import selectors
|
|
6
|
+
import shutil
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
MODEL = re.compile(r'[a-zA-Z0-9._:/-]{1,100}')
|
|
13
|
+
|
|
14
|
+
def clean_catalog(rows):
|
|
15
|
+
result=[]; seen=set()
|
|
16
|
+
if not isinstance(rows,list):return result
|
|
17
|
+
for row in rows[:100]:
|
|
18
|
+
if not isinstance(row,dict) or row.get('hidden'):continue
|
|
19
|
+
model=row.get('model') or row.get('id')
|
|
20
|
+
if not isinstance(model,str) or not MODEL.fullmatch(model) or model in seen:continue
|
|
21
|
+
name=row.get('displayName') or model
|
|
22
|
+
if not isinstance(name,str):name=model
|
|
23
|
+
name=' '.join(name.split())[:100]
|
|
24
|
+
result.append({'id':model,'name':name or model});seen.add(model)
|
|
25
|
+
if len(result)==40:break
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
def codex_catalog(path, env, *, timeout=8):
|
|
29
|
+
from kira_agent import CODEX_DISABLED
|
|
30
|
+
argv=[path,'app-server','--listen','stdio://','-c','analytics.enabled=false',
|
|
31
|
+
'-c','history.persistence="none"','-c','mcp_servers={}', '-c','model_provider="openai"']
|
|
32
|
+
for feature in CODEX_DISABLED:argv+=['-c',f'features.{feature}=false']
|
|
33
|
+
with tempfile.TemporaryDirectory(prefix='kira-models-') as cwd:
|
|
34
|
+
child=subprocess.Popen(argv,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,
|
|
35
|
+
cwd=cwd,env=env,start_new_session=True)
|
|
36
|
+
selector=selectors.DefaultSelector();selector.register(child.stdout,selectors.EVENT_READ)
|
|
37
|
+
def send(value):
|
|
38
|
+
child.stdin.write((json.dumps(value)+'\n').encode());child.stdin.flush()
|
|
39
|
+
try:
|
|
40
|
+
send({'id':1,'method':'initialize','params':{'clientInfo':{'name':'kira_model_picker','version':'0.1.5'}}})
|
|
41
|
+
deadline=time.monotonic()+timeout;buffer=b'';received=0;requested=False
|
|
42
|
+
while time.monotonic()<deadline:
|
|
43
|
+
for key,_ in selector.select(min(.2,max(0,deadline-time.monotonic()))):
|
|
44
|
+
chunk=os.read(key.fileobj.fileno(),65536)
|
|
45
|
+
if not chunk:raise ValueError('Catalog ended.')
|
|
46
|
+
received+=len(chunk)
|
|
47
|
+
if received>262144:raise ValueError('Catalog too large.')
|
|
48
|
+
buffer+=chunk
|
|
49
|
+
while b'\n' in buffer:
|
|
50
|
+
line,buffer=buffer.split(b'\n',1)
|
|
51
|
+
try:message=json.loads(line)
|
|
52
|
+
except ValueError:continue
|
|
53
|
+
if not isinstance(message,dict):continue
|
|
54
|
+
if message.get('id')==1 and not requested:
|
|
55
|
+
if 'error' in message:raise ValueError('Initialization failed.')
|
|
56
|
+
requested=True
|
|
57
|
+
send({'method':'initialized'})
|
|
58
|
+
send({'id':2,'method':'model/list','params':{'limit':100,'includeHidden':False}})
|
|
59
|
+
elif message.get('id')==2 and requested:
|
|
60
|
+
if 'error' in message:raise ValueError('Catalog unavailable.')
|
|
61
|
+
result=message.get('result')
|
|
62
|
+
return clean_catalog(result.get('data')) if isinstance(result,dict) else []
|
|
63
|
+
raise TimeoutError('Catalog timed out.')
|
|
64
|
+
finally:
|
|
65
|
+
selector.close()
|
|
66
|
+
try:os.killpg(child.pid,signal.SIGTERM)
|
|
67
|
+
except ProcessLookupError:pass
|
|
68
|
+
try:child.wait(timeout=2)
|
|
69
|
+
except subprocess.TimeoutExpired:
|
|
70
|
+
try:os.killpg(child.pid,signal.SIGKILL)
|
|
71
|
+
except ProcessLookupError:pass
|
|
72
|
+
child.wait()
|
|
73
|
+
child.stdin.close();child.stdout.close()
|
|
74
|
+
|
|
75
|
+
def catalog(provider):
|
|
76
|
+
from kira_agent import native_env
|
|
77
|
+
if provider=='claude':
|
|
78
|
+
return {'models':[{'id':'sonnet','name':'Sonnet'}, {'id':'opus','name':'Opus'}, {'id':'haiku','name':'Haiku'}],
|
|
79
|
+
'note':'Claude CLI model choices. Check a response to verify access for your account.'}
|
|
80
|
+
if provider!='codex':raise ValueError('Unsupported provider.')
|
|
81
|
+
try:
|
|
82
|
+
path=shutil.which('codex')
|
|
83
|
+
models=codex_catalog(path,native_env()) if path else []
|
|
84
|
+
except (OSError,ValueError,TypeError,TimeoutError):models=[]
|
|
85
|
+
return {'models':models,'note':'CLI catalog choices. Check a response to verify account access.' if models else
|
|
86
|
+
'Model choices are unavailable. Use the default model, recheck, or enter an ID under Advanced.'}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kira-wallet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Manage your wallets with your own AI. Track tokens, research markets and refresh holdings in a local workspace.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
"kira_ows.py",
|
|
57
57
|
"ows_bridge.cjs",
|
|
58
58
|
"docs/KIRA_LIVE_QA.md",
|
|
59
|
-
"docs/KIRA_DISCOVERY_ACCEPTANCE.md"
|
|
59
|
+
"docs/KIRA_DISCOVERY_ACCEPTANCE.md",
|
|
60
|
+
"kira_models.py"
|
|
60
61
|
],
|
|
61
62
|
"scripts": {
|
|
62
63
|
"test": "python3 -m unittest discover -s . -p 'test_*.py' && python3 -m unittest discover -s viewer -p 'test_*.py' && node test-onchain.cjs && node --check viewer/static/app.js && node --check viewer/static/jobs.js && node test-workspace.cjs && node --check viewer/static/workspace.js && node --check viewer/static/workspace-model.js && node test-watching.cjs && node test-watching-ui.cjs && node --check viewer/static/watching.js && node --check viewer/static/watching-model.js && node --check viewer/static/agent.js && node test-agent-ui.cjs && node test-markdown.cjs && node --check viewer/static/report.js && node test-report-ui.cjs && node test-holdings-ui.cjs && node test-rpc-fallback.cjs && node test-ows.cjs && node test-ows-ui.cjs && node --check viewer/static/ows.js && node --check viewer/static/chat-workspace.js && node --check viewer/static/selects.js && node --check site/demo.js"
|
|
@@ -347,6 +347,49 @@
|
|
|
347
347
|
"idempotency_key"
|
|
348
348
|
],
|
|
349
349
|
"additionalProperties": false
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"type": "object",
|
|
353
|
+
"properties": {
|
|
354
|
+
"schema_version": {
|
|
355
|
+
"const": 1
|
|
356
|
+
},
|
|
357
|
+
"operation": {
|
|
358
|
+
"const": "settings.provider"
|
|
359
|
+
},
|
|
360
|
+
"input": {
|
|
361
|
+
"type": "object",
|
|
362
|
+
"properties": {
|
|
363
|
+
"provider": {
|
|
364
|
+
"enum": [
|
|
365
|
+
"public",
|
|
366
|
+
"alchemy"
|
|
367
|
+
]
|
|
368
|
+
},
|
|
369
|
+
"key_env": {
|
|
370
|
+
"type": "string",
|
|
371
|
+
"pattern": "^[A-Z_][A-Z0-9_]{0,99}$"
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
"required": [
|
|
375
|
+
"provider",
|
|
376
|
+
"key_env"
|
|
377
|
+
],
|
|
378
|
+
"additionalProperties": false
|
|
379
|
+
},
|
|
380
|
+
"idempotency_key": {
|
|
381
|
+
"type": "string",
|
|
382
|
+
"minLength": 8,
|
|
383
|
+
"maxLength": 200
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
"required": [
|
|
387
|
+
"schema_version",
|
|
388
|
+
"operation",
|
|
389
|
+
"input",
|
|
390
|
+
"idempotency_key"
|
|
391
|
+
],
|
|
392
|
+
"additionalProperties": false
|
|
350
393
|
}
|
|
351
394
|
]
|
|
352
395
|
}
|
package/viewer/onboarding.py
CHANGED
|
@@ -6,8 +6,10 @@ def readiness():
|
|
|
6
6
|
config = load_config()
|
|
7
7
|
discovery = config['discovery']
|
|
8
8
|
configured = discovery['provider'] != 'none'
|
|
9
|
-
|
|
9
|
+
local_available = bool(secret_values(config).get(discovery['key_env']))
|
|
10
|
+
available = configured and local_available
|
|
10
11
|
return {'rpc_mode': config['rpc']['mode'],
|
|
11
12
|
'discovery_provider': discovery['provider'],
|
|
12
13
|
'discovery_configured': configured,
|
|
13
|
-
'discovery_key_available': bool(available)
|
|
14
|
+
'discovery_key_available': bool(available),
|
|
15
|
+
'local_key_available': local_available}
|
package/viewer/server.py
CHANGED
|
@@ -100,7 +100,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
100
100
|
self.json({'instance':os.environ.get('KIRA_INSTANCE_ID'),'read_only':not getattr(self.server,'controls',False),'controls':getattr(self.server,'controls',False)});return
|
|
101
101
|
if path=='/api/session':
|
|
102
102
|
self.json({'controls':getattr(self.server,'controls',False),'token':self.server.session_token if getattr(self.server,'controls',False) else None});return
|
|
103
|
-
if path.startswith('/api/jobs') or path.startswith('/api/chat/turn/') or path in ('/api/agent','/api/chat','/api/chat/history','/api/onboarding','/api/snapshots','/api/snapshot','/api/compare','/api/settings','/api/ows'):
|
|
103
|
+
if path.startswith('/api/jobs') or path.startswith('/api/chat/turn/') or path in ('/api/agent','/api/agent/models','/api/chat','/api/chat/history','/api/onboarding','/api/snapshots','/api/snapshot','/api/compare','/api/settings','/api/ows'):
|
|
104
104
|
if not self.authenticated():return
|
|
105
105
|
query=parse_qs(urlparse(self.path).query)
|
|
106
106
|
if path in ('/api/agent','/api/chat'):
|
|
@@ -108,6 +108,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
108
108
|
self.json({'config':None,'providers':[],'conversation_id':None,'messages':[],'active_turn':None})
|
|
109
109
|
else:self.json(self.server.agent.status(query.get('recheck')==['1']))
|
|
110
110
|
elif path.startswith('/api/chat/turn/'):self.json(self.server.agent.read(path.removeprefix('/api/chat/turn/')))
|
|
111
|
+
elif path=='/api/agent/models':
|
|
112
|
+
self.json({'models':[],'note':'Model choices are available in a personal workspace.'} if json.loads((ROOT/'wallets.json').read_text()).get('demo') else self.server.agent.models(query.get('provider',[''])[0],query.get('recheck')==['1']))
|
|
111
113
|
elif path=='/api/chat/history':self.json(self.server.agent.history(query.get('id',[None])[0]))
|
|
112
114
|
elif path=='/api/jobs':self.json(self.server.jobs.list())
|
|
113
115
|
elif path.startswith('/api/jobs/'):self.json(self.server.jobs.get(path.removeprefix('/api/jobs/')))
|
package/viewer/static/agent.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
let agentState = null, agentProvider = 'codex', agentStep = 1, setupRevision = 0, agentVerified = false;
|
|
3
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;
|
|
4
|
+
let chatSignature = '', agentReady = false, chatDraftRevision = 0, modelCatalogRevision = 0, modelOpenRevision = 0;
|
|
5
|
+
let modelChoices = [], modelChoicesProvider = null;
|
|
5
6
|
const scopeNames = {none: 'No wallet context', wallet: 'One approved wallet', portfolio: 'Whole portfolio'};
|
|
6
7
|
const questionStarters = {
|
|
7
8
|
liquidity: 'Which tokens in my wallets may have been sitting idle, and what could I receive if I sold my full balance? Show each token’s chain, contract, quantity, price and timestamp, pool pair and venue, spot value, and any recorded full-balance output. Use actual transfer-history evidence for inactivity; if it is unavailable, say so. Keep sale proceeds, price impact, fees and gas unknown when there is no current executable quote.',
|
|
@@ -21,7 +22,7 @@ function scopeLabel(config) {
|
|
|
21
22
|
}
|
|
22
23
|
function renderAgent() {
|
|
23
24
|
const config = agentState?.config;
|
|
24
|
-
$('chat-model-label').textContent = config ? (config.provider === 'codex' ? 'Codex' : 'Claude') + ' · ' + (config.model
|
|
25
|
+
$('chat-model-label').textContent = config ? (config.provider === 'codex' ? 'Codex' : 'Claude') + ' · ' + modelName(config.model, config.provider) : 'Connect a model';
|
|
25
26
|
$('chat-setup').classList.toggle('connected', Boolean(config));
|
|
26
27
|
$('chat-context').textContent = scopeLabel(config);
|
|
27
28
|
$('chat-choose-context').hidden = !config || !state?.wallets.length || !localSession?.controls;
|
|
@@ -44,6 +45,29 @@ function renderAgent() {
|
|
|
44
45
|
scroller.scrollTop=follow?scroller.scrollHeight:previousTop;
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
function modelName(id, provider = agentProvider) { return (provider === modelChoicesProvider ? modelChoices.find(row => row.id === id)?.name : null) || id || 'Default model'; }
|
|
49
|
+
function renderModelChoice() {
|
|
50
|
+
const model = $('agent-model').value.trim();
|
|
51
|
+
$('agent-model-choice').innerHTML = '<option value="">Use default model</option>' + modelChoices.map(row => '<option value="' + escapeHTML(row.id) + '">' + escapeHTML(row.name) + '</option>').join('') + '<option value="__custom__">Custom model ID…</option>';
|
|
52
|
+
$('agent-model-choice').value = !model ? '' : modelChoices.some(row => row.id === model) ? model : '__custom__';
|
|
53
|
+
if ($('agent-model-choice').value === '__custom__') $('agent-model-advanced').open = true;
|
|
54
|
+
if (typeof KiraSelect !== 'undefined') KiraSelect.refresh();
|
|
55
|
+
}
|
|
56
|
+
async function loadModelChoices(recheck = false) {
|
|
57
|
+
const revision = ++modelCatalogRevision, provider = agentProvider;
|
|
58
|
+
modelChoices = []; modelChoicesProvider = provider; renderModelChoice();
|
|
59
|
+
$('agent-model-note').textContent = 'Loading model choices. You can continue with the default model.';
|
|
60
|
+
try {
|
|
61
|
+
const result = await localAPI('/api/agent/models?provider=' + provider + (recheck ? '&recheck=1' : ''));
|
|
62
|
+
if (revision !== modelCatalogRevision || provider !== agentProvider || !$('agent-dialog').open) return;
|
|
63
|
+
modelChoices = (Array.isArray(result.models) ? result.models : []).filter(row => row && typeof row.id === 'string' && typeof row.name === 'string');
|
|
64
|
+
$('agent-model-note').textContent = result.note || 'Check a response to verify access for your account.';
|
|
65
|
+
} catch {
|
|
66
|
+
if (revision !== modelCatalogRevision || !$('agent-dialog').open) return;
|
|
67
|
+
$('agent-model-note').textContent = 'Model choices are unavailable. Use the default model, recheck, or enter an ID under Advanced.';
|
|
68
|
+
}
|
|
69
|
+
renderModelChoice();
|
|
70
|
+
}
|
|
47
71
|
function renderProviders() {
|
|
48
72
|
$('agent-providers').innerHTML = (agentState?.providers || []).map(row => {
|
|
49
73
|
const ready = row.installed && row.supported && row.logged_in;
|
|
@@ -65,9 +89,10 @@ function updateStep() {
|
|
|
65
89
|
const selected = agentState?.providers.find(p => p.id === agentProvider);
|
|
66
90
|
$('agent-next').disabled = agentStep === 1 ? !(selected?.installed && selected.supported && selected.logged_in) : !$('agent-trust').checked || (currentSetup().scope === 'wallet' && !$('agent-wallet').value);
|
|
67
91
|
$('agent-wallet-field').hidden = currentSetup().scope !== 'wallet';
|
|
92
|
+
$('agent-tools').disabled = currentSetup().scope === 'none';
|
|
68
93
|
if (agentStep === 3) {
|
|
69
94
|
const config = currentSetup();
|
|
70
|
-
$('agent-review').innerHTML = '<dt>Account</dt><dd>' + escapeHTML(selected?.name || '') + '</dd><dt>Model</dt><dd>' + escapeHTML(config.model
|
|
95
|
+
$('agent-review').innerHTML = '<dt>Account</dt><dd>' + escapeHTML(selected?.name || '') + '</dd><dt>Model</dt><dd>' + escapeHTML(modelName(config.model)) + '</dd><dt>Context</dt><dd>' + escapeHTML(scopeLabel(config)) + '</dd><dt>History</dt><dd>' + (config.retain_history ? 'Saved on this computer' : 'Memory only') + '</dd>';
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
async function loadAgent(recheck = false) {
|
|
@@ -98,36 +123,47 @@ async function openAgent() {
|
|
|
98
123
|
if (state?.demo) { toast('The sample workspace does not contact model services. Run kira setup for your personal workspace.'); return; }
|
|
99
124
|
if (!localSession?.controls) { toast('Start your local workspace with kira setup to enable connection settings.'); return; }
|
|
100
125
|
$('agent-error').textContent = ''; $('agent-providers').innerHTML = '<p>Checking installed accounts…</p>';
|
|
101
|
-
|
|
126
|
+
const opening = ++modelOpenRevision;
|
|
127
|
+
$('agent-dialog').showModal(); agentStep = 1; invalidateSetup(); updateStep();
|
|
128
|
+
$('agent-next').disabled = true; $('agent-model').disabled = true; $('agent-model-choice').disabled = true;
|
|
129
|
+
if (typeof KiraSelect !== 'undefined') KiraSelect.refresh();
|
|
102
130
|
try {
|
|
103
131
|
await loadAgent();
|
|
132
|
+
if (opening !== modelOpenRevision || !$('agent-dialog').open) return;
|
|
104
133
|
const config = agentState.config;
|
|
105
134
|
agentProvider = config?.provider || agentState.providers.find(p => p.installed && p.supported && p.logged_in)?.id || 'codex';
|
|
106
135
|
$('agent-model').value = config?.model || '';
|
|
107
136
|
document.querySelector('[name="agent-scope"][value="' + (config?.scope || 'portfolio') + '"]').checked = true;
|
|
108
137
|
$('agent-wallet').innerHTML = '<option value="">Choose a wallet</option>' + (state?.wallets || []).map(w => `<option value="${escapeHTML(w.key)}">${escapeHTML(w.name)}</option>`).join('');
|
|
109
|
-
$('agent-wallet').value = config?.wallet || ''; $('agent-retain').checked = config
|
|
110
|
-
renderProviders();
|
|
111
|
-
} catch (error) { $('agent-error').textContent = error.message; }
|
|
138
|
+
$('agent-wallet').value = config?.wallet || ''; $('agent-retain').checked = config ? config.retain_history === true : true; $('agent-trust').checked = config?.trust_native_cli || false; $('agent-tools').checked = config ? config.wallet_tools === true : true;
|
|
139
|
+
renderProviders(); loadModelChoices();
|
|
140
|
+
} catch (error) { if (opening === modelOpenRevision) $('agent-error').textContent = error.message; }
|
|
141
|
+
finally { if (opening === modelOpenRevision) { $('agent-model').disabled = false; $('agent-model-choice').disabled = false; if (typeof KiraSelect !== 'undefined') KiraSelect.refresh(); } }
|
|
112
142
|
}
|
|
113
143
|
$('chat-setup').addEventListener('click', openAgent);
|
|
114
144
|
$('chat-choose-context').addEventListener('click', openAgent);
|
|
115
|
-
$('settings-agent').addEventListener('click', () => { $('settings-dialog').close(); openAgent(); });
|
|
145
|
+
$('settings-agent').addEventListener('click', () => { $('workspace-settings-dialog').close(); openAgent(); });
|
|
116
146
|
$('agent-providers').addEventListener('click', event => {
|
|
117
147
|
const button = event.target.closest('[data-provider]'); if (!button || setupTesting) return;
|
|
118
|
-
if (agentProvider !== button.dataset.provider) { agentProvider = button.dataset.provider; $('agent-model').value = ''; invalidateSetup(); renderProviders(); }
|
|
148
|
+
if (agentProvider !== button.dataset.provider) { agentProvider = button.dataset.provider; $('agent-model').value = ''; invalidateSetup(); renderProviders(); loadModelChoices(); }
|
|
119
149
|
});
|
|
120
150
|
$('agent-cli-help').addEventListener('click', async event => {
|
|
121
151
|
const button = event.target.closest('[data-copy-cli]'); if (!button) return;
|
|
122
152
|
try { await navigator.clipboard.writeText(button.dataset.copyCli); button.textContent = 'Copied'; } catch { toast('Select and copy the command in your terminal.'); }
|
|
123
153
|
});
|
|
154
|
+
$('agent-model-choice').addEventListener('change', () => {
|
|
155
|
+
const value = $('agent-model-choice').value;
|
|
156
|
+
if (value === '__custom__') { $('agent-model-advanced').open = true; $('agent-model').focus(); }
|
|
157
|
+
else { $('agent-model').value = value; invalidateSetup(); updateStep(); }
|
|
158
|
+
});
|
|
159
|
+
$('agent-model').addEventListener('input', renderModelChoice);
|
|
124
160
|
for (const id of ['agent-model', 'agent-wallet', 'agent-retain', 'agent-trust', 'agent-tools']) $(id).addEventListener('input', () => { invalidateSetup(); updateStep(); });
|
|
125
161
|
document.querySelectorAll('[name="agent-scope"]').forEach(input => input.addEventListener('change', () => { invalidateSetup(); updateStep(); }));
|
|
126
162
|
$('agent-next').addEventListener('click', () => { if (!$('agent-next').disabled) { agentStep++; updateStep(); $('agent-dialog').scrollTop = 0; } });
|
|
127
163
|
$('agent-back').addEventListener('click', () => { agentStep--; updateStep(); });
|
|
128
164
|
$('agent-recheck').addEventListener('click', async () => {
|
|
129
165
|
$('agent-recheck').disabled = true; $('agent-error').textContent = '';
|
|
130
|
-
try { await loadAgent(true); invalidateSetup(); renderProviders(); } catch (error) { $('agent-error').textContent = error.message; }
|
|
166
|
+
try { await loadAgent(true); invalidateSetup(); renderProviders(); loadModelChoices(true); } catch (error) { $('agent-error').textContent = error.message; }
|
|
131
167
|
finally { $('agent-recheck').disabled = false; }
|
|
132
168
|
});
|
|
133
169
|
async function waitTurn(id, valid) {
|
|
@@ -154,7 +190,12 @@ $('agent-test').addEventListener('click', async () => {
|
|
|
154
190
|
finally { setupTesting = false; setupTurn = null; $('agent-test').disabled = false; $('agent-back').disabled = false; }
|
|
155
191
|
});
|
|
156
192
|
$('agent-dialog').addEventListener('close', () => {
|
|
157
|
-
|
|
193
|
+
const query = new URLSearchParams(location.search);
|
|
194
|
+
if (query.get('setup') === '1') {
|
|
195
|
+
query.delete('setup');
|
|
196
|
+
history.replaceState(null, '', location.pathname + (query.size ? '?' + query : '') + location.hash);
|
|
197
|
+
}
|
|
198
|
+
modelOpenRevision++; modelCatalogRevision++; invalidateSetup(); if (setupTurn) agentPost('/api/chat/cancel', {id: setupTurn}).catch(() => {});
|
|
158
199
|
});
|
|
159
200
|
$('agent-save').addEventListener('click', async () => {
|
|
160
201
|
if (!agentVerified) return; $('agent-save').disabled = true; $('agent-error').textContent = '';
|
|
@@ -221,9 +262,19 @@ $('chat-new').addEventListener('click', async () => {
|
|
|
221
262
|
catch (error) { $('chat-error').textContent = error.message; }
|
|
222
263
|
});
|
|
223
264
|
async function initialAgent() {
|
|
224
|
-
|
|
225
|
-
|
|
265
|
+
const setupRequested = new URLSearchParams(location.search).get('setup') === '1';
|
|
266
|
+
if (agentLoading || !localSession?.controls || (agentReady && !setupRequested) || (setupRequested && !state)) return; agentLoading = true;
|
|
267
|
+
try {
|
|
268
|
+
await loadAgent();
|
|
269
|
+
if (setupRequested && new URLSearchParams(location.search).get('setup') === '1') {
|
|
270
|
+
history.replaceState(null, '', '/#/home'); readRoute();
|
|
271
|
+
if (!$('agent-dialog').open) await openAgent();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
226
274
|
catch { /* The normal local session polling can reconnect. */ }
|
|
227
275
|
finally { agentLoading = false; }
|
|
228
276
|
}
|
|
229
|
-
renderAgent(); initialAgent(); const agentStartup = setInterval(() => {
|
|
277
|
+
renderAgent(); initialAgent(); const agentStartup = setInterval(() => {
|
|
278
|
+
initialAgent();
|
|
279
|
+
if (agentReady && new URLSearchParams(location.search).get('setup') !== '1') clearInterval(agentStartup);
|
|
280
|
+
}, 1000);
|
package/viewer/static/app.js
CHANGED
|
@@ -153,7 +153,7 @@ function renderDiscoveryCoverage(w) {
|
|
|
153
153
|
$('discovery-notice-text').textContent=notice.message;
|
|
154
154
|
const controls=typeof localSession!=='undefined'&&localSession?.controls===true&&state?.demo!==true;
|
|
155
155
|
$('discovery-settings').hidden=!controls||!(notice.action==='settings'||notice.settings);
|
|
156
|
-
$('discovery-settings').textContent=notice.settingsLabel||'Connect
|
|
156
|
+
$('discovery-settings').textContent=notice.settingsLabel||'Connect data provider';
|
|
157
157
|
$('discovery-refresh').hidden=!controls||notice.action!=='refresh';
|
|
158
158
|
$('discovery-refresh').textContent=notice.refreshLabel||'Refresh holdings';
|
|
159
159
|
$('discovery-refresh').disabled=$('refresh-wallet').disabled;
|
package/viewer/static/index.html
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
<div id="wallet-actions" class="wallet-actions" hidden><button id="refresh-wallet" class="quiet-button" type="button"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M3 11a9 9 0 1 1 2.6 7M3 5v6h6"/></svg>Refresh holdings</button><button id="refresh-prices" class="quiet-button" type="button"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 17l5-5 4 3 7-10M15 5h5v5"/></svg>Refresh prices</button><button id="edit-tags" class="quiet-button" type="button"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 20l4-1L20 7l-3-3L5 16zM14 7l3 3"/></svg>Edit names</button><button id="view-history" class="quiet-button" type="button"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 4h14v16H5zM8 8h8M8 12h8M8 16h5"/></svg>Saved analyses</button></div>
|
|
51
51
|
<section class="wallet-heading"><div><div class="eyebrow">WALLET OVERVIEW</div><h1 id="wallet-name">Your wallets</h1><div class="wallet-address"><span id="address"></span><button id="copy-address" class="icon-button" aria-label="Copy wallet address" title="Copy wallet address"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="8" y="8" width="11" height="12" rx="2"/><path d="M15 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h3"/></svg></button><span id="wallet-tags"></span></div></div><a id="report-link" class="text-link" target="_blank" rel="noreferrer">Research notes <span aria-hidden="true">↗</span></a></section>
|
|
52
52
|
<section class="portfolio-summary" aria-label="Portfolio summary"><div class="valuation"><div class="summary-label">Estimated portfolio value</div><div id="total-value" class="total-value">—</div><div id="valuation-note" class="valuation-note">Loading the latest analysis</div></div><div class="summary-stat"><span class="summary-label">Assets</span><strong id="asset-count">—</strong><span id="unpriced-count" class="stat-note"></span></div><div class="summary-stat"><span class="summary-label">Networks</span><strong id="chain-count">—</strong><span id="coverage-note" class="stat-note"></span></div></section>
|
|
53
|
-
<section id="discovery-notice" class="discovery-notice" aria-labelledby="discovery-notice-title" hidden><strong id="discovery-notice-title"></strong><p id="discovery-notice-text" role="status"></p><div class="discovery-actions"><button id="discovery-refresh" class="quiet-button" type="button" hidden>Refresh holdings</button><button id="discovery-settings" class="quiet-button" type="button" hidden>Connect
|
|
53
|
+
<section id="discovery-notice" class="discovery-notice" aria-labelledby="discovery-notice-title" hidden><strong id="discovery-notice-title"></strong><p id="discovery-notice-text" role="status"></p><div class="discovery-actions"><button id="discovery-refresh" class="quiet-button" type="button" hidden>Refresh holdings</button><button id="discovery-settings" class="quiet-button" type="button" hidden>Connect data provider</button></div><details><summary>Network coverage details</summary><p id="discovery-network-details"></p></details></section>
|
|
54
54
|
<section class="holdings" aria-labelledby="holdings-title"><div class="holdings-toolbar"><div><h2 id="holdings-title">Holdings <span id="visible-count"></span></h2></div><div class="filters"><label class="chain-select"><span class="sr-only">Filter by chain</span><select id="chain-filter"><option value="all">All chains</option></select><svg viewBox="0 0 16 16" aria-hidden="true"><path d="m4 6 4 4 4-4"/></svg></label><div class="segment" role="group" aria-label="Minimum token value"><button data-min="0" aria-pressed="true">All</button><button data-min="5" aria-pressed="false">≥ $5</button><button data-min="10" aria-pressed="false">≥ $10</button></div></div></div>
|
|
55
55
|
<div id="holdings-filter-note" class="holdings-filter-note" role="status" hidden></div><div id="asset-groups"></div>
|
|
56
56
|
<div id="empty" class="empty" hidden><div class="empty-mark" aria-hidden="true">∅</div><h3>No assets in this view</h3><p>Choose another chain or lower the minimum value.</p></div>
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
<form id="wallet-form">
|
|
102
102
|
<div class="dialog-heading"><img src="/kira-logo.png" width="42" height="42" alt=""><h2 id="wallet-dialog-title">Add a Watching wallet</h2><button type="button" data-close-dialog aria-label="Close dialog"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 6 12 12M6 18 18 6"/></svg></button></div>
|
|
103
103
|
<p id="wallet-intro">Follow a public address. Your keys stay in your wallet.</p>
|
|
104
|
-
<div id="wallet-entry-modes" class="wallet-entry-modes" role="group" aria-label="Choose address source"><button type="button" id="wallet-manual-mode" aria-pressed="true">Enter address</button><button type="button" id="wallet-browser-mode" aria-pressed="false">Browser wallet</button><button type="button" id="wallet-ows-mode">Local OWS wallet</button></div>
|
|
104
|
+
<div id="wallet-entry-modes" class="wallet-entry-modes" role="group" aria-label="Choose address source"><button type="button" id="wallet-manual-mode" class="quiet-button" aria-pressed="true">Enter address</button><button type="button" id="wallet-browser-mode" class="quiet-button" aria-pressed="false">Browser wallet</button><button type="button" id="wallet-ows-mode" class="quiet-button">Local OWS wallet</button></div>
|
|
105
105
|
<p id="wallet-demo-note" class="wallet-demo-note" hidden>Sample mode: explore this form with a public address. Connecting and research are available in a personal workspace.</p>
|
|
106
106
|
<section id="wallet-browser-panel" class="wallet-browser-panel" aria-label="Browser wallet connection" hidden>
|
|
107
107
|
<h3 id="wallet-providers-title">Choose a browser wallet</h3>
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
<label>Wallet name<input id="new-wallet-tag" required maxlength="200" placeholder="My research wallet" autocomplete="off"></label>
|
|
122
122
|
<section id="wallet-preview" class="wallet-preview" aria-label="Wallet registration preview" hidden><h3>Ready to watch</h3><dl><dt>Public address</dt><dd id="wallet-preview-address"></dd><dt>Exact wallet name</dt><dd id="wallet-preview-name"></dd></dl></section>
|
|
123
123
|
<p id="wallet-existing" class="wallet-existing" hidden>This Watching wallet is already saved. <a id="wallet-existing-link">Open the saved wallet</a> to keep its current names.</p>
|
|
124
|
-
<section id="wallet-discovery-warning" class="discovery-notice" aria-label="Token discovery connection"><strong id="wallet-discovery-warning-title">Checking token discovery</strong><p id="wallet-discovery-warning-text">An indexer is needed to find other ERC20 holdings. Public RPC alone checks native balances and Mint Club assets.</p><button id="wallet-discovery-settings" class="quiet-button" type="button">Connect
|
|
124
|
+
<section id="wallet-discovery-warning" class="discovery-notice" aria-label="Token discovery connection"><strong id="wallet-discovery-warning-title">Checking token discovery</strong><p id="wallet-discovery-warning-text">An indexer is needed to find other ERC20 holdings. Public RPC alone checks native balances and Mint Club assets.</p><button id="wallet-discovery-settings" class="quiet-button" type="button">Connect data provider</button></section>
|
|
125
125
|
<p class="form-note">Research can consume provider allowance. No signature is requested.</p>
|
|
126
126
|
<button id="wallet-register" class="primary-button" type="submit" disabled>Add and research</button>
|
|
127
127
|
</div>
|
|
@@ -141,13 +141,36 @@
|
|
|
141
141
|
<p id="ows-error" class="form-error" role="alert"></p>
|
|
142
142
|
</dialog>
|
|
143
143
|
<dialog id="tags-dialog" class="research-dialog" aria-labelledby="tags-title"><form id="tags-form"><div class="dialog-heading"><h2 id="tags-title">Wallet names</h2><button type="button" data-close-dialog aria-label="Close dialog"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 6 12 12M6 18 18 6"/></svg></button></div><label>One exact name per line<textarea id="tag-values" required rows="4"></textarea></label><p class="form-note">The first name is shown in navigation. Saved analyses retain their original names.</p><p id="tags-error" class="form-error" role="alert"></p><button class="primary-button" type="submit">Save names</button></form></dialog>
|
|
144
|
-
<dialog id="settings-dialog" class="research-dialog" aria-labelledby="
|
|
144
|
+
<dialog id="workspace-settings-dialog" class="research-dialog" aria-labelledby="workspace-settings-title">
|
|
145
|
+
<div class="dialog-heading"><h2 id="workspace-settings-title">Workspace settings</h2><button type="button" data-close-dialog aria-label="Close settings">×</button></div>
|
|
146
|
+
<div class="settings-section"><h3>Model and permissions</h3><p id="workspace-model-summary" class="form-note"></p><button id="settings-agent" class="quiet-button" type="button">Change model and permissions</button></div>
|
|
147
|
+
<div class="settings-section"><h3>Data provider connection</h3><p id="workspace-data-summary" class="form-note"></p><button id="settings-data" class="quiet-button" type="button">Manage data connection</button></div>
|
|
148
|
+
</dialog>
|
|
149
|
+
<dialog id="settings-dialog" class="research-dialog" aria-labelledby="settings-title">
|
|
150
|
+
<div class="dialog-heading"><h2 id="settings-title">Data provider connection</h2><button type="button" data-close-dialog aria-label="Close connection settings">×</button></div>
|
|
151
|
+
<form id="provider-form">
|
|
152
|
+
<label>Connection<select id="data-provider"><option value="public">Basic · public connection</option><option value="alchemy">Alchemy · broader token coverage</option></select></label>
|
|
153
|
+
<p id="provider-description" class="form-note"></p><p id="provider-key-status" class="form-note" role="status"></p>
|
|
154
|
+
<div id="provider-key-guide" class="connection-guide" hidden><strong>Connect your Alchemy account locally</strong><p>Create a key in the <a href="https://dashboard.alchemy.com/" target="_blank" rel="noreferrer">Alchemy dashboard</a>. Put it in a private environment file, then run <code>kira config import-env --file /path/to/private.env</code> in your terminal. Reopen these settings after setup. Keep the key outside this browser.</p></div>
|
|
155
|
+
<button id="provider-save" class="primary-button" type="submit">Save connection</button>
|
|
156
|
+
</form>
|
|
157
|
+
<p id="provider-status" class="form-note" role="status"></p><p id="settings-error" class="form-error" role="alert"></p>
|
|
158
|
+
<details id="connection-advanced"><summary>Advanced connection settings</summary>
|
|
159
|
+
<label>Alchemy key reference<input id="discovery-key" value="ALCHEMY_API_KEY" pattern="[A-Z_][A-Z0-9_]*" maxlength="100"></label><p class="form-note">Environment variable name only. The connection save above uses this reference.</p>
|
|
160
|
+
<form id="rpc-form"><label>RPC preference<select id="rpc-mode"><option value="public">Public RPC</option><option value="custom">Custom RPC references</option></select></label>
|
|
161
|
+
<div id="rpc-custom-fields" hidden><label>Custom references, one per line<textarea id="rpc-references" rows="3" placeholder="8453 KIRA_BASE_RPC"></textarea></label><p class="form-note">Chain ID and environment variable name. Keep URLs and keys in your private environment file.</p><label class="checkbox-label"><input id="rpc-custom-only" type="checkbox">Require custom RPC only</label></div>
|
|
162
|
+
<p class="form-note">Custom RPC reads known assets. An arbitrary endpoint may not find other ERC20 holdings. Public fallback applies when enabled. RPC preferences do not change token discovery.</p><button class="quiet-button" type="submit">Save RPC preference</button>
|
|
163
|
+
</form>
|
|
164
|
+
</details>
|
|
165
|
+
<p class="form-note">Saving a connection does not register a wallet or start research. Refresh holdings to use it in an existing wallet. Saved analyses keep their original coverage.</p>
|
|
166
|
+
<button id="settings-done" class="quiet-button" type="button">Done</button>
|
|
167
|
+
</dialog>
|
|
145
168
|
<dialog id="history-dialog" class="research-dialog history-dialog" aria-labelledby="history-title"><div class="dialog-heading"><h2 id="history-title">Saved analyses</h2><button type="button" data-close-dialog aria-label="Close dialog"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 6 12 12M6 18 18 6"/></svg></button></div><div id="history-list"></div><form id="compare-form" hidden><label>Earlier analysis<select id="compare-before"></select></label><label>Later analysis<select id="compare-after"></select></label><button class="quiet-button" type="submit">Compare evidence</button></form><div id="comparison-result" class="comparison-result"></div><p id="history-error" class="form-error" role="alert"></p></dialog>
|
|
146
169
|
<dialog id="agent-dialog" class="research-dialog agent-dialog" aria-labelledby="agent-title">
|
|
147
170
|
<div class="dialog-heading"><div><span class="eyebrow">MAKE KIRA YOURS</span><h2 id="agent-title">Connect your account.</h2></div><button id="agent-close" type="button" data-close-dialog aria-label="Close setup"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="m6 6 12 12M6 18 18 6"/></svg></button></div>
|
|
148
171
|
<ol class="setup-progress" aria-label="Setup progress"><li data-step="1" aria-current="step"><span>1</span> Account</li><li data-step="2"><span>2</span> Permissions</li><li data-step="3"><span>3</span> Ready</li></ol>
|
|
149
|
-
<section id="agent-step-1"><p class="setup-intro">Bring the model account you already use. Kira connects your installed CLI, with its existing login and subscription limits.</p><div id="agent-providers" class="provider-grid" role="group" aria-label="Choose your CLI account"></div><div id="agent-cli-help" class="cli-help"></div><button id="agent-recheck" class="text-button" type="button">Recheck installed CLIs</button><label class="model-field">Model
|
|
150
|
-
<section id="agent-step-2" hidden><h3>What can Kira send?</h3><p class="setup-intro">Your portfolio stays on this computer. When you send a message, its text and the context below go through your CLI to its model service.</p><fieldset class="scope-choices"><legend class="sr-only">Automatic context</legend><label><input type="radio" name="agent-scope" value="none"><span><strong>No wallet context</strong><small>
|
|
172
|
+
<section id="agent-step-1"><p class="setup-intro">Bring the model account you already use. Kira connects your installed CLI, with its existing login and subscription limits.</p><div id="agent-providers" class="provider-grid" role="group" aria-label="Choose your CLI account"></div><div id="agent-cli-help" class="cli-help"></div><button id="agent-recheck" class="text-button" type="button">Recheck installed CLIs</button><label class="model-field">Model<select id="agent-model-choice"><option value="">Use default model</option></select></label><p id="agent-model-note" class="form-note" role="status">Use the default model to get started. A response check verifies account access.</p><details id="agent-model-advanced"><summary>Advanced model choice</summary><label>Custom model ID<input id="agent-model" maxlength="100" autocomplete="off" placeholder="Optional model identifier"></label><p class="form-note">Use an identifier supported by your installed CLI. Leave blank for its default model.</p></details></section>
|
|
173
|
+
<section id="agent-step-2" hidden><h3>What can Kira send?</h3><p class="setup-intro">Your portfolio stays on this computer. When you send a message, its text and the context below go through your CLI to its model service.</p><fieldset class="scope-choices"><legend class="sr-only">Automatic context</legend><label><input type="radio" name="agent-scope" value="none"><span><strong>No wallet context</strong><small>General chat and manual wallet management stay available. Wallet AI context and research are off. Change this later in Workspace settings.</small></span></label><label><input type="radio" name="agent-scope" value="wallet"><span><strong>One wallet</strong><small>Recorded holdings, addresses, names, timestamps and coverage for one wallet.</small></span></label><label><input type="radio" name="agent-scope" value="portfolio" checked><span><strong>Whole portfolio</strong><small>The same recorded facts for every registered wallet.</small></span></label></fieldset><label id="agent-wallet-field" hidden>Approved wallet<select id="agent-wallet"></select></label><label class="permission-choice"><input id="agent-retain" type="checkbox" checked><span><strong>Save chat history on this computer (recommended)</strong><small>Reopen your chats after restarting Kira. Off keeps new chats only until Kira stops. Saving does not expand model memory or wallet access. Provider and native CLI data policies remain separate.</small></span></label><label class="permission-choice"><input id="agent-tools" type="checkbox" checked><span><strong>Let Kira research my wallets</strong><small>Read saved analyses and start holdings or price updates for the wallets selected above. Jobs appear in Activity. Requires wallet context above. You can change this later in Workspace settings. Your keys stay outside the agent.</small></span></label><label class="permission-choice native-trust"><input id="agent-trust" type="checkbox"><span><strong>I trust my installed CLI and its account policy</strong><small>Kira limits the context it supplies and uses scoped wallet research tools. Native CLI and administrator policy remain trusted components. No passwords or API keys are entered here.</small></span></label><p class="form-note">Changing any model or permission setting starts a fresh conversation. You can change all these choices in Workspace settings. Research tools can refresh approved wallets. Chat cannot sign or trade.</p></section>
|
|
151
174
|
<section id="agent-step-3" hidden><div class="ready-art"><img src="/kira-logo.png" width="64" height="64" alt=""></div><h3>One last hello.</h3><p class="setup-intro">Check a real response before you start. This check sends a generic greeting with no wallet data.</p><dl id="agent-review" class="agent-review"></dl><button id="agent-test" class="quiet-button" type="button">Check connection</button><p id="agent-test-status" role="status"></p><p class="form-note">Messages use your chosen CLI account. Its subscription limits and data policies apply.</p></section>
|
|
152
175
|
<p id="agent-error" class="form-error" role="alert"></p><div class="setup-footer"><button id="agent-back" class="quiet-button" type="button" hidden>Back</button><button id="agent-next" class="primary-button" type="button">Continue</button><button id="agent-save" class="primary-button" type="button" hidden disabled>Start talking with Kira</button></div>
|
|
153
176
|
</dialog>
|
package/viewer/static/jobs.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
let localSession=null, jobList=[], jobsSignature='', localPolling=false;
|
|
3
3
|
let discoverySettingsRevision='';
|
|
4
4
|
const pendingActions=new Set();
|
|
5
|
-
const jobLabels={'wallet.add':'Wallet research','wallet.refresh':'Holdings refresh','prices.refresh':'Price refresh','wallet.setTags':'Wallet names','settings.rpc':'RPC settings','settings.discovery':'Token discovery'};
|
|
5
|
+
const jobLabels={'wallet.add':'Wallet research','wallet.refresh':'Holdings refresh','prices.refresh':'Price refresh','wallet.setTags':'Wallet names','settings.rpc':'RPC settings','settings.discovery':'Token discovery','settings.provider':'Data provider connection'};
|
|
6
6
|
const stageLabels={queued:'Waiting to start',starting:'Opening saved evidence',registered:'Wallet registered',discovery:'Token discovery',chain:'Checking on-chain holdings',dex_discovery:'Checking markets',token_images:'Preparing token artwork',published:'Evidence saved',failed:'Research stopped',stopped:'Research stopped',interrupted:'Ready for recovery'};
|
|
7
7
|
async function localAPI(path,options={}){
|
|
8
8
|
const response=await fetch(path,{cache:'no-store',...options,headers:{'X-Kira-Session':localSession?.token||'',...options.headers},signal:options.signal||AbortSignal.timeout(12000)});
|
|
@@ -16,7 +16,7 @@ async function submitOperation(operation,input){
|
|
|
16
16
|
const key=sessionStorage.getItem(storageKey)||crypto.randomUUID();sessionStorage.setItem(storageKey,key);
|
|
17
17
|
try{
|
|
18
18
|
const result=await localAPI('/api/operations',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({schema_version:1,operation,input,idempotency_key:key})});
|
|
19
|
-
sessionStorage.removeItem(storageKey);toast('Saved '+(jobLabels[operation]||'job').toLowerCase());await pollJobs();return result;
|
|
19
|
+
sessionStorage.removeItem(storageKey);toast((operation.startsWith('settings.')?'Queued ':'Saved ')+(jobLabels[operation]||'job').toLowerCase());await pollJobs();return result;
|
|
20
20
|
}finally{pendingActions.delete(fingerprint);}
|
|
21
21
|
}
|
|
22
22
|
function refreshControlState(){
|
|
@@ -76,7 +76,7 @@ async function pollJobs(){
|
|
|
76
76
|
if(!localSession){const response=await fetch('/api/session',{cache:'no-store',signal:AbortSignal.timeout(5000)});if(!response.ok)return;localSession=await response.json();}
|
|
77
77
|
refreshControlState();if(!localSession.controls){renderJobs();return;}
|
|
78
78
|
const jobs=await localAPI('/api/jobs');jobList=jobs;const reconnected=!jobsConnected;jobsConnected=true;lastJobsPoll=new Date().toISOString();
|
|
79
|
-
const setting=jobs.filter(j=>j.operation
|
|
79
|
+
const setting=jobs.filter(j=>j.operation.startsWith('settings.')&&j.state==='succeeded').map(j=>j.job_id+':'+j.updated_at).sort().join(',');
|
|
80
80
|
if(setting!==discoverySettingsRevision){discoverySettingsRevision=setting;if(typeof loadSetupReadiness==='function')await loadSetupReadiness();}
|
|
81
81
|
const signature=JSON.stringify(jobs)+JSON.stringify(jobs.map(j=>KiraView.jobPresentation(j).quiet));if(signature!==jobsSignature||reconnected){jobsSignature=signature;renderJobs();}else{$('jobs-freshness').textContent='Checked '+stamp(lastJobsPoll);}
|
|
82
82
|
refreshControlState();
|
|
@@ -108,12 +108,109 @@ $('refresh-prices').addEventListener('click',async()=>{try{await submitOperation
|
|
|
108
108
|
$('edit-tags').addEventListener('click',()=>{if(!currentWallet())return;$('tags-dialog').dataset.wallet=selectedWallet;$('tag-values').value=currentWallet().tags.join('\n');$('tags-error').textContent='';$('tags-dialog').showModal();});
|
|
109
109
|
formAction($('tags-form'),$('tags-error'),async()=>{await submitOperation('wallet.setTags',{wallet:$('tags-dialog').dataset.wallet,tags:$('tag-values').value.split('\n').filter(t=>t.length)});$('tags-dialog').close();});
|
|
110
110
|
$('jobs-list').addEventListener('click',async event=>{const button=event.target.closest('[data-job-action]');if(!button)return;button.disabled=true;try{await submitOperation('job.'+button.dataset.jobAction,{job_id:button.dataset.jobId});}catch(error){button.disabled=false;toast(error.message);}});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
let connectionConfig = null, connectionReadiness = null, connectionRevision = 0, workspaceSettingsRevision = 0, pendingSetting = null, settingSaving = false, settingSaved = false, settingFailed = false;
|
|
112
|
+
function connectionSummary(config) {
|
|
113
|
+
return config.discovery.provider === 'alchemy' ? 'Alchemy selected. Coverage is confirmed by holdings research.' : config.rpc.mode === 'custom' ? 'Custom RPC selected. Broader token discovery is off.' : 'Basic public connection. Broader token discovery is off.';
|
|
114
|
+
}
|
|
115
|
+
function renderConnectionForm() {
|
|
116
|
+
const alchemy = $('data-provider').value === 'alchemy';
|
|
117
|
+
const available = connectionReadiness?.local_key_available === true && $('discovery-key').value === connectionConfig?.discovery.key_env;
|
|
118
|
+
$('provider-description').textContent = alchemy ? 'One connection enables Alchemy RPC reads and broader ERC20 discovery on supported networks. Custom chain overrides stay in place.' : 'Use public RPC for native and known assets. Other ERC20 holdings may be missing.';
|
|
119
|
+
$('provider-key-status').textContent = !alchemy ? '' : available ? 'Local key found. Research results will confirm access and chain coverage.' : 'A local Alchemy key is required before this connection can be saved.';
|
|
120
|
+
$('provider-key-guide').hidden = !alchemy || available;
|
|
121
|
+
$('rpc-custom-fields').hidden = $('rpc-mode').value !== 'custom';
|
|
122
|
+
if (typeof KiraSelect !== 'undefined') KiraSelect.refresh();
|
|
123
|
+
}
|
|
124
|
+
async function openWorkspaceSettings() {
|
|
125
|
+
if (!localSession?.controls) return;
|
|
126
|
+
const revision = ++workspaceSettingsRevision;
|
|
127
|
+
if (!$('workspace-settings-dialog').open) $('workspace-settings-dialog').showModal();
|
|
128
|
+
$('workspace-model-summary').textContent = 'Checking saved model and permissions…';
|
|
129
|
+
$('workspace-data-summary').textContent = 'Checking the saved connection…';
|
|
130
|
+
const [modelResult, dataResult] = await Promise.allSettled([localAPI('/api/agent'), localAPI('/api/settings')]);
|
|
131
|
+
if (revision !== workspaceSettingsRevision || !$('workspace-settings-dialog').open) return;
|
|
132
|
+
if (modelResult.status === 'fulfilled') {
|
|
133
|
+
const config = modelResult.value.config;
|
|
134
|
+
$('workspace-model-summary').textContent = config ? (config.provider === 'codex' ? 'Codex' : 'Claude') + ' · ' + (config.model || 'Default model') + ' · ' + (config.scope === 'none' ? 'No wallet context' : config.scope === 'wallet' ? 'One wallet' : 'Whole portfolio') + ' · History ' + (config.retain_history ? 'saved locally' : 'off') + ' · Research ' + (config.wallet_tools && config.scope !== 'none' ? 'on' : 'off') : 'Connect Codex or Claude. Choose wallet context, local history and research access.';
|
|
135
|
+
} else $('workspace-model-summary').textContent = 'Could not check saved model settings. Reopen to retry.';
|
|
136
|
+
$('workspace-data-summary').textContent = dataResult.status === 'fulfilled' ? connectionSummary(dataResult.value) : 'Could not check the data connection. Reopen to retry.';
|
|
137
|
+
|
|
138
|
+
}
|
|
139
|
+
async function openDataConnections(advanced = false) {
|
|
140
|
+
if (!localSession?.controls) return;
|
|
141
|
+
const revision = ++connectionRevision;
|
|
142
|
+
$('settings-error').textContent = ''; $('provider-status').textContent = pendingSetting ? 'A saved request is pending. Save again to check its result.' : '';
|
|
143
|
+
$('provider-save').disabled = true; $('settings-dialog').showModal();
|
|
144
|
+
$('settings-done').textContent = $('wallet-dialog').open ? 'Back to wallet' : 'Done';
|
|
145
|
+
$('connection-advanced').open = advanced;
|
|
146
|
+
try {
|
|
147
|
+
const [config, readiness] = await Promise.all([localAPI('/api/settings'), localAPI('/api/onboarding')]);
|
|
148
|
+
if (revision !== connectionRevision || !$('settings-dialog').open) return;
|
|
149
|
+
connectionConfig = config; connectionReadiness = readiness;
|
|
150
|
+
$('data-provider').value = config.discovery.provider === 'alchemy' ? 'alchemy' : 'public';
|
|
151
|
+
$('rpc-mode').value = config.rpc.mode; $('rpc-custom-only').checked = !config.rpc.allow_public_fallback;
|
|
152
|
+
$('rpc-references').value = Object.entries(config.rpc.chains).map(([chain,row]) => chain+' '+row.url_env).join('\n');
|
|
153
|
+
$('discovery-key').value = config.discovery.key_env;
|
|
154
|
+
$('provider-status').textContent = pendingSetting ? 'A saved request is pending. Save again to check its result.' : connectionSummary(config);
|
|
155
|
+
renderConnectionForm(); $('provider-save').disabled = false;
|
|
156
|
+
} catch (error) { if (revision === connectionRevision) $('settings-error').textContent = error.message; }
|
|
157
|
+
}
|
|
158
|
+
async function saveConnectionSetting(operation, input) {
|
|
159
|
+
if (settingSaving) throw new Error('A settings request is already being checked. Wait for its result.');
|
|
160
|
+
settingSaving = true; settingSaved = false; settingFailed = false;
|
|
161
|
+
try {
|
|
162
|
+
await finishConnectionSetting(operation,input);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
$('provider-status').textContent = settingSaved ? 'Connection saved. Status could not be refreshed; retry checks the same request.' : pendingSetting ? 'The saved request is pending. Retry checks the same request.' : settingFailed ? 'Connection settings were not saved.' : 'Could not confirm the settings request. Check Activity or retry the same choices.';
|
|
165
|
+
throw settingSaved ? new Error('Settings were saved. Reopen or retry to refresh the connection status.') : error;
|
|
166
|
+
}
|
|
167
|
+
finally { settingSaving = false; }
|
|
168
|
+
}
|
|
169
|
+
async function finishConnectionSetting(operation, input) {
|
|
170
|
+
const signature = JSON.stringify({operation,input});
|
|
171
|
+
if (!pendingSetting) {
|
|
172
|
+
const accepted = await submitOperation(operation, input);
|
|
173
|
+
pendingSetting = {id:accepted.job_id,signature};
|
|
174
|
+
}
|
|
175
|
+
const pending = pendingSetting;
|
|
176
|
+
$('provider-status').textContent = 'Connection settings are queued. Waiting for the saved result…';
|
|
177
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
178
|
+
const result = await localAPI('/api/jobs/' + encodeURIComponent(pending.id));
|
|
179
|
+
if (['succeeded','failed','cancelled','interrupted','partial'].includes(result.state)) {
|
|
180
|
+
if (result.state !== 'succeeded') { settingFailed = true; if (pendingSetting === pending) pendingSetting = null; throw new Error(result.errors?.[0]?.message || 'Connection settings were not saved. Review Activity and retry.'); }
|
|
181
|
+
if (pending.signature !== signature) { if (pendingSetting === pending) pendingSetting = null; throw new Error('The previous settings request finished. Review your new choices and save again.'); }
|
|
182
|
+
settingSaved = true;
|
|
183
|
+
$('provider-status').textContent = 'Connection saved. Refresh holdings to check the new coverage.';
|
|
184
|
+
if (typeof loadSetupReadiness === 'function') await loadSetupReadiness();
|
|
185
|
+
connectionConfig = await localAPI('/api/settings'); connectionReadiness = await localAPI('/api/onboarding');
|
|
186
|
+
renderConnectionForm(); if (pendingSetting === pending) pendingSetting = null; return;
|
|
187
|
+
}
|
|
188
|
+
await new Promise(resolve => setTimeout(resolve, 400));
|
|
189
|
+
}
|
|
190
|
+
throw new Error('Settings are still queued. Check Activity or save again to check the same request.');
|
|
191
|
+
}
|
|
192
|
+
$('open-settings').addEventListener('click',openWorkspaceSettings);
|
|
193
|
+
$('settings-data').addEventListener('click',()=>openDataConnections());
|
|
194
|
+
$('discovery-settings').addEventListener('click',()=>openDataConnections($('discovery-settings').textContent === 'Review RPC settings'));
|
|
195
|
+
$('wallet-discovery-settings').addEventListener('click',()=>openDataConnections());
|
|
196
|
+
$('settings-done').addEventListener('click',()=>$('settings-dialog').close());
|
|
197
|
+
$('workspace-settings-dialog').addEventListener('close',()=>{workspaceSettingsRevision++;});
|
|
198
|
+
$('settings-dialog').addEventListener('close',()=>{connectionRevision++; if ($('workspace-settings-dialog').open) openWorkspaceSettings();});
|
|
199
|
+
$('data-provider').addEventListener('change',renderConnectionForm);
|
|
200
|
+
$('rpc-mode').addEventListener('change',renderConnectionForm);
|
|
201
|
+
$('discovery-key').addEventListener('input',renderConnectionForm);
|
|
114
202
|
$('discovery-refresh').addEventListener('click',()=>$('refresh-wallet').click());
|
|
115
|
-
formAction($('
|
|
116
|
-
|
|
203
|
+
formAction($('provider-form'),$('settings-error'),async()=>{
|
|
204
|
+
const input = {provider:$('data-provider').value,key_env:$('discovery-key').value.trim()};
|
|
205
|
+
$('data-provider').disabled = true; $('discovery-key').disabled = true;
|
|
206
|
+
try { await saveConnectionSetting('settings.provider',input); }
|
|
207
|
+
finally { $('data-provider').disabled = false; $('discovery-key').disabled = false; if (typeof KiraSelect !== 'undefined') KiraSelect.refresh(); }
|
|
208
|
+
});
|
|
209
|
+
formAction($('rpc-form'),$('settings-error'),async()=>{
|
|
210
|
+
const lines=$('rpc-references').value.split('\n').filter(s=>s.trim());
|
|
211
|
+
const chains=lines.map(line=>{const parts=line.trim().split(/\s+/);if(parts.length!==2)throw new Error('Enter a chain ID and environment variable name on each line.');return {chain_id:Number(parts[0]),url_env:parts[1]};});
|
|
212
|
+
await saveConnectionSetting('settings.rpc',{mode:$('rpc-mode').value,allow_public_fallback:!$('rpc-custom-only').checked,chains});
|
|
213
|
+
});
|
|
117
214
|
$('view-history').addEventListener('click',async()=>{try{const rows=await localAPI('/api/snapshots?wallet='+encodeURIComponent(selectedWallet));$('history-list').innerHTML=rows.map(row=>`<article class="history-row"><strong>${stamp(row.compiled_at)}</strong><span>${escapeHTML(row.status==='completed'?'Saved':'Saved with coverage gaps')}</span><small>${escapeHTML(row.snapshot_id)}</small></article>`).join('')||'<p>No analysis has been published yet.</p>';$('compare-form').hidden=rows.length<2;for(const id of ['compare-before','compare-after'])$(id).innerHTML=rows.map(row=>`<option value="${escapeHTML(row.snapshot_id)}">${stamp(row.compiled_at)} · ${escapeHTML(row.snapshot_id.split('/').at(-1))}</option>`).join('');$('compare-after').selectedIndex=Math.max(0,rows.length-1);$('comparison-result').textContent='';$('history-error').textContent='';$('history-dialog').showModal();}catch(error){toast(error.message);}});
|
|
118
215
|
formAction($('compare-form'),$('history-error'),async()=>{const comparison=await localAPI('/api/compare?before='+encodeURIComponent($('compare-before').value)+'&after='+encodeURIComponent($('compare-after').value));$('comparison-result').innerHTML=`<p>${escapeHTML(comparison.note)}</p><p>Coverage observations ${comparison.coverage_changed?'changed':'unchanged'} · native price references ${comparison.price_references_changed?'changed':'unchanged'}</p><div class="table-wrap"><table><thead><tr><th>Token / chain</th><th>Earlier balance</th><th>Later balance</th><th>Change</th></tr></thead><tbody>${comparison.positions.map(p=>`<tr><td>${escapeHTML(p.symbol||shortAddress(p.address))} · ${p.chain_id}<small>${escapeHTML(p.presence.replaceAll('_',' '))}${p.price_references_changed?' · price references changed':''}${p.valuation_method_changed?' · method changed':''}</small></td><td>${escapeHTML(p.before_balance??'Unknown')}</td><td>${escapeHTML(p.after_balance??'Unknown')}</td><td>${escapeHTML(p.balance_delta??'Unknown')}</td></tr>`).join('')}</tbody></table></div>`;});
|
|
119
216
|
window.addEventListener('hashchange',()=>{refreshControlState();renderJobs();});
|
package/viewer/static/report.js
CHANGED
|
@@ -17,11 +17,11 @@ function renderSetupPath() {
|
|
|
17
17
|
$('home-empty').hidden=wallet||!$('setup-path').hidden;
|
|
18
18
|
$('setup-path').innerHTML='<details '+(!wallet||!discovery?'open':'')+'><summary>'+ (wallet?'Finish your setup':'Set up your first wallet')+'</summary><div class="section-heading"><h2>'+ (wallet?'Finish your setup':'Start with one wallet')+'</h2><span class="section-note">Your keys stay yours</span></div><ol class="setup-checklist">'+
|
|
19
19
|
'<li><span class="setup-check">'+(wallet?'✓':'1')+'</span><div><strong>Add a public wallet</strong><p>'+(wallet?'Saved locally.':'Paste an EVM address or choose a browser wallet. No signature or seed phrase.')+'</p></div><button class="quiet-button" id="setup-wallet" type="button">'+(wallet?'Add another':'Add wallet')+'</button></li>'+
|
|
20
|
-
'<li><span class="setup-check">'+(discovery?'✓':'2')+'</span><div><strong>
|
|
20
|
+
'<li><span class="setup-check">'+(discovery?'✓':'2')+'</span><div><strong>Data provider connection <small>optional</small></strong><p>'+(!setupReadiness?(setupUnavailable?'Could not check the connection. Retrying…':'Checking the data connection…'):discovery?'Local Alchemy key found. Actual chain coverage is confirmed by research results.':configured?'Alchemy selected, but its local key is unavailable. Review the connection guide.':'Public RPC checks known assets. Connect Alchemy for broader ERC20 discovery, or continue with limited coverage.')+'</p></div><button class="quiet-button" id="setup-discovery" type="button">Review connection</button></li>'+
|
|
21
21
|
'<li><span class="setup-check">'+(analysed?'✓':'3')+'</span><div><strong>Read your first report</strong><p>'+(analysed?'Saved balances, prices and coverage are ready.':'Adding a wallet starts one explicit research job. Follow its progress in Activity; gaps are shown in the report.')+'</p></div><a class="quiet-button" href="#/activity">View Activity</a></li>'+
|
|
22
22
|
'<li><span class="setup-check">'+(ai?'✓':'4')+'</span><div><strong>Connect your AI <small>optional</small></strong><p>'+(ai?'Connected. Wallet context still follows your explicit permissions.':'Use your installed Codex or Claude account when you are ready. Tracking works without it.')+'</p></div><button class="quiet-button" id="setup-ai" type="button">'+(ai?'Permissions':'Connect AI')+'</button></li></ol></details>';
|
|
23
23
|
$('setup-wallet').addEventListener('click',openAdd);
|
|
24
|
-
$('setup-discovery').addEventListener('click',()
|
|
24
|
+
$('setup-discovery').addEventListener('click',()=>openDataConnections());
|
|
25
25
|
$('setup-ai').addEventListener('click',openAgent);
|
|
26
26
|
}
|
|
27
27
|
async function loadSetupReadiness(){
|
|
@@ -128,7 +128,7 @@ function renderWatching() {
|
|
|
128
128
|
const discovery=typeof setupReadiness!=='undefined'?setupReadiness:null,ready=discovery?.discovery_key_available===true;
|
|
129
129
|
$('wallet-discovery-warning').hidden=ready||!!submission||state?.demo===true;
|
|
130
130
|
$('wallet-discovery-warning-title').textContent=!discovery?'Token discovery is not verified':discovery.discovery_configured?'Token discovery needs a connection':'Token discovery is off';
|
|
131
|
-
$('wallet-discovery-warning-text').textContent=discovery?.discovery_configured?'The selected
|
|
131
|
+
$('wallet-discovery-warning-text').textContent=discovery?.discovery_configured?'The selected Alchemy key is unavailable. Connect your data provider before researching, or continue with native and Mint Club assets only.':'Other ERC20 holdings may be missing. Connect a data provider before researching, or continue with native and Mint Club assets only.';
|
|
132
132
|
$('wallet-register').textContent = watchingSubmitting ? 'Saving wallet…' : ready||state?.demo?'Add and research':'Add with limited discovery';
|
|
133
133
|
if (browser && $('wallet-dialog').open && watchingLastStatus === 'requesting' && current.status === 'accounts')
|
|
134
134
|
$('wallet-accounts').querySelector('button')?.focus();
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
const connection=missing&&(disabled||noKey);
|
|
32
32
|
return {title:'Some balances could not be checked',
|
|
33
33
|
message:'On-chain checks did not finish on '+rpc.length+(rpc.length===1?' network.':' networks.')+' Missing balances are unknown, so this portfolio is partial. Wait a moment, then retry holdings. If errors continue, review RPC settings or use a different provider.'+(connection?' Token discovery also needs a connection to find other ERC20 holdings.':''),
|
|
34
|
-
action:'refresh',settings:true,rpcIssues:true,refreshLabel:'Retry holdings',settingsLabel:connection?'Review
|
|
34
|
+
action:'refresh',settings:true,rpcIssues:true,refreshLabel:'Retry holdings',settingsLabel:connection?'Review data connection':'Review RPC settings'};
|
|
35
35
|
}
|
|
36
|
-
if(missing&&disabled)return {title:'Token discovery is off',message:'This analysis checks native balances and Mint Club assets only. Other ERC20 holdings may be missing. Connect
|
|
36
|
+
if(missing&&disabled)return {title:'Token discovery is off',message:'This analysis checks native balances and Mint Club assets only. Other ERC20 holdings may be missing. Connect a data provider, then refresh holdings.',action:'settings'};
|
|
37
37
|
if(missing&&noKey)return {title:'Token discovery needs a connection',message:'The selected indexer credential is unavailable. Review its connection, then refresh holdings. This portfolio is partial.',action:'settings'};
|
|
38
38
|
if(missing&&readiness?.discovery_key_available&&chains.some(c=>['disabled','missing_credential'].includes(c.discovery_status)))return {title:'Refresh holdings to discover other tokens',message:'Your discovery connection is now available. This saved analysis predates it and remains partial until you refresh holdings.',action:'refresh'};
|
|
39
39
|
return {title:'Your portfolio is partial',message:'Some token discovery or on-chain checks did not complete. Missing holdings remain unknown. Review the network details below before refreshing holdings.',action:readiness?.discovery_key_available?'refresh':'settings'};
|
|
@@ -287,9 +287,9 @@ main { margin-left: 232px; width: calc(100% - 232px); }
|
|
|
287
287
|
.watching-dialog { width: min(520px, calc(100% - 32px)); }
|
|
288
288
|
.watching-dialog [hidden], #wallet-session-panel[hidden] { display: none; }
|
|
289
289
|
.watching-dialog .text-button { min-height: 44px; padding: 10px 2px; margin-right: 12px; }
|
|
290
|
-
.wallet-entry-modes { display:
|
|
291
|
-
.wallet-entry-modes button {
|
|
292
|
-
.wallet-entry-modes button[aria-pressed=true] { color:
|
|
290
|
+
.wallet-entry-modes { display: flex; flex-wrap: wrap; gap: 9px; margin: 22px 0; }
|
|
291
|
+
.wallet-entry-modes button { flex: 1 1 130px; min-height: 44px; }
|
|
292
|
+
.wallet-entry-modes button[aria-pressed=true] { color: #fff; background: var(--accent, #6b5189); border-color: var(--accent, #6b5189); }
|
|
293
293
|
.watching-dialog.is-review .wallet-entry-modes { margin: 16px 0; }
|
|
294
294
|
.watching-dialog.is-review .wallet-preview { padding: 16px; }
|
|
295
295
|
.watching-dialog h3 { font-size: 14px; font-weight: 600; letter-spacing: -.01em; margin: 0 0 8px; }
|
|
@@ -468,3 +468,11 @@ main { margin-left: 232px; width: calc(100% - 232px); }
|
|
|
468
468
|
.scene-caption small { max-width: 112px; }
|
|
469
469
|
}
|
|
470
470
|
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
|
|
471
|
+
|
|
472
|
+
.settings-section + .settings-section { margin-top:24px; padding-top:20px; border-top:1px solid #e2dce8; }
|
|
473
|
+
.connection-guide { padding:14px; margin:14px 0; border:1px solid #d8cfdf; border-radius:12px; font-size:13px; line-height:1.6; }
|
|
474
|
+
.connection-guide p { margin-bottom:0; }
|
|
475
|
+
.connection-guide code { overflow-wrap:anywhere; }
|
|
476
|
+
#connection-advanced { margin-top:22px; }
|
|
477
|
+
#connection-advanced > label { margin-top:14px; }
|
|
478
|
+
#settings-done { margin-top:8px; }
|