opencode-kiro 0.5.0-beta.4 → 0.5.0-beta.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/README.md +82 -17
- package/dist/chunk-LRAGM3PR.js +103 -0
- package/dist/{credits-box-view-GLFJSV6M.js → credits-box-view-2OGZPIW6.js} +7 -2
- package/dist/{credits-chip-view-YP4RPX26.js → credits-chip-view-M7PLKDN4.js} +3 -3
- package/dist/server.js +105 -21
- package/dist/tui.js +26 -27
- package/package.json +2 -2
- package/dist/chunk-M7OUVABX.js +0 -40
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# opencode-kiro
|
|
2
2
|
|
|
3
|
-
> ⚠️ **Experimental prerelease** — `0.5.0-beta.
|
|
3
|
+
> ⚠️ **Experimental prerelease** — `0.5.0-beta.5` targets the **unreleased OpenCode v2**
|
|
4
4
|
> plugin contract at a pinned snapshot. It does **not** work with OpenCode v1.
|
|
5
5
|
> v1 users: stay on **`opencode-kiro@0.4.0`** (the `main` branch / npm `latest` line,
|
|
6
6
|
> which remains the supported stable release). See [CHANGELOG.md](./CHANGELOG.md) and
|
|
@@ -18,7 +18,7 @@ The plugin supplies:
|
|
|
18
18
|
minimal self-registration fallback when the catalog lacks a `kiro` entry
|
|
19
19
|
- **Provider ownership**: an AISDK hook constructs the provider from
|
|
20
20
|
[`kiro-acp-ai-provider`](https://www.npmjs.com/package/kiro-acp-ai-provider) with the
|
|
21
|
-
right options (`cwd`, `agent`, `trustAllTools`, `mcpTimeout`, `contextWindows`)
|
|
21
|
+
right options (`cwd`, `agent`, `trustAllTools`, `mcpTimeout`, `stall`, `contextWindows`)
|
|
22
22
|
- **TUI credits display**: a live Kiro credits box in the sidebar and a compact
|
|
23
23
|
credits chip in the prompt footer row beside the host cost/context display, both
|
|
24
24
|
styled with the host's active theme tokens — auto-loaded from the same single
|
|
@@ -37,7 +37,7 @@ This prerelease is built and tested against **one pinned OpenCode v2 snapshot**:
|
|
|
37
37
|
|---|---|
|
|
38
38
|
| Tested OpenCode commit (`upstream/v2` head, 2026-08-29) | `8ba434b5973856b2f32b8cd3543e154b25c413e6` |
|
|
39
39
|
| `@opencode-ai/plugin` | `0.0.0-dev-18686` (exact) |
|
|
40
|
-
| Package version | `0.5.0-beta.
|
|
40
|
+
| Package version | `0.5.0-beta.5` |
|
|
41
41
|
|
|
42
42
|
Full pin table and verification steps: [docs/COMPATIBILITY.md](./docs/COMPATIBILITY.md).
|
|
43
43
|
There is no `engines.opencode` constraint — the v2 host has no stable semver yet; the
|
|
@@ -60,14 +60,14 @@ global `~/.config/opencode/opencode.json`:
|
|
|
60
60
|
|
|
61
61
|
```json
|
|
62
62
|
{
|
|
63
|
-
"plugins": ["opencode-kiro@0.5.0-beta.
|
|
63
|
+
"plugins": ["opencode-kiro@0.5.0-beta.5"]
|
|
64
64
|
}
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
> ⚠️ **Always pin the exact version, as above.** The host background-auto-refreshes
|
|
68
68
|
> unpinned npm plugin packages to whatever the registry serves, so a bare `"opencode-kiro"`
|
|
69
69
|
> spec can silently move you off the tested build. Use the exact
|
|
70
|
-
> `opencode-kiro@0.5.0-beta.
|
|
70
|
+
> `opencode-kiro@0.5.0-beta.5` spec.
|
|
71
71
|
|
|
72
72
|
The object form pins the same version and takes the plugin options described in
|
|
73
73
|
[Plugin options](#plugin-options):
|
|
@@ -76,7 +76,7 @@ The object form pins the same version and takes the plugin options described in
|
|
|
76
76
|
{
|
|
77
77
|
"plugins": [
|
|
78
78
|
{
|
|
79
|
-
"package": "opencode-kiro@0.5.0-beta.
|
|
79
|
+
"package": "opencode-kiro@0.5.0-beta.5",
|
|
80
80
|
"options": {}
|
|
81
81
|
}
|
|
82
82
|
]
|
|
@@ -98,16 +98,18 @@ All options are optional; omit the `options` object entirely to get the defaults
|
|
|
98
98
|
| `agent` | string | `"opencode"` | kiro-cli agent name the provider runs under |
|
|
99
99
|
| `mcpTimeout` | number | `45` | MCP tool-call timeout, in **minutes** |
|
|
100
100
|
| `discover` | boolean | `true` | Set `false` to skip the setup-time model discovery kick-off when Kiro is already connected. Discovery triggered by login/credential events still runs. |
|
|
101
|
+
| `stall` | object | `{ "afterMs": 10000, "live": "reasoning" }` | Stall notice for turns with no model output. `afterMs` is the silence threshold in milliseconds (`0` disables the notice entirely); `live` is `"reasoning"` (live notice in the transcript, visible only when thinking is shown) or `"off"` (summary line only). See [Slow responses](#slow-responses). |
|
|
101
102
|
|
|
102
103
|
```json
|
|
103
104
|
{
|
|
104
105
|
"plugins": [
|
|
105
106
|
{
|
|
106
|
-
"package": "opencode-kiro@0.5.0-beta.
|
|
107
|
+
"package": "opencode-kiro@0.5.0-beta.5",
|
|
107
108
|
"options": {
|
|
108
109
|
"agent": "opencode",
|
|
109
110
|
"mcpTimeout": 45,
|
|
110
|
-
"discover": true
|
|
111
|
+
"discover": true,
|
|
112
|
+
"stall": { "afterMs": 10000, "live": "reasoning" }
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
]
|
|
@@ -117,7 +119,10 @@ All options are optional; omit the `options` object entirely to get the defaults
|
|
|
117
119
|
A value of the wrong type falls back to its default; unknown keys are ignored. The
|
|
118
120
|
contract per option: `mcpTimeout` must be a positive number of minutes; zero, negative,
|
|
119
121
|
or non-numeric values fall back to the default (`45`); an empty `agent` string falls
|
|
120
|
-
back to `"opencode"`.
|
|
122
|
+
back to `"opencode"`. `stall` must be an object; both members are optional and each is
|
|
123
|
+
checked on its own - `afterMs` must be a number of milliseconds of `0` or more, `live`
|
|
124
|
+
must be exactly `"off"` or `"reasoning"`. An invalid member is dropped while the valid
|
|
125
|
+
one is kept, and an object with nothing valid left counts as omitted (defaults apply).
|
|
121
126
|
|
|
122
127
|
> ⚠️ **Options work on the npm channel only.** The host passes `options` to plugins it
|
|
123
128
|
> installed from npm (the `"package": "opencode-kiro@<version>"` form above). Local
|
|
@@ -129,6 +134,49 @@ There is **no `cwd` option**. The working directory handed to kiro-cli is derive
|
|
|
129
134
|
automatically from the OpenCode location the plugin is set up for, once per location;
|
|
130
135
|
a user-supplied path could only be less accurate.
|
|
131
136
|
|
|
137
|
+
### Slow responses
|
|
138
|
+
|
|
139
|
+
When the Kiro backend is overloaded, kiro-cli retries the request on its own and the
|
|
140
|
+
turn produces no output for a while - a spinner with nothing behind it. The `stall`
|
|
141
|
+
option controls how the plugin surfaces that wait:
|
|
142
|
+
|
|
143
|
+
- **By default** (`afterMs: 10000`, `live: "reasoning"`): after 10 seconds without
|
|
144
|
+
model output, a short reasoning block appears in the transcript, along the lines of
|
|
145
|
+
*"Kiro: no output for 10s - the model may be overloaded and kiro-cli is retrying."*
|
|
146
|
+
It refreshes every further 10 seconds while the silence lasts and closes with
|
|
147
|
+
*"output resumed after Ns"* once real output arrives, or *"turn ended after Ns
|
|
148
|
+
without further output"* if the turn ends first. The block is separate from the
|
|
149
|
+
model's own reasoning and text; the answer is unaffected. Because the live notice is
|
|
150
|
+
rendered as a reasoning block, the TUI shows it **collapsed by default (click to
|
|
151
|
+
expand)**, and only when the TUI shows thinking (`session.thinking: "show"` in
|
|
152
|
+
`cli.json`); with reasoning hidden the notice is hidden too, while the
|
|
153
|
+
after-the-turn summary line described next still appears.
|
|
154
|
+
- **After the turn**, the credits box in the sidebar and the credits chip in the prompt
|
|
155
|
+
footer add one line, `last turn stalled Ns (Reason)`, for as long as the last
|
|
156
|
+
completed turn is the one that stalled. The parenthesized reason (for example
|
|
157
|
+
`ModelOverloaded`) is taken, best-effort, from the last error kiro-cli wrote to its
|
|
158
|
+
own log during that turn; when no such line is available the summary shows the
|
|
159
|
+
duration alone.
|
|
160
|
+
- **`live: "off"`** hides the live transcript block; the after-the-turn summary line
|
|
161
|
+
still appears.
|
|
162
|
+
- **`afterMs: 0`** disables the feature entirely: no transcript block and no summary.
|
|
163
|
+
Any other value changes the silence threshold (in milliseconds).
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"plugins": [
|
|
168
|
+
{
|
|
169
|
+
"package": "opencode-kiro@0.5.0-beta.5",
|
|
170
|
+
"options": { "stall": { "live": "off" } }
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The notice is informational only; the plugin never cancels or retries the turn itself.
|
|
177
|
+
Requires `kiro-acp-ai-provider` 3.2.0 (see [docs/COMPATIBILITY.md](./docs/COMPATIBILITY.md)
|
|
178
|
+
for the pinned SDK version).
|
|
179
|
+
|
|
132
180
|
### Disabling
|
|
133
181
|
|
|
134
182
|
One `"-kiro"` directive in the same `plugins` array disables **everything**: it removes
|
|
@@ -137,7 +185,7 @@ activates either:
|
|
|
137
185
|
|
|
138
186
|
```json
|
|
139
187
|
{
|
|
140
|
-
"plugins": ["opencode-kiro@0.5.0-beta.
|
|
188
|
+
"plugins": ["opencode-kiro@0.5.0-beta.5", "-kiro"]
|
|
141
189
|
}
|
|
142
190
|
```
|
|
143
191
|
|
|
@@ -159,14 +207,14 @@ npm install && npm run build && npm pack
|
|
|
159
207
|
```
|
|
160
208
|
|
|
161
209
|
```json
|
|
162
|
-
{ "plugins": ["opencode-kiro@file:/absolute/path/to/opencode-kiro-0.5.0-beta.
|
|
210
|
+
{ "plugins": ["opencode-kiro@file:/absolute/path/to/opencode-kiro-0.5.0-beta.5.tgz"] }
|
|
163
211
|
```
|
|
164
212
|
|
|
165
213
|
A bare path or bare `file:` spec is rejected at the tested SHA — the `name@file:` form
|
|
166
214
|
is required. **Caveat (local `file:` installs only)**: the colon in the resulting
|
|
167
215
|
install dirname defeats the host's OpenTUI loader shim, so the TUI surfaces render a
|
|
168
216
|
contained per-slot error notice instead of the credits views (the rest of the TUI keeps
|
|
169
|
-
working). Registry installs (`opencode-kiro@0.5.0-beta.
|
|
217
|
+
working). Registry installs (`opencode-kiro@0.5.0-beta.5`) use colon-free paths and are
|
|
170
218
|
fully green — this affects local tarball validation only.
|
|
171
219
|
|
|
172
220
|
The host resolves entrypoints from the package `exports` (`./server` for the server
|
|
@@ -208,6 +256,14 @@ sets the model's base effort. A discovery failure or duplicate runtime ID leaves
|
|
|
208
256
|
catalog unchanged (fail-open). If the loaded catalog has no `kiro` provider at all, the
|
|
209
257
|
plugin self-registers a minimal fallback entry so discovered models remain usable.
|
|
210
258
|
|
|
259
|
+
Discovery is bounded and self-healing: a probe that gets no answer from kiro-cli within
|
|
260
|
+
60 seconds is treated as failed, and a failed probe is retried while Kiro stays
|
|
261
|
+
connected (after 5, 20, and 60 seconds) before giving up until the next login or
|
|
262
|
+
credential change. Each failure is reported on the server's stderr with an
|
|
263
|
+
`[opencode-kiro]` prefix - visible in `opencode serve` output, or from the TUI with
|
|
264
|
+
`OPENCODE_PRINT_LOGS=1` - so a missing model list is never silent. A login triggers one
|
|
265
|
+
probe per project location, however many credential events the host emits for it.
|
|
266
|
+
|
|
211
267
|
List the resulting models with:
|
|
212
268
|
|
|
213
269
|
```bash
|
|
@@ -279,8 +335,11 @@ double-counted. See [CHANGELOG.md](./CHANGELOG.md) for details.
|
|
|
279
335
|
login while one is pending cancels the earlier attempt.
|
|
280
336
|
- **Model discovery (catalog transform)**: after login (and on later login events) the
|
|
281
337
|
plugin runs `listModels()` outside the transform, then applies the validated capture
|
|
282
|
-
via a catalog transform and reload
|
|
283
|
-
effort variants projected, fail-open on any discovery error.
|
|
338
|
+
via a catalog transform and reload - exact ID matching, catalog metadata preserved,
|
|
339
|
+
effort variants projected, fail-open on any discovery error. Each probe runs under a
|
|
340
|
+
generation token: a logout, a newer probe, or a retry bumps the generation, and a
|
|
341
|
+
result that arrives for an older generation is discarded instead of overwriting the
|
|
342
|
+
catalog.
|
|
284
343
|
- **Provider ownership (AISDK hooks)**: the plugin's `sdk` hook constructs the provider
|
|
285
344
|
from `kiro-acp-ai-provider` with the plugin-supplied settings (identifying itself to
|
|
286
345
|
kiro-cli as `opencode-kiro`) and sets it as the event's SDK, so the Kiro provider is
|
|
@@ -304,6 +363,11 @@ double-counted. See [CHANGELOG.md](./CHANGELOG.md) for details.
|
|
|
304
363
|
metadata; OpenCode persists them key-unwrapped on message part state
|
|
305
364
|
(`part.state.credits`, `part.state.creditsUnit`), and the TUI plugin sums them per
|
|
306
365
|
assistant message (deduped across parts).
|
|
366
|
+
- **Stall status**: when a turn stalled, the SDK attaches `status`
|
|
367
|
+
(`{ stalledMs, hint? }`) to the same provider metadata; the TUI plugin reads it from
|
|
368
|
+
`part.state.status` on the same path as credits and renders the one-line summary
|
|
369
|
+
described in [Slow responses](#slow-responses). The live transcript notice is a
|
|
370
|
+
reasoning fragment emitted by the SDK itself, so it needs no TUI support.
|
|
307
371
|
|
|
308
372
|
## Troubleshooting
|
|
309
373
|
|
|
@@ -313,10 +377,11 @@ double-counted. See [CHANGELOG.md](./CHANGELOG.md) for details.
|
|
|
313
377
|
| Auth times out after ~120s | Complete the browser login faster, or run `kiro-cli login` yourself, then re-run `opencode auth login` (fast path). |
|
|
314
378
|
| No credits line / credits stay 0 | Credits appear after the first **completed** kiro turn; cancelled turns and turns without usage state contribute nothing. Check the plugin is active (no stray `"-kiro"` directive — note that directive residue can persist on a reused data dir). |
|
|
315
379
|
| Credits surfaces never appear | The TUI half auto-loads from the server `plugins` entry via `tui: true` — no separate TUI config exists. If the box/chip are missing, the server plugin itself is not loading (check your `plugins` entry and restart opencode). For local `name@file:` tarball installs, a contained per-slot error notice instead of the credits views is the known colon-path caveat; use a registry install. |
|
|
316
|
-
| `kiro` provider not showing in `opencode models` | Run `opencode auth login` first: models are discovered after auth. If the loaded catalog lacks a `kiro` entry, the plugin self-registers a minimal fallback during discovery. |
|
|
380
|
+
| `kiro` provider not showing in `opencode models` | Run `opencode auth login` first: models are discovered after auth. If the loaded catalog lacks a `kiro` entry, the plugin self-registers a minimal fallback during discovery. If you are logged in and the list (or the effort variants) is still missing, the discovery probe may have failed or timed out: it is retried automatically for a few minutes, and each attempt is reported on the server's stderr with an `[opencode-kiro]` prefix (`opencode serve`, or `OPENCODE_PRINT_LOGS=1` with the TUI). Logging in again starts a fresh probe. |
|
|
381
|
+
| Spinner with no output for a long time | The Kiro backend is likely overloaded and kiro-cli is retrying the request. With the default `stall` option a collapsed reasoning block saying so appears in the transcript after 10 seconds (only when thinking is shown; see [Slow responses](#slow-responses)), and the credits box/chip show `last turn stalled Ns (Reason)` once the turn ends. If no such block appears and the model list or effort variants are also missing, discovery may be the problem instead: watch the server log (`OPENCODE_PRINT_LOGS=1`, or run under `opencode serve`) for `[opencode-kiro]` lines. kiro-cli's own errors are in its log under your temp directory (`kiro-log/kiro-chat.log`). |
|
|
317
382
|
| Path install rejected (`must export id`) | Use the `name@file:<absolute tarball path>` form after `npm run build && npm pack` in your checkout (both entry modules export ids). |
|
|
318
383
|
| Provider visible but runs fail | The provider can be selectable before any credential exists. Run `opencode auth login` first. |
|
|
319
|
-
| Worked yesterday, broken today | This prerelease targets one pinned OpenCode snapshot (see [Compatibility](#compatibility)). If your OpenCode build moved past the tested SHA, the v2 plugin surface may have changed underneath it. The other cause is an **unpinned** `plugins` entry (a bare `"opencode-kiro"`): the host background-auto-refreshes unpinned npm plugin packages, so the plugin itself can move off the tested build without you changing anything — pin the exact `opencode-kiro@0.5.0-beta.
|
|
384
|
+
| Worked yesterday, broken today | This prerelease targets one pinned OpenCode snapshot (see [Compatibility](#compatibility)). If your OpenCode build moved past the tested SHA, the v2 plugin surface may have changed underneath it. The other cause is an **unpinned** `plugins` entry (a bare `"opencode-kiro"`): the host background-auto-refreshes unpinned npm plugin packages, so the plugin itself can move off the tested build without you changing anything — pin the exact `opencode-kiro@0.5.0-beta.5` spec. |
|
|
320
385
|
|
|
321
386
|
## Legacy: v1 / OpenCode v1 users (`0.4.0`)
|
|
322
387
|
|
|
@@ -325,7 +390,7 @@ v1 (`opencode >= 1.16.0`). It uses the v1 contract throughout: singular `plugin`
|
|
|
325
390
|
arrays in `opencode.json` and `tui.json`, the `opencode plugin opencode-kiro`
|
|
326
391
|
installer, and `part.metadata.kiro` credits. Its full documentation is the README at
|
|
327
392
|
the [`v0.4.0` tag](https://github.com/NachoFLizaur/opencode-kiro/tree/v0.4.0)
|
|
328
|
-
(equivalently, `main`). Do not install `0.5.0-beta.
|
|
393
|
+
(equivalently, `main`). Do not install `0.5.0-beta.5` into an OpenCode v1 setup.
|
|
329
394
|
|
|
330
395
|
## Development
|
|
331
396
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/tui/credits.ts
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null;
|
|
4
|
+
}
|
|
5
|
+
function readCreditState(state) {
|
|
6
|
+
if (!isRecord(state)) return void 0;
|
|
7
|
+
if (typeof state.credits !== "number" || !Number.isFinite(state.credits)) return void 0;
|
|
8
|
+
return {
|
|
9
|
+
credits: state.credits,
|
|
10
|
+
unit: typeof state.creditsUnit === "string" && state.creditsUnit.length > 0 ? state.creditsUnit : void 0
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function readStallStatus(status) {
|
|
14
|
+
if (!isRecord(status)) return void 0;
|
|
15
|
+
const { stalledMs, hint } = status;
|
|
16
|
+
if (typeof stalledMs !== "number" || !Number.isFinite(stalledMs) || stalledMs <= 0) return void 0;
|
|
17
|
+
return typeof hint === "string" && hint.length > 0 ? { stalledMs, hint } : { stalledMs };
|
|
18
|
+
}
|
|
19
|
+
function readPartMetadata(state) {
|
|
20
|
+
if (!isRecord(state)) return void 0;
|
|
21
|
+
const credits = readCreditState(state);
|
|
22
|
+
const status = readStallStatus(state.status);
|
|
23
|
+
if (!credits && !status) return void 0;
|
|
24
|
+
return { ...credits, ...status ? { status } : {} };
|
|
25
|
+
}
|
|
26
|
+
function carrierState(part) {
|
|
27
|
+
if (!isRecord(part)) return void 0;
|
|
28
|
+
if (part.type !== "text" && part.type !== "reasoning") return void 0;
|
|
29
|
+
return part.state;
|
|
30
|
+
}
|
|
31
|
+
function readPartCredits(part) {
|
|
32
|
+
return readCreditState(carrierState(part));
|
|
33
|
+
}
|
|
34
|
+
function readPartStatus(part) {
|
|
35
|
+
const state = carrierState(part);
|
|
36
|
+
return isRecord(state) ? readStallStatus(state.status) : void 0;
|
|
37
|
+
}
|
|
38
|
+
function messageCredits(parts) {
|
|
39
|
+
const carriers = parts.map(readPartCredits).filter((value) => value !== void 0);
|
|
40
|
+
const last = carriers.at(-1);
|
|
41
|
+
if (!last) return void 0;
|
|
42
|
+
return {
|
|
43
|
+
credits: last.credits,
|
|
44
|
+
unit: last.unit ?? carriers.findLast((carrier) => carrier.unit !== void 0)?.unit
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function messageStatus(parts) {
|
|
48
|
+
return parts.map(readPartStatus).findLast((value) => value !== void 0);
|
|
49
|
+
}
|
|
50
|
+
function messageMetadata(parts) {
|
|
51
|
+
const credits = messageCredits(parts);
|
|
52
|
+
const status = messageStatus(parts);
|
|
53
|
+
if (!credits && !status) return void 0;
|
|
54
|
+
return { ...credits, ...status ? { status } : {} };
|
|
55
|
+
}
|
|
56
|
+
function addMessageMetadata(acc, hit) {
|
|
57
|
+
if (!hit) return acc;
|
|
58
|
+
return {
|
|
59
|
+
total: acc.total + (hit.credits ?? 0),
|
|
60
|
+
unit: hit.unit ?? acc.unit,
|
|
61
|
+
present: true,
|
|
62
|
+
...hit.status ? { status: hit.status } : {}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
var creditsAmount = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 });
|
|
66
|
+
function formatCredits(value, unit) {
|
|
67
|
+
const amount = creditsAmount.format(Number.isFinite(value) ? value : 0);
|
|
68
|
+
if (!unit) return amount;
|
|
69
|
+
const label = value === 1 || unit.endsWith("s") ? unit : `${unit}s`;
|
|
70
|
+
return `${amount} ${label}`;
|
|
71
|
+
}
|
|
72
|
+
var ERROR_SUFFIX = "Error";
|
|
73
|
+
function stallReason(hint) {
|
|
74
|
+
if (typeof hint !== "string" || hint.length === 0) return void 0;
|
|
75
|
+
const token = /\bkind:\s*([A-Z][A-Za-z0-9]*)/.exec(hint)?.[1] ?? /\b([A-Z][A-Za-z0-9]*Error)\b/.exec(hint)?.[1];
|
|
76
|
+
if (!token) return void 0;
|
|
77
|
+
const reason = token.endsWith(ERROR_SUFFIX) ? token.slice(0, -ERROR_SUFFIX.length) : token;
|
|
78
|
+
return reason.length > 0 ? reason : void 0;
|
|
79
|
+
}
|
|
80
|
+
function formatStallSummary(status) {
|
|
81
|
+
const valid = readStallStatus(status);
|
|
82
|
+
if (!valid) return void 0;
|
|
83
|
+
const seconds = Math.max(1, Math.round(valid.stalledMs / 1e3));
|
|
84
|
+
const reason = stallReason(valid.hint);
|
|
85
|
+
return reason ? `last turn stalled ${seconds}s (${reason})` : `last turn stalled ${seconds}s`;
|
|
86
|
+
}
|
|
87
|
+
var STALL_SEPARATOR = " \xB7 ";
|
|
88
|
+
function creditsChipText(credits) {
|
|
89
|
+
if (!credits.present) return "";
|
|
90
|
+
const total = formatCredits(credits.total, credits.unit);
|
|
91
|
+
const stall = formatStallSummary(credits.status);
|
|
92
|
+
return stall ? `${total}${STALL_SEPARATOR}${stall}` : total;
|
|
93
|
+
}
|
|
94
|
+
var money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
readPartMetadata,
|
|
98
|
+
messageMetadata,
|
|
99
|
+
addMessageMetadata,
|
|
100
|
+
formatCredits,
|
|
101
|
+
formatStallSummary,
|
|
102
|
+
creditsChipText
|
|
103
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
formatCredits
|
|
3
|
-
|
|
2
|
+
formatCredits,
|
|
3
|
+
formatStallSummary
|
|
4
|
+
} from "./chunk-LRAGM3PR.js";
|
|
4
5
|
|
|
5
6
|
// src/tui/credits-box-view.ts
|
|
6
7
|
import { createElement, insert, insertNode, setProp } from "@opentui/solid";
|
|
@@ -16,6 +17,10 @@ function createCreditsBoxView(credits, tokens) {
|
|
|
16
17
|
root,
|
|
17
18
|
plainLine(() => current().present ? formatCredits(current().total, current().unit) : "", tokens?.subdued)
|
|
18
19
|
);
|
|
20
|
+
insertNode(
|
|
21
|
+
root,
|
|
22
|
+
plainLine(() => current().present ? formatStallSummary(current().status) ?? "" : "", tokens?.subdued)
|
|
23
|
+
);
|
|
19
24
|
return root;
|
|
20
25
|
}
|
|
21
26
|
function headerLine(content, fg) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
creditsChipText
|
|
3
|
+
} from "./chunk-LRAGM3PR.js";
|
|
4
4
|
|
|
5
5
|
// src/tui/credits-chip-view.ts
|
|
6
6
|
import { createElement, insert, setProp } from "@opentui/solid";
|
|
@@ -12,7 +12,7 @@ function createCreditsChipView(credits, tokens) {
|
|
|
12
12
|
setProp(chip, "wrapMode", "none");
|
|
13
13
|
setProp(chip, "flexShrink", 0);
|
|
14
14
|
if (tokens?.subdued !== void 0) setProp(chip, "fg", tokens.subdued);
|
|
15
|
-
insert(chip, () =>
|
|
15
|
+
insert(chip, () => creditsChipText(current()));
|
|
16
16
|
return chip;
|
|
17
17
|
}
|
|
18
18
|
export {
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// package.json
|
|
2
|
-
var version = "0.5.0-beta.
|
|
2
|
+
var version = "0.5.0-beta.5";
|
|
3
3
|
|
|
4
4
|
// src/server/auth.ts
|
|
5
5
|
var KIRO_INTEGRATION_ID = "kiro";
|
|
@@ -133,6 +133,25 @@ var KIRO_PROVIDER_PACKAGE = "aisdk:kiro-acp-ai-provider";
|
|
|
133
133
|
function asString(value) {
|
|
134
134
|
return value;
|
|
135
135
|
}
|
|
136
|
+
var DISCOVERY_TIMEOUT_MS = 6e4;
|
|
137
|
+
var RETRY_BACKOFF_MS = [5e3, 2e4, 6e4];
|
|
138
|
+
var LOG_PREFIX = "[opencode-kiro]";
|
|
139
|
+
function describeError(error) {
|
|
140
|
+
return error instanceof Error ? error.message : String(error);
|
|
141
|
+
}
|
|
142
|
+
function cancelRetry(state) {
|
|
143
|
+
if (state.retryTimer !== void 0) {
|
|
144
|
+
clearTimeout(state.retryTimer);
|
|
145
|
+
state.retryTimer = void 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function clearDiscoveryTimers(state) {
|
|
149
|
+
if (state.probeTimer !== void 0) {
|
|
150
|
+
clearTimeout(state.probeTimer);
|
|
151
|
+
state.probeTimer = void 0;
|
|
152
|
+
}
|
|
153
|
+
cancelRetry(state);
|
|
154
|
+
}
|
|
136
155
|
function createDiscoveryResources() {
|
|
137
156
|
return {
|
|
138
157
|
state: void 0,
|
|
@@ -201,38 +220,91 @@ function applyCatalogSnapshot(draft, state) {
|
|
|
201
220
|
trustAllTools: true,
|
|
202
221
|
// intentionally not exposed as a plugin option
|
|
203
222
|
mcpTimeout: state.options.mcpTimeout,
|
|
223
|
+
...state.options.stall !== void 0 ? { stall: state.options.stall } : {},
|
|
204
224
|
contextWindows
|
|
205
225
|
};
|
|
206
226
|
});
|
|
207
227
|
}
|
|
208
228
|
function createDiscover(context, state) {
|
|
209
|
-
async function
|
|
229
|
+
async function publish(gen, discovered) {
|
|
230
|
+
if (gen !== state.generation) return;
|
|
231
|
+
const uniqueIds = new Set(discovered.map((model) => model.modelId));
|
|
232
|
+
if (uniqueIds.size !== discovered.length) {
|
|
233
|
+
console.error(
|
|
234
|
+
`${LOG_PREFIX} model discovery for ${state.cwd} returned duplicate model ids; catalog left unchanged`
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
cancelRetry(state);
|
|
239
|
+
state.snapshot = discovered;
|
|
240
|
+
await context.catalog.reload();
|
|
241
|
+
}
|
|
242
|
+
function probeDeadline(gen, probe) {
|
|
243
|
+
return new Promise((_resolve, reject) => {
|
|
244
|
+
const timer = setTimeout(() => {
|
|
245
|
+
if (state.probeTimer === timer) state.probeTimer = void 0;
|
|
246
|
+
void probe.then((discovered) => publish(gen, discovered)).catch(() => {
|
|
247
|
+
});
|
|
248
|
+
reject(new Error(`no response after ${DISCOVERY_TIMEOUT_MS / 1e3}s`));
|
|
249
|
+
}, DISCOVERY_TIMEOUT_MS);
|
|
250
|
+
state.probeTimer = timer;
|
|
251
|
+
const settle = () => {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
if (state.probeTimer === timer) state.probeTimer = void 0;
|
|
254
|
+
};
|
|
255
|
+
probe.then(settle, settle);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function runDiscovery(gen, attempt) {
|
|
210
259
|
try {
|
|
211
260
|
const { listModels } = await import("kiro-acp-ai-provider");
|
|
212
|
-
const
|
|
261
|
+
const probe = listModels({ cwd: state.cwd });
|
|
262
|
+
const discovered = await Promise.race([probe, probeDeadline(gen, probe)]);
|
|
263
|
+
await publish(gen, discovered);
|
|
264
|
+
} catch (error) {
|
|
213
265
|
if (gen !== state.generation) return;
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
266
|
+
const delay = RETRY_BACKOFF_MS[attempt];
|
|
267
|
+
const next = delay === void 0 ? "giving up until the next credential change" : `retrying in ${delay / 1e3}s`;
|
|
268
|
+
console.error(
|
|
269
|
+
`${LOG_PREFIX} model discovery failed for ${state.cwd} (attempt ${attempt + 1}/${RETRY_BACKOFF_MS.length + 1}): ${describeError(error)}; ${next}`
|
|
270
|
+
);
|
|
271
|
+
if (delay !== void 0) scheduleRetry(gen, attempt + 1, delay);
|
|
219
272
|
}
|
|
220
273
|
}
|
|
274
|
+
function start(attempt) {
|
|
275
|
+
const gen = ++state.generation;
|
|
276
|
+
const run = runDiscovery(gen, attempt).finally(() => {
|
|
277
|
+
if (state.inflight === run) state.inflight = void 0;
|
|
278
|
+
});
|
|
279
|
+
state.inflight = run;
|
|
280
|
+
return run;
|
|
281
|
+
}
|
|
282
|
+
function scheduleRetry(gen, attempt, delay) {
|
|
283
|
+
cancelRetry(state);
|
|
284
|
+
state.retryTimer = setTimeout(() => {
|
|
285
|
+
state.retryTimer = void 0;
|
|
286
|
+
void retry(gen, attempt).catch(() => {
|
|
287
|
+
});
|
|
288
|
+
}, delay);
|
|
289
|
+
}
|
|
290
|
+
async function retry(gen, attempt) {
|
|
291
|
+
if (gen !== state.generation) return;
|
|
292
|
+
const active = await context.integration.connection.active(KIRO_INTEGRATION_ID);
|
|
293
|
+
if (!active || gen !== state.generation) return;
|
|
294
|
+
await start(attempt);
|
|
295
|
+
}
|
|
221
296
|
return async function discover(_reason) {
|
|
222
297
|
const active = await context.integration.connection.active(KIRO_INTEGRATION_ID);
|
|
223
298
|
if (!active) {
|
|
299
|
+
cancelRetry(state);
|
|
224
300
|
state.generation += 1;
|
|
225
301
|
state.snapshot = void 0;
|
|
226
302
|
await context.catalog.reload();
|
|
227
303
|
return;
|
|
228
304
|
}
|
|
229
305
|
if (state.inflight !== void 0) return state.inflight;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (state.inflight === run) state.inflight = void 0;
|
|
233
|
-
});
|
|
234
|
-
state.inflight = run;
|
|
235
|
-
return run;
|
|
306
|
+
cancelRetry(state);
|
|
307
|
+
return start(0);
|
|
236
308
|
};
|
|
237
309
|
}
|
|
238
310
|
function isKiroCredentialEvent(event) {
|
|
@@ -253,10 +325,8 @@ async function consumeEvents(iterator, discover) {
|
|
|
253
325
|
if (result.done) return;
|
|
254
326
|
const event = result.value;
|
|
255
327
|
if (isKiroCredentialEvent(event)) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
} catch {
|
|
259
|
-
}
|
|
328
|
+
void discover(event.type).catch(() => {
|
|
329
|
+
});
|
|
260
330
|
}
|
|
261
331
|
}
|
|
262
332
|
} catch {
|
|
@@ -269,7 +339,9 @@ async function registerDiscovery(context, resources, options) {
|
|
|
269
339
|
options,
|
|
270
340
|
snapshot: void 0,
|
|
271
341
|
generation: 0,
|
|
272
|
-
inflight: void 0
|
|
342
|
+
inflight: void 0,
|
|
343
|
+
probeTimer: void 0,
|
|
344
|
+
retryTimer: void 0
|
|
273
345
|
};
|
|
274
346
|
resources.state = state;
|
|
275
347
|
const discover = createDiscover(context, state);
|
|
@@ -286,6 +358,7 @@ async function registerDiscovery(context, resources, options) {
|
|
|
286
358
|
}
|
|
287
359
|
return async () => {
|
|
288
360
|
state.generation += 1;
|
|
361
|
+
clearDiscoveryTimers(state);
|
|
289
362
|
const errors = [];
|
|
290
363
|
const eventIterator = resources.eventIterator;
|
|
291
364
|
resources.eventIterator = void 0;
|
|
@@ -336,7 +409,8 @@ var SETTINGS_ALLOWLIST = [
|
|
|
336
409
|
"sessionId",
|
|
337
410
|
"contextWindow",
|
|
338
411
|
"contextWindows",
|
|
339
|
-
"mcpTimeout"
|
|
412
|
+
"mcpTimeout",
|
|
413
|
+
"stall"
|
|
340
414
|
];
|
|
341
415
|
function sanitizeSettings(options) {
|
|
342
416
|
const settings = {};
|
|
@@ -464,12 +538,22 @@ function buildCleanup(state) {
|
|
|
464
538
|
|
|
465
539
|
// src/server.ts
|
|
466
540
|
function resolveOptions(raw) {
|
|
541
|
+
const stall = resolveStall(raw.stall);
|
|
467
542
|
return {
|
|
468
543
|
agent: typeof raw.agent === "string" && raw.agent !== "" ? raw.agent : "opencode",
|
|
469
544
|
mcpTimeout: typeof raw.mcpTimeout === "number" && Number.isFinite(raw.mcpTimeout) && raw.mcpTimeout > 0 ? raw.mcpTimeout : 45,
|
|
470
|
-
discover: typeof raw.discover === "boolean" ? raw.discover : true
|
|
545
|
+
discover: typeof raw.discover === "boolean" ? raw.discover : true,
|
|
546
|
+
...stall !== void 0 ? { stall } : {}
|
|
471
547
|
};
|
|
472
548
|
}
|
|
549
|
+
function resolveStall(raw) {
|
|
550
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
551
|
+
const { afterMs, live } = raw;
|
|
552
|
+
const stall = {};
|
|
553
|
+
if (typeof afterMs === "number" && Number.isFinite(afterMs) && afterMs >= 0) stall.afterMs = afterMs;
|
|
554
|
+
if (live === "off" || live === "reasoning") stall.live = live;
|
|
555
|
+
return Object.keys(stall).length > 0 ? stall : void 0;
|
|
556
|
+
}
|
|
473
557
|
var plugin = {
|
|
474
558
|
id: "kiro",
|
|
475
559
|
// tui: true (dist/promise/plugin.d.ts) — the host auto-loads this
|
package/dist/tui.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
addMessageMetadata,
|
|
3
|
+
messageMetadata,
|
|
4
|
+
readPartMetadata
|
|
5
|
+
} from "./chunk-LRAGM3PR.js";
|
|
5
6
|
|
|
6
7
|
// src/tui/transient-credits.ts
|
|
7
8
|
var SEPARATOR = "\0";
|
|
@@ -26,34 +27,31 @@ function recordTextEnded(store, event) {
|
|
|
26
27
|
const { sessionID, assistantMessageID, ordinal, state } = data;
|
|
27
28
|
if (!isValidKeyPart(sessionID) || !isValidKeyPart(assistantMessageID)) return;
|
|
28
29
|
if (typeof ordinal !== "number" || !Number.isInteger(ordinal) || ordinal < 0) return;
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
31
|
-
store.entries.set(transientKey(sessionID, assistantMessageID, ordinal),
|
|
30
|
+
const metadata = readPartMetadata(state);
|
|
31
|
+
if (!metadata) return;
|
|
32
|
+
store.entries.set(transientKey(sessionID, assistantMessageID, ordinal), metadata);
|
|
32
33
|
}
|
|
33
34
|
function transientForMessage(store, sessionID, assistantMessageID) {
|
|
34
|
-
|
|
35
|
-
let bestOrdinal = -1;
|
|
35
|
+
const entries = [];
|
|
36
36
|
for (const [key, value] of store.entries) {
|
|
37
37
|
const parsed = parseKey(key);
|
|
38
38
|
if (!parsed || parsed[0] !== sessionID || parsed[1] !== assistantMessageID) continue;
|
|
39
|
-
|
|
40
|
-
bestOrdinal = parsed[2];
|
|
41
|
-
best = value;
|
|
42
|
-
}
|
|
39
|
+
entries.push([parsed[2], value]);
|
|
43
40
|
}
|
|
44
|
-
return
|
|
41
|
+
if (entries.length === 0) return void 0;
|
|
42
|
+
entries.sort((a, b) => a[0] - b[0]);
|
|
43
|
+
const credits = entries.findLast(([, value]) => value.credits !== void 0)?.[1];
|
|
44
|
+
const status = entries.findLast(([, value]) => value.status !== void 0)?.[1]?.status;
|
|
45
|
+
return {
|
|
46
|
+
...credits ? { credits: credits.credits, unit: credits.unit } : {},
|
|
47
|
+
...status ? { status } : {}
|
|
48
|
+
};
|
|
45
49
|
}
|
|
46
50
|
function mergedMessageCredits(store, sessionID, durableMessages) {
|
|
47
51
|
return durableMessages.filter((message) => message.type === "assistant").reduce(
|
|
48
52
|
(acc, message) => {
|
|
49
|
-
const durable =
|
|
50
|
-
|
|
51
|
-
if (!hit) return acc;
|
|
52
|
-
return {
|
|
53
|
-
total: acc.total + hit.credits,
|
|
54
|
-
unit: hit.unit ?? acc.unit,
|
|
55
|
-
present: true
|
|
56
|
-
};
|
|
53
|
+
const durable = messageMetadata(message.content ?? []);
|
|
54
|
+
return addMessageMetadata(acc, durable ?? transientForMessage(store, sessionID, message.id));
|
|
57
55
|
},
|
|
58
56
|
{ total: 0, unit: void 0, present: false }
|
|
59
57
|
);
|
|
@@ -68,7 +66,7 @@ function reconcile(store, sessionID, durableMessages) {
|
|
|
68
66
|
store.entries.delete(key);
|
|
69
67
|
continue;
|
|
70
68
|
}
|
|
71
|
-
if (
|
|
69
|
+
if (messageMetadata(message.content ?? []) !== void 0) store.entries.delete(key);
|
|
72
70
|
}
|
|
73
71
|
}
|
|
74
72
|
function clear(store) {
|
|
@@ -109,19 +107,20 @@ var plugin = {
|
|
|
109
107
|
const store = memoryStore ? memoryStore[0] : createTransientStore();
|
|
110
108
|
if (!memoryStore) disposers.push(() => clear(store));
|
|
111
109
|
const [{ createCreditsBoxView }, { createCreditsChipView }, { createSignal }] = await Promise.all([
|
|
112
|
-
import("./credits-box-view-
|
|
113
|
-
import("./credits-chip-view-
|
|
110
|
+
import("./credits-box-view-2OGZPIW6.js"),
|
|
111
|
+
import("./credits-chip-view-M7PLKDN4.js"),
|
|
114
112
|
import("solid-js")
|
|
115
113
|
]);
|
|
116
114
|
const [transientVersion, setTransientVersion] = createSignal(0);
|
|
117
|
-
const
|
|
115
|
+
const recordEnded = (event) => {
|
|
118
116
|
try {
|
|
119
117
|
recordTextEnded(store, event);
|
|
120
118
|
setTransientVersion((version) => version + 1);
|
|
121
119
|
} catch {
|
|
122
120
|
}
|
|
123
|
-
}
|
|
124
|
-
disposers.push(
|
|
121
|
+
};
|
|
122
|
+
disposers.push(context.data.on("session.text.ended", recordEnded));
|
|
123
|
+
disposers.push(context.data.on("session.reasoning.ended", recordEnded));
|
|
125
124
|
const creditsFor = (sessionID) => () => {
|
|
126
125
|
if (!sessionID) return EMPTY_CREDITS;
|
|
127
126
|
transientVersion();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-kiro",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.5",
|
|
4
4
|
"description": "The ACP-compliant Kiro plugin for opencode: auth via the official kiro-cli login, the live Kiro model lineup through the Agent Client Protocol, and TUI credits display",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nacho F. Lizaur (https://github.com/NachoFLizaur)",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@opentui/solid": "0.5.9",
|
|
49
|
-
"kiro-acp-ai-provider": "3.
|
|
49
|
+
"kiro-acp-ai-provider": "3.2.0",
|
|
50
50
|
"solid-js": "1.9.12"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
package/dist/chunk-M7OUVABX.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// src/tui/credits.ts
|
|
2
|
-
function isRecord(value) {
|
|
3
|
-
return typeof value === "object" && value !== null;
|
|
4
|
-
}
|
|
5
|
-
function readCreditState(state) {
|
|
6
|
-
if (!isRecord(state)) return void 0;
|
|
7
|
-
if (typeof state.credits !== "number" || !Number.isFinite(state.credits)) return void 0;
|
|
8
|
-
return {
|
|
9
|
-
credits: state.credits,
|
|
10
|
-
unit: typeof state.creditsUnit === "string" && state.creditsUnit.length > 0 ? state.creditsUnit : void 0
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
function readPartCredits(part) {
|
|
14
|
-
if (!isRecord(part)) return void 0;
|
|
15
|
-
if (part.type !== "text" && part.type !== "reasoning") return void 0;
|
|
16
|
-
return readCreditState(part.state);
|
|
17
|
-
}
|
|
18
|
-
function messageCredits(parts) {
|
|
19
|
-
const carriers = parts.map(readPartCredits).filter((value) => value !== void 0);
|
|
20
|
-
const last = carriers.at(-1);
|
|
21
|
-
if (!last) return void 0;
|
|
22
|
-
return {
|
|
23
|
-
credits: last.credits,
|
|
24
|
-
unit: last.unit ?? carriers.findLast((carrier) => carrier.unit !== void 0)?.unit
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
var creditsAmount = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 });
|
|
28
|
-
function formatCredits(value, unit) {
|
|
29
|
-
const amount = creditsAmount.format(Number.isFinite(value) ? value : 0);
|
|
30
|
-
if (!unit) return amount;
|
|
31
|
-
const label = value === 1 || unit.endsWith("s") ? unit : `${unit}s`;
|
|
32
|
-
return `${amount} ${label}`;
|
|
33
|
-
}
|
|
34
|
-
var money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
|
|
35
|
-
|
|
36
|
-
export {
|
|
37
|
-
readCreditState,
|
|
38
|
-
messageCredits,
|
|
39
|
-
formatCredits
|
|
40
|
-
};
|