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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # opencode-kiro
2
2
 
3
- > ⚠️ **Experimental prerelease** — `0.5.0-beta.4` 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
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.4` |
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.4"]
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.4` spec.
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.4",
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.4",
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.4", "-kiro"]
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.4.tgz"] }
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.4`) use colon-free paths and are
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 — exact ID matching, catalog metadata preserved,
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.4` spec. |
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.4` into an OpenCode v1 setup.
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
- } 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,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.5.0-beta.4";
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 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) {
210
259
  try {
211
260
  const { listModels } = await import("kiro-acp-ai-provider");
212
- 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) {
213
265
  if (gen !== state.generation) return;
214
- const uniqueIds = new Set(discovered.map((model) => model.modelId));
215
- if (uniqueIds.size !== discovered.length) return;
216
- state.snapshot = discovered;
217
- await context.catalog.reload();
218
- } 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);
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
- const gen = ++state.generation;
231
- const run = runDiscovery(gen).finally(() => {
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
- try {
257
- await discover(event.type);
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
- 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.4",
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.1.0",
49
+ "kiro-acp-ai-provider": "3.2.0",
50
50
  "solid-js": "1.9.12"
51
51
  },
52
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
- };