opencode-kiro 0.5.0-beta.3 → 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 CHANGED
@@ -1,12 +1,11 @@
1
1
  # opencode-kiro
2
2
 
3
- > ⚠️ **Experimental prerelease** — `0.5.0-beta.3` targets the **unreleased OpenCode v2**
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
- > which remains the supported stable release). See
7
- > [RELEASE_NOTES_0.5.0-beta.3.md](./RELEASE_NOTES_0.5.0-beta.3.md) and
8
- > [PINNED_VERSIONS.md](./PINNED_VERSIONS.md) for exact pins and the tested OpenCode SHA.
9
- > No OpenCode v2 release date is known or claimed here.
6
+ > which remains the supported stable release). See [CHANGELOG.md](./CHANGELOG.md) and
7
+ > [docs/COMPATIBILITY.md](./docs/COMPATIBILITY.md) for exact pins and the tested
8
+ > OpenCode SHA. No OpenCode v2 release date is known or claimed here.
10
9
 
11
10
  The ACP-compliant [Kiro](https://kiro.dev) plugin for [opencode](https://opencode.ai).
12
11
 
@@ -19,7 +18,7 @@ The plugin supplies:
19
18
  minimal self-registration fallback when the catalog lacks a `kiro` entry
20
19
  - **Provider ownership**: an AISDK hook constructs the provider from
21
20
  [`kiro-acp-ai-provider`](https://www.npmjs.com/package/kiro-acp-ai-provider) with the
22
- right options (`cwd`, `agent`, `trustAllTools`, `mcpTimeout`, `contextWindows`)
21
+ right options (`cwd`, `agent`, `trustAllTools`, `mcpTimeout`, `stall`, `contextWindows`)
23
22
  - **TUI credits display**: a live Kiro credits box in the sidebar and a compact
24
23
  credits chip in the prompt footer row beside the host cost/context display, both
25
24
  styled with the host's active theme tokens — auto-loaded from the same single
@@ -36,11 +35,11 @@ This prerelease is built and tested against **one pinned OpenCode v2 snapshot**:
36
35
 
37
36
  | Item | Value |
38
37
  |---|---|
39
- | Tested OpenCode commit (`upstream/v2`) | `8ba434b5973856b2f32b8cd3543e154b25c413e6` |
38
+ | Tested OpenCode commit (`upstream/v2` head, 2026-08-29) | `8ba434b5973856b2f32b8cd3543e154b25c413e6` |
40
39
  | `@opencode-ai/plugin` | `0.0.0-dev-18686` (exact) |
41
- | Package version | `0.5.0-beta.3` |
40
+ | Package version | `0.5.0-beta.5` |
42
41
 
43
- Full pin table and verification evidence: [PINNED_VERSIONS.md](./PINNED_VERSIONS.md).
42
+ Full pin table and verification steps: [docs/COMPATIBILITY.md](./docs/COMPATIBILITY.md).
44
43
  There is no `engines.opencode` constraint — the v2 host has no stable semver yet; the
45
44
  tested SHA above is the compatibility target. Other v2 snapshots may or may not work.
46
45
 
@@ -55,26 +54,29 @@ tested SHA above is the compatibility target. Other v2 snapshots may or may not
55
54
  ## Install and configure
56
55
 
57
56
  **One config entry — that's the whole setup.** Add the package to the **`plugins`**
58
- array (plural) of your OpenCode config (`opencode.json`, project or global):
57
+ array (plural) of your OpenCode config. That is any `opencode.json` or `opencode.jsonc`
58
+ (or `.opencode/opencode.json`) found walking up from the project directory, or the
59
+ global `~/.config/opencode/opencode.json`:
59
60
 
60
61
  ```json
61
62
  {
62
- "plugins": ["opencode-kiro@0.5.0-beta.3"]
63
+ "plugins": ["opencode-kiro@0.5.0-beta.5"]
63
64
  }
64
65
  ```
65
66
 
66
- > ⚠️ **Always pin the exact version, as above.** The host now background-auto-refreshes
67
- > UNPINNED npm plugin packages to whatever the registry serves — a bare `"opencode-kiro"`
67
+ > ⚠️ **Always pin the exact version, as above.** The host background-auto-refreshes
68
+ > unpinned npm plugin packages to whatever the registry serves, so a bare `"opencode-kiro"`
68
69
  > spec can silently move you off the tested build. Use the exact
69
- > `opencode-kiro@0.5.0-beta.3` spec.
70
+ > `opencode-kiro@0.5.0-beta.5` spec.
70
71
 
71
- The object form pins the same version and leaves room for future options:
72
+ The object form pins the same version and takes the plugin options described in
73
+ [Plugin options](#plugin-options):
72
74
 
73
75
  ```json
74
76
  {
75
77
  "plugins": [
76
78
  {
77
- "package": "opencode-kiro@0.5.0-beta.3",
79
+ "package": "opencode-kiro@0.5.0-beta.5",
78
80
  "options": {}
79
81
  }
80
82
  ]
@@ -85,8 +87,95 @@ This loads the server entry (`./server` export): auth, model discovery, and prov
85
87
  ownership. The server plugin declares **`tui: true`**, so the host TUI **auto-loads the
86
88
  package's `./tui` entrypoint by itself** — the sidebar credits box and the prompt footer
87
89
  credits chip appear with no further configuration. **No `cli.json` TUI entry is
88
- needed.** (The beta.1 two-file setup is obsolete: if you added `"opencode-kiro"` to
89
- your global `cli.json` `plugins` array for beta.1, remove it.)
90
+ needed.**
91
+
92
+ ### Plugin options
93
+
94
+ All options are optional; omit the `options` object entirely to get the defaults.
95
+
96
+ | Option | Type | Default | Meaning |
97
+ |---|---|---|---|
98
+ | `agent` | string | `"opencode"` | kiro-cli agent name the provider runs under |
99
+ | `mcpTimeout` | number | `45` | MCP tool-call timeout, in **minutes** |
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). |
102
+
103
+ ```json
104
+ {
105
+ "plugins": [
106
+ {
107
+ "package": "opencode-kiro@0.5.0-beta.5",
108
+ "options": {
109
+ "agent": "opencode",
110
+ "mcpTimeout": 45,
111
+ "discover": true,
112
+ "stall": { "afterMs": 10000, "live": "reasoning" }
113
+ }
114
+ }
115
+ ]
116
+ }
117
+ ```
118
+
119
+ A value of the wrong type falls back to its default; unknown keys are ignored. The
120
+ contract per option: `mcpTimeout` must be a positive number of minutes; zero, negative,
121
+ or non-numeric values fall back to the default (`45`); an empty `agent` string falls
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).
126
+
127
+ > ⚠️ **Options work on the npm channel only.** The host passes `options` to plugins it
128
+ > installed from npm (the `"package": "opencode-kiro@<version>"` form above). Local
129
+ > `name@file:` tarball installs count as the npm channel, so options work there too.
130
+ > Bundled or built-in plugin loads receive an empty options object, so **the defaults
131
+ > always apply there** regardless of what you write in `options`.
132
+
133
+ There is **no `cwd` option**. The working directory handed to kiro-cli is derived
134
+ automatically from the OpenCode location the plugin is set up for, once per location;
135
+ a user-supplied path could only be less accurate.
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).
90
179
 
91
180
  ### Disabling
92
181
 
@@ -96,7 +185,7 @@ activates either:
96
185
 
97
186
  ```json
98
187
  {
99
- "plugins": ["opencode-kiro@0.5.0-beta.3", "-kiro"]
188
+ "plugins": ["opencode-kiro@0.5.0-beta.5", "-kiro"]
100
189
  }
101
190
  ```
102
191
 
@@ -118,14 +207,14 @@ npm install && npm run build && npm pack
118
207
  ```
119
208
 
120
209
  ```json
121
- { "plugins": ["opencode-kiro@file:/absolute/path/to/opencode-kiro-0.5.0-beta.3.tgz"] }
210
+ { "plugins": ["opencode-kiro@file:/absolute/path/to/opencode-kiro-0.5.0-beta.5.tgz"] }
122
211
  ```
123
212
 
124
213
  A bare path or bare `file:` spec is rejected at the tested SHA — the `name@file:` form
125
214
  is required. **Caveat (local `file:` installs only)**: the colon in the resulting
126
215
  install dirname defeats the host's OpenTUI loader shim, so the TUI surfaces render a
127
216
  contained per-slot error notice instead of the credits views (the rest of the TUI keeps
128
- working). Registry installs (`opencode-kiro@0.5.0-beta.3`) use colon-free paths and are
217
+ working). Registry installs (`opencode-kiro@0.5.0-beta.5`) use colon-free paths and are
129
218
  fully green — this affects local tarball validation only.
130
219
 
131
220
  The host resolves entrypoints from the package `exports` (`./server` for the server
@@ -148,10 +237,8 @@ Select the **Kiro** integration, then the **Kiro CLI Login** method:
148
237
  Complete the login there; the plugin polls for up to 120 seconds and stores a minimal
149
238
  credential record when kiro-cli reports success.
150
239
 
151
- There is no configuration prompt during login anymore: the beta.1
152
- "Enable the Kiro credits sidebar?" consent select was removed (upstream deleted the
153
- prompts API in favor of forms — and the sidebar no longer needs consent-driven config,
154
- since `tui: true` auto-loads it).
240
+ There is no configuration prompt during login; the sidebar and footer credits surfaces
241
+ auto-load from the single `plugins` entry.
155
242
 
156
243
  If the flow times out, authenticate directly with kiro-cli (`kiro-cli login`) and run
157
244
  `opencode auth login` again; the fast path then completes immediately.
@@ -169,6 +256,14 @@ sets the model's base effort. A discovery failure or duplicate runtime ID leaves
169
256
  catalog unchanged (fail-open). If the loaded catalog has no `kiro` provider at all, the
170
257
  plugin self-registers a minimal fallback entry so discovered models remain usable.
171
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
+
172
267
  List the resulting models with:
173
268
 
174
269
  ```bash
@@ -176,16 +271,11 @@ opencode models
176
271
  opencode run -m kiro/<exact-model-id> "hello"
177
272
  ```
178
273
 
179
- Effort-capable models expose their variants through OpenCode's model-variant selection.
180
- The chosen level reaches the SDK through the provider-settings path: OpenCode overlays
181
- the selected variant's `settings` onto the model's `settings`, hands them to the
182
- plugin's `aisdk` hook as `event.options`, and the plugin passes them verbatim to
183
- `createKiroAcp({ effort })`. That is why the plugin emits the SDK's own `effort` key
184
- (not `reasoningEffort`) — and why the SDK factory setting, not per-call provider
185
- options, is the working carrier: OpenCode builds per-call `providerOptions` only for
186
- the first-party `@ai-sdk/*` provider families, so `providerOptions.kiro.*` is never
187
- populated for an `aisdk:` package provider like this one. Kiro cannot disable thinking,
188
- so even the lowest level still produces a reasoning trail.
274
+ Effort-capable models expose their variants through OpenCode's model-variant selection:
275
+ pick the variant to choose a reasoning-effort level. The selected level is applied per
276
+ request, and one provider instance (one kiro-cli process) serves every effort level, so
277
+ switching effort does not start additional kiro-cli processes. Kiro cannot disable
278
+ thinking, so even the lowest level still produces a reasoning trail.
189
279
 
190
280
  ## Credits in the TUI
191
281
 
@@ -196,7 +286,7 @@ plugin renders two surfaces:
196
286
  - a Kiro credits box in the sidebar (`sidebar.content` claim), showing the session's
197
287
  live credits total and unit
198
288
  - a compact credits chip in the prompt footer row beside the host cost/context
199
- display (`prompt.footer.status` claim, v1 placement restored) with the same total
289
+ display (`prompt.footer.status` claim) with the same total
200
290
 
201
291
  Both are additive `append` claims — they compose with the host's built-in content and
202
292
  never replace it — and both pick up the active theme's text tokens (feature-detected;
@@ -208,11 +298,11 @@ The only configuration needed is the single `plugins` entry from
208
298
  [Install](#install-and-configure) — the TUI half auto-loads via `tui: true`.
209
299
 
210
300
  Durable credits are read straight from host message state (the host persists provider
211
- state on text end — fixed upstream since beta.1). While a turn is still streaming,
301
+ state on text end). While a turn is still streaming,
212
302
  credits for just-ended text are picked up live through a transient overlay that works
213
303
  around a host reducer bug at the pinned snapshot (the live event path still drops
214
304
  provider state); once durable state arrives it is authoritative and nothing is
215
- double-counted. See the release notes for details.
305
+ double-counted. See [CHANGELOG.md](./CHANGELOG.md) for details.
216
306
 
217
307
  ## Known limitations (prerelease)
218
308
 
@@ -220,7 +310,7 @@ double-counted. See the release notes for details.
220
310
  upstream, but the live `session.text.ended` reducer still drops provider state at
221
311
  the tested SHA, so in-turn updates come from the plugin's transient overlay
222
312
  (durable state always wins on reconcile). See
223
- [Credits in the TUI](#credits-in-the-tui) and the release notes.
313
+ [Credits in the TUI](#credits-in-the-tui) and [CHANGELOG.md](./CHANGELOG.md).
224
314
  - **Credits render in the TUI only.** Every other cost surface (ACP clients, web,
225
315
  desktop, share pages, CLI cost output) shows $0.00 for Kiro sessions because the
226
316
  catalog declares Kiro's per-token `cost` as 0 (subscription-metered, no per-token
@@ -239,17 +329,33 @@ double-counted. See the release notes for details.
239
329
  ## How it works
240
330
 
241
331
  - **Auth (Integration + Credential)**: the plugin upserts the `kiro` integration with a
242
- "Kiro CLI Login" OAuth method. `verifyAuth` from `kiro-acp-ai-provider` is the auth
243
- authority (it delegates to kiro-cli); success is stored as a minimal
244
- `Credential.OAuth` presence record.
332
+ "Kiro CLI Login" OAuth method. `verifyAuthAsync` from `kiro-acp-ai-provider` is the
333
+ auth authority (it delegates to kiro-cli without blocking the host's event loop);
334
+ success is stored as a minimal `Credential.OAuth` presence record. Starting a new
335
+ login while one is pending cancels the earlier attempt.
245
336
  - **Model discovery (catalog transform)**: after login (and on later login events) the
246
337
  plugin runs `listModels()` outside the transform, then applies the validated capture
247
- via a catalog transform and reload — exact ID matching, catalog metadata preserved,
248
- effort variants projected, fail-open on any discovery error.
249
- - **Provider ownership (AISDK hook)**: the plugin's SDK hook always constructs the
250
- provider from `kiro-acp-ai-provider` with the plugin-supplied options and sets it as
251
- the event's SDK, so the Kiro provider is always plugin-owned. The options relay each
252
- model's context window into the SDK's `contextWindows` map keyed by model ID.
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.
343
+ - **Provider ownership (AISDK hooks)**: the plugin's `sdk` hook constructs the provider
344
+ from `kiro-acp-ai-provider` with the plugin-supplied settings (identifying itself to
345
+ kiro-cli as `opencode-kiro`) and sets it as the event's SDK, so the Kiro provider is
346
+ always plugin-owned; one instance is cached per distinct settings and shared across
347
+ effort variants. The `language` hook applies the selected effort per request. The
348
+ settings relay each model's context window into the SDK's `contextWindows` map keyed
349
+ by model ID.
350
+ - **Effort carrier (why SDK settings, not `providerOptions`)**: OpenCode overlays the
351
+ selected variant's `settings` onto the model's `settings` and hands them to the
352
+ plugin's `aisdk` hooks as `event.options`; the `language` hook reads
353
+ `event.options.effort` and passes it to the shared provider as a per-model override
354
+ (`languageModel(id, { effort })`). The plugin emits the SDK's own `effort` key (not
355
+ `reasoningEffort`), and the settings path is the working carrier because OpenCode
356
+ builds per-call `providerOptions` only for the first-party `@ai-sdk/*` provider
357
+ families - `providerOptions.kiro.*` is never populated for an `aisdk:` package
358
+ provider like this one.
253
359
  - **Session affinity & reset (in-SDK)**: the SDK keys kiro-cli sessions off OpenCode's
254
360
  session affinity, isolates tool-less utility calls on an ephemeral session, detects
255
361
  prompt-history divergence, and starts a fresh kiro session when needed.
@@ -257,6 +363,11 @@ double-counted. See the release notes for details.
257
363
  metadata; OpenCode persists them key-unwrapped on message part state
258
364
  (`part.state.credits`, `part.state.creditsUnit`), and the TUI plugin sums them per
259
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.
260
371
 
261
372
  ## Troubleshooting
262
373
 
@@ -266,10 +377,11 @@ double-counted. See the release notes for details.
266
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). |
267
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). |
268
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. |
269
- | `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`). |
270
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). |
271
383
  | Provider visible but runs fail | The provider can be selectable before any credential exists. Run `opencode auth login` first. |
272
- | 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. |
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. |
273
385
 
274
386
  ## Legacy: v1 / OpenCode v1 users (`0.4.0`)
275
387
 
@@ -278,7 +390,7 @@ v1 (`opencode >= 1.16.0`). It uses the v1 contract throughout: singular `plugin`
278
390
  arrays in `opencode.json` and `tui.json`, the `opencode plugin opencode-kiro`
279
391
  installer, and `part.metadata.kiro` credits. Its full documentation is the README at
280
392
  the [`v0.4.0` tag](https://github.com/NachoFLizaur/opencode-kiro/tree/v0.4.0)
281
- (equivalently, `main`). Do not install `0.5.0-beta.3` into an OpenCode v1 setup.
393
+ (equivalently, `main`). Do not install `0.5.0-beta.5` into an OpenCode v1 setup.
282
394
 
283
395
  ## Development
284
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
- } from "./chunk-M7OUVABX.js";
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
- formatCredits
3
- } from "./chunk-M7OUVABX.js";
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, () => current().present ? formatCredits(current().total, current().unit) : "");
15
+ insert(chip, () => creditsChipText(current()));
16
16
  return chip;
17
17
  }
18
18
  export {
package/dist/server.js CHANGED
@@ -1,3 +1,6 @@
1
+ // package.json
2
+ var version = "0.5.0-beta.5";
3
+
1
4
  // src/server/auth.ts
2
5
  var KIRO_INTEGRATION_ID = "kiro";
3
6
  var KIRO_INTEGRATION_NAME = "Kiro";
@@ -8,6 +11,8 @@ var POLL_INTERVAL_MS = 2e3;
8
11
  var MAX_WAIT_MS = 12e4;
9
12
  var NOT_INSTALLED_MESSAGE = "kiro-cli is not installed. Install it from https://kiro.dev/docs/cli/";
10
13
  var TIMEOUT_MESSAGE = "Kiro authentication timed out. Run `kiro-cli login` manually, then re-run `opencode auth login`.";
14
+ var CANCELLED_MESSAGE = "Kiro authentication was cancelled.";
15
+ var SUPERSEDED_MESSAGE = "Kiro CLI login superseded by a new login attempt.";
11
16
  var LOGIN_INSTRUCTIONS = "Complete Kiro authentication in the browser window that just opened (if no browser opened, run `kiro-cli login` in another terminal). Waiting for login...";
12
17
  var ALREADY_AUTHENTICATED_INSTRUCTIONS = "Already authenticated with Kiro CLI.";
13
18
  function createAuthResources() {
@@ -18,7 +23,7 @@ function createAuthResources() {
18
23
  disposeRegistration: void 0
19
24
  };
20
25
  }
21
- function releaseLoginResources(resources) {
26
+ function releaseLoginResources(resources, reason) {
22
27
  if (resources.pollTimer !== void 0) {
23
28
  clearTimeout(resources.pollTimer);
24
29
  resources.pollTimer = void 0;
@@ -30,7 +35,7 @@ function releaseLoginResources(resources) {
30
35
  if (resources.cancelPoll !== void 0) {
31
36
  const cancel = resources.cancelPoll;
32
37
  resources.cancelPoll = void 0;
33
- cancel();
38
+ cancel(reason);
34
39
  }
35
40
  }
36
41
  function kiroCredential() {
@@ -45,12 +50,15 @@ function kiroCredential() {
45
50
  function pollForLogin(verifyAuth, resources) {
46
51
  return new Promise((resolve, reject) => {
47
52
  const start = Date.now();
48
- resources.cancelPoll = () => {
49
- reject(new Error("Kiro authentication was cancelled."));
53
+ const cancel = (reason) => {
54
+ reject(reason ?? new Error(CANCELLED_MESSAGE));
50
55
  };
51
- const tick = () => {
56
+ resources.cancelPoll = cancel;
57
+ const tick = async () => {
52
58
  resources.pollTimer = void 0;
53
- if (verifyAuth().authenticated) {
59
+ const status = await verifyAuth();
60
+ if (resources.cancelPoll !== cancel) return;
61
+ if (status.authenticated) {
54
62
  resources.cancelPoll = void 0;
55
63
  releaseLoginResources(resources);
56
64
  resolve(kiroCredential());
@@ -62,14 +70,19 @@ function pollForLogin(verifyAuth, resources) {
62
70
  reject(new Error(TIMEOUT_MESSAGE));
63
71
  return;
64
72
  }
65
- resources.pollTimer = setTimeout(tick, POLL_INTERVAL_MS);
73
+ resources.pollTimer = setTimeout(() => {
74
+ void tick();
75
+ }, POLL_INTERVAL_MS);
66
76
  };
67
- resources.pollTimer = setTimeout(tick, POLL_INTERVAL_MS);
77
+ resources.pollTimer = setTimeout(() => {
78
+ void tick();
79
+ }, POLL_INTERVAL_MS);
68
80
  });
69
81
  }
70
82
  async function authorize(resources) {
71
- const { verifyAuth } = await import("kiro-acp-ai-provider");
72
- const status = verifyAuth();
83
+ const { verifyAuthAsync } = await import("kiro-acp-ai-provider");
84
+ const { execFile } = await import("child_process");
85
+ const status = await verifyAuthAsync();
73
86
  if (!status.installed) throw new Error(NOT_INSTALLED_MESSAGE);
74
87
  if (status.authenticated) {
75
88
  return {
@@ -79,16 +92,16 @@ async function authorize(resources) {
79
92
  callback: Promise.resolve(kiroCredential())
80
93
  };
81
94
  }
82
- const { execFile } = await import("child_process");
95
+ if (resources.cancelPoll !== void 0) {
96
+ releaseLoginResources(resources, new Error(SUPERSEDED_MESSAGE));
97
+ }
83
98
  resources.child = execFile("kiro-cli", ["login"], {
84
99
  shell: process.platform === "win32"
85
100
  });
86
- return {
87
- url: KIRO_DOCS_URL,
88
- instructions: LOGIN_INSTRUCTIONS,
89
- mode: "auto",
90
- callback: pollForLogin(verifyAuth, resources)
91
- };
101
+ const callback = pollForLogin(verifyAuthAsync, resources);
102
+ callback.catch(() => {
103
+ });
104
+ return { url: KIRO_DOCS_URL, instructions: LOGIN_INSTRUCTIONS, mode: "auto", callback };
92
105
  }
93
106
  async function registerAuth(context, resources) {
94
107
  const registration = await context.integration.transform((draft) => {
@@ -120,6 +133,25 @@ var KIRO_PROVIDER_PACKAGE = "aisdk:kiro-acp-ai-provider";
120
133
  function asString(value) {
121
134
  return value;
122
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
+ }
123
155
  function createDiscoveryResources() {
124
156
  return {
125
157
  state: void 0,
@@ -184,41 +216,95 @@ function applyCatalogSnapshot(draft, state) {
184
216
  provider.settings = {
185
217
  ...provider.settings,
186
218
  cwd: state.cwd,
187
- agent: "opencode",
219
+ agent: state.options.agent,
188
220
  trustAllTools: true,
189
- mcpTimeout: 45,
221
+ // intentionally not exposed as a plugin option
222
+ mcpTimeout: state.options.mcpTimeout,
223
+ ...state.options.stall !== void 0 ? { stall: state.options.stall } : {},
190
224
  contextWindows
191
225
  };
192
226
  });
193
227
  }
194
228
  function createDiscover(context, state) {
195
- async function runDiscovery(gen) {
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) {
196
259
  try {
197
260
  const { listModels } = await import("kiro-acp-ai-provider");
198
- const discovered = await listModels({ cwd: state.cwd });
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) {
199
265
  if (gen !== state.generation) return;
200
- const uniqueIds = new Set(discovered.map((model) => model.modelId));
201
- if (uniqueIds.size !== discovered.length) return;
202
- state.snapshot = discovered;
203
- await context.catalog.reload();
204
- } catch {
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);
205
272
  }
206
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
+ }
207
296
  return async function discover(_reason) {
208
297
  const active = await context.integration.connection.active(KIRO_INTEGRATION_ID);
209
298
  if (!active) {
299
+ cancelRetry(state);
210
300
  state.generation += 1;
211
301
  state.snapshot = void 0;
212
302
  await context.catalog.reload();
213
303
  return;
214
304
  }
215
305
  if (state.inflight !== void 0) return state.inflight;
216
- const gen = ++state.generation;
217
- const run = runDiscovery(gen).finally(() => {
218
- if (state.inflight === run) state.inflight = void 0;
219
- });
220
- state.inflight = run;
221
- return run;
306
+ cancelRetry(state);
307
+ return start(0);
222
308
  };
223
309
  }
224
310
  function isKiroCredentialEvent(event) {
@@ -239,22 +325,23 @@ async function consumeEvents(iterator, discover) {
239
325
  if (result.done) return;
240
326
  const event = result.value;
241
327
  if (isKiroCredentialEvent(event)) {
242
- try {
243
- await discover(event.type);
244
- } catch {
245
- }
328
+ void discover(event.type).catch(() => {
329
+ });
246
330
  }
247
331
  }
248
332
  } catch {
249
333
  }
250
334
  }
251
- async function registerDiscovery(context, resources) {
335
+ async function registerDiscovery(context, resources, options) {
252
336
  const { location } = await context.integration.list();
253
337
  const state = {
254
338
  cwd: location.directory,
339
+ options,
255
340
  snapshot: void 0,
256
341
  generation: 0,
257
- inflight: void 0
342
+ inflight: void 0,
343
+ probeTimer: void 0,
344
+ retryTimer: void 0
258
345
  };
259
346
  resources.state = state;
260
347
  const discover = createDiscover(context, state);
@@ -265,12 +352,13 @@ async function registerDiscovery(context, resources) {
265
352
  const iterator = context.event.subscribe()[Symbol.asyncIterator]();
266
353
  resources.eventIterator = iterator;
267
354
  resources.eventTask = consumeEvents(iterator, discover);
268
- if (await context.integration.connection.active(KIRO_INTEGRATION_ID)) {
355
+ if (options.discover && await context.integration.connection.active(KIRO_INTEGRATION_ID)) {
269
356
  void discover("setup").catch(() => {
270
357
  });
271
358
  }
272
359
  return async () => {
273
360
  state.generation += 1;
361
+ clearDiscoveryTimers(state);
274
362
  const errors = [];
275
363
  const eventIterator = resources.eventIterator;
276
364
  resources.eventIterator = void 0;
@@ -299,16 +387,39 @@ async function registerDiscovery(context, resources) {
299
387
 
300
388
  // src/server/aisdk.ts
301
389
  var KIRO_SDK_PACKAGE = "kiro-acp-ai-provider";
390
+ var CLIENT_INFO = { name: "opencode-kiro", version };
302
391
  function createAisdkResources() {
303
392
  return {
304
393
  ownedSdks: /* @__PURE__ */ new Set(),
305
394
  cache: /* @__PURE__ */ new Map(),
306
- disposeHook: void 0
395
+ disposeHooks: []
307
396
  };
308
397
  }
309
398
  function isKiroPackage(pkg) {
310
399
  return pkg === KIRO_SDK_PACKAGE || pkg === `aisdk:${KIRO_SDK_PACKAGE}`;
311
400
  }
401
+ var SETTINGS_ALLOWLIST = [
402
+ "cwd",
403
+ "model",
404
+ "agent",
405
+ "trustAllTools",
406
+ "agentPrompt",
407
+ "env",
408
+ "clientInfo",
409
+ "sessionId",
410
+ "contextWindow",
411
+ "contextWindows",
412
+ "mcpTimeout",
413
+ "stall"
414
+ ];
415
+ function sanitizeSettings(options) {
416
+ const settings = {};
417
+ for (const key of SETTINGS_ALLOWLIST) {
418
+ const value = options[key];
419
+ if (value !== void 0) settings[key] = value;
420
+ }
421
+ return settings;
422
+ }
312
423
  function isJsonSafe(value) {
313
424
  if (value === null) return true;
314
425
  const kind = typeof value;
@@ -329,19 +440,20 @@ function stableStringify(value) {
329
440
  }
330
441
  return JSON.stringify(value);
331
442
  }
332
- function stableOptionsKey(options) {
333
- return isJsonSafe(options) ? stableStringify(options) : void 0;
443
+ function stableSettingsKey(settings) {
444
+ return isJsonSafe(settings) ? stableStringify(settings) : void 0;
334
445
  }
335
446
  async function registerAisdkHook(context, resources) {
336
- const registration = await context.aisdk.hook(
447
+ const sdkRegistration = await context.aisdk.hook(
337
448
  "sdk",
338
449
  async (event) => {
339
450
  if (!isKiroPackage(event.package)) return;
340
- const { createKiroAcp } = await import("kiro-acp-ai-provider");
341
- const key = stableOptionsKey(event.options);
451
+ const settings = { ...sanitizeSettings(event.options), clientInfo: CLIENT_INFO };
452
+ const key = stableSettingsKey(settings);
342
453
  let sdk = key === void 0 ? void 0 : resources.cache.get(key);
343
454
  if (sdk === void 0) {
344
- sdk = createKiroAcp(event.options);
455
+ const { createKiroAcp } = await import("kiro-acp-ai-provider");
456
+ sdk = createKiroAcp(settings);
345
457
  resources.ownedSdks.add(sdk);
346
458
  if (key !== void 0) resources.cache.set(key, sdk);
347
459
  }
@@ -349,14 +461,34 @@ async function registerAisdkHook(context, resources) {
349
461
  },
350
462
  { providerID: KIRO_PROVIDER_ID }
351
463
  );
352
- resources.disposeHook = registration.dispose;
464
+ resources.disposeHooks.push(sdkRegistration.dispose);
465
+ let languageRegistration;
466
+ try {
467
+ languageRegistration = await context.aisdk.hook(
468
+ "language",
469
+ async (event) => {
470
+ const effort = typeof event.options.effort === "string" ? event.options.effort : void 0;
471
+ const overrides = effort === void 0 ? void 0 : { effort };
472
+ event.language = event.sdk.languageModel(String(event.model.modelID), overrides);
473
+ },
474
+ { providerID: KIRO_PROVIDER_ID }
475
+ );
476
+ } catch (error) {
477
+ await sdkRegistration.dispose().catch(() => {
478
+ });
479
+ resources.disposeHooks.length = 0;
480
+ throw error;
481
+ }
482
+ resources.disposeHooks.push(languageRegistration.dispose);
353
483
  return async () => {
354
484
  const errors = [];
355
- resources.disposeHook = void 0;
356
- try {
357
- await registration.dispose();
358
- } catch (error) {
359
- errors.push(error);
485
+ const disposeHooks = resources.disposeHooks.splice(0);
486
+ for (const dispose of disposeHooks) {
487
+ try {
488
+ await dispose();
489
+ } catch (error) {
490
+ errors.push(error);
491
+ }
360
492
  }
361
493
  const owned = Array.from(resources.ownedSdks);
362
494
  resources.ownedSdks.clear();
@@ -405,17 +537,35 @@ function buildCleanup(state) {
405
537
  }
406
538
 
407
539
  // src/server.ts
540
+ function resolveOptions(raw) {
541
+ const stall = resolveStall(raw.stall);
542
+ return {
543
+ agent: typeof raw.agent === "string" && raw.agent !== "" ? raw.agent : "opencode",
544
+ mcpTimeout: typeof raw.mcpTimeout === "number" && Number.isFinite(raw.mcpTimeout) && raw.mcpTimeout > 0 ? raw.mcpTimeout : 45,
545
+ discover: typeof raw.discover === "boolean" ? raw.discover : true,
546
+ ...stall !== void 0 ? { stall } : {}
547
+ };
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
+ }
408
557
  var plugin = {
409
558
  id: "kiro",
410
- // tui: true (dist/promise/plugin.d.ts:40) — the host auto-loads this
559
+ // tui: true (dist/promise/plugin.d.ts) — the host auto-loads this
411
560
  // package's `./tui` entrypoint for npm-channel installs (single config entry)
412
561
  tui: true,
413
562
  async setup(context) {
563
+ const options = resolveOptions(context.options ?? {});
414
564
  const state = createServerState();
415
565
  const cleanup = buildCleanup(state);
416
566
  try {
417
567
  state.disposers.push(await registerAuth(context, state.auth));
418
- state.disposers.push(await registerDiscovery(context, state.discovery));
568
+ state.disposers.push(await registerDiscovery(context, state.discovery, options));
419
569
  state.disposers.push(await registerAisdkHook(context, state.aisdk));
420
570
  } catch (error) {
421
571
  await Promise.resolve(cleanup()).catch(() => {
package/dist/tui.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
- messageCredits,
3
- readCreditState
4
- } from "./chunk-M7OUVABX.js";
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 credits = readCreditState(state);
30
- if (!credits) return;
31
- store.entries.set(transientKey(sessionID, assistantMessageID, ordinal), credits);
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
- let best;
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
- if (parsed[2] > bestOrdinal) {
40
- bestOrdinal = parsed[2];
41
- best = value;
42
- }
39
+ entries.push([parsed[2], value]);
43
40
  }
44
- return best;
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 = messageCredits(message.content ?? []);
50
- const hit = durable ?? transientForMessage(store, sessionID, message.id);
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 (messageCredits(message.content ?? []) !== void 0) store.entries.delete(key);
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-GLFJSV6M.js"),
113
- import("./credits-chip-view-YP4RPX26.js"),
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 unsubscribeTextEnded = context.data.on("session.text.ended", (event) => {
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(unsubscribeTextEnded);
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",
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)",
@@ -38,12 +38,15 @@
38
38
  "engines": {
39
39
  "node": ">=20"
40
40
  },
41
+ "publishConfig": {
42
+ "tag": "beta"
43
+ },
41
44
  "peerDependencies": {
42
45
  "@opencode-ai/plugin": "0.0.0-dev-18686"
43
46
  },
44
47
  "dependencies": {
45
48
  "@opentui/solid": "0.5.9",
46
- "kiro-acp-ai-provider": "3.0.0",
49
+ "kiro-acp-ai-provider": "3.2.0",
47
50
  "solid-js": "1.9.12"
48
51
  },
49
52
  "devDependencies": {
@@ -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
- };