@lawaty/lacode 1.3.0-beta.0 → 1.3.0-beta.2

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/dist/agents.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/lib/pool.ts
2
- import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join as join2 } from "node:path";
5
5
 
package/dist/commands.js CHANGED
@@ -3,7 +3,7 @@ var AGENT_INSTRUCTIONS_COMMAND = "agent-instructions";
3
3
  var AGENT_INSTRUCTIONS_WRITER = "instructions";
4
4
 
5
5
  // src/lib/pool.ts
6
- import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
7
  import { homedir } from "node:os";
8
8
  import { join as join2 } from "node:path";
9
9
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/lib/pool.ts
2
- import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join as join2 } from "node:path";
5
5
 
@@ -799,6 +799,56 @@ function coolProvider(s, provider, until, reason) {
799
799
  }
800
800
  return affected;
801
801
  }
802
+ function recordFailure(s, f) {
803
+ try {
804
+ const path5 = join2(s.dir, "failures.jsonl");
805
+ const trimSize = 64 * 1024;
806
+ const keepLines = 200;
807
+ if (existsSync(path5)) {
808
+ const st = statSync(path5);
809
+ if (typeof st.size === "number" && st.size > trimSize) {
810
+ try {
811
+ const content = readFileSync2(path5, "utf8");
812
+ const parts = content.split("\n");
813
+ const lines = parts.filter((line) => line.length > 0);
814
+ if (lines.length > keepLines) {
815
+ const kept = lines.slice(lines.length - keepLines);
816
+ writeFileSync(`${path5}.tmp`, kept.join("\n") + "\n");
817
+ renameSync(`${path5}.tmp`, path5);
818
+ } else if (lines.length === 0 && parts.length >= 1) {
819
+ }
820
+ } catch {
821
+ }
822
+ }
823
+ }
824
+ appendFileSync(path5, JSON.stringify(f) + "\n");
825
+ } catch (error) {
826
+ console.error("[agent-pool] failed to record failure:", String(error));
827
+ }
828
+ }
829
+ function readFailures(s, limit) {
830
+ const path5 = join2(s.dir, "failures.jsonl");
831
+ try {
832
+ if (!existsSync(path5)) return [];
833
+ const content = readFileSync2(path5, "utf8");
834
+ const parts = content.split("\n");
835
+ const records = [];
836
+ for (let i = 0; i < parts.length; i++) {
837
+ const line = parts[i];
838
+ if (line.length === 0) continue;
839
+ try {
840
+ const parsed = JSON.parse(line);
841
+ records.push(parsed);
842
+ } catch {
843
+ }
844
+ }
845
+ if (limit <= 0) return [];
846
+ if (records.length <= limit) return records;
847
+ return records.slice(records.length - limit);
848
+ } catch {
849
+ return [];
850
+ }
851
+ }
802
852
 
803
853
  // src/lib/agent-instructions.ts
804
854
  var AGENT_INSTRUCTIONS_COMMAND = "agent-instructions";
@@ -2545,6 +2595,8 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
2545
2595
  }
2546
2596
  };
2547
2597
  const abortReports = /* @__PURE__ */ new Map();
2598
+ const spawns = /* @__PURE__ */ new Map();
2599
+ const observedErrors = /* @__PURE__ */ new Map();
2548
2600
  const abortKind = (reason) => isQuotaReason(reason) || isLimitText(reason) ? "rate-limit" : "provider error";
2549
2601
  const abortLabel = (kind, elapsedMs) => {
2550
2602
  const seconds = Math.max(1, Math.round(elapsedMs / 1e3));
@@ -2565,13 +2617,29 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
2565
2617
  if (oldest !== void 0) patchedCancelledParts.delete(oldest);
2566
2618
  }
2567
2619
  const error = `${cancelledPrefix} ${report.message}`;
2620
+ const updated = { ...part, state: { ...part.state, error } };
2568
2621
  try {
2622
+ const updater = client.part?.update;
2623
+ if (typeof updater === "function") {
2624
+ const result = await updater({
2625
+ sessionID: part.sessionID,
2626
+ messageID: part.messageID,
2627
+ partID: part.id,
2628
+ directory,
2629
+ part: updated,
2630
+ signal: AbortSignal.timeout(5e3)
2631
+ });
2632
+ if (!result?.response?.ok) throw new Error(`server answered HTTP ${result?.response?.status ?? "?"}`);
2633
+ abortReports.delete(key);
2634
+ return;
2635
+ }
2569
2636
  const url = new URL(
2570
2637
  `/session/${encodeURIComponent(part.sessionID)}/message/${encodeURIComponent(part.messageID)}/part/${encodeURIComponent(part.id)}`,
2571
2638
  serverUrl
2572
2639
  );
2573
2640
  if (directory) url.searchParams.set("directory", directory);
2574
2641
  const headers = { "Content-Type": "application/json" };
2642
+ if (directory) headers["x-opencode-directory"] = directory;
2575
2643
  const password = process.env.OPENCODE_SERVER_PASSWORD;
2576
2644
  if (password) {
2577
2645
  const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
@@ -3051,6 +3119,26 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
3051
3119
  }
3052
3120
  if (!limited2 && !outage && !auth2) {
3053
3121
  if (own) {
3122
+ const detail = haystack2.slice(0, 400);
3123
+ recordFailure(s, {
3124
+ ts: Date.now(),
3125
+ source: "observed",
3126
+ base: own.claim.v,
3127
+ slot: own.claim.k,
3128
+ model: own.claim.m,
3129
+ session: sessionID,
3130
+ reason: "provider error",
3131
+ elapsedMs: Date.now() - own.claim.t,
3132
+ released: true,
3133
+ detail
3134
+ });
3135
+ if (!observedErrors.has(own.key)) {
3136
+ observedErrors.set(own.key, { reason: "provider error", detail });
3137
+ if (observedErrors.size > 32) {
3138
+ const oldest = observedErrors.keys().next().value;
3139
+ if (oldest !== void 0) observedErrors.delete(oldest);
3140
+ }
3141
+ }
3054
3142
  release(s, own.key);
3055
3143
  await log("warn", `released ${own.claim.v} claim on ${own.claim.m} after a terminal session error`, {
3056
3144
  base: own.claim.v,
@@ -3122,6 +3210,21 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
3122
3210
  const report = abortReports.get(key);
3123
3211
  if (report) {
3124
3212
  await patchCancelledPart(full, key, report);
3213
+ } else if (observedErrors.has(key)) {
3214
+ const spawn3 = spawns.get(key);
3215
+ const seen = observedErrors.get(key);
3216
+ const elapsedMs = spawn3 ? Date.now() - spawn3.t : 0;
3217
+ const failureReport = {
3218
+ slot: spawn3?.slot ?? 0,
3219
+ model: spawn3?.model ?? "?",
3220
+ elapsedMs,
3221
+ kind: seen.reason,
3222
+ released: true,
3223
+ message: `${spawn3?.model ?? "?"} \u2014 ${seen.reason}${seen.detail ? `: ${seen.detail.slice(0, 200)}` : ""}`
3224
+ };
3225
+ await patchCancelledPart(full, key, failureReport);
3226
+ observedErrors.delete(key);
3227
+ spawns.delete(key);
3125
3228
  } else {
3126
3229
  pendingCancelledParts.set(key, full);
3127
3230
  if (pendingCancelledParts.size > 32) {
@@ -3142,7 +3245,31 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
3142
3245
  const gateway5xx = status !== void 0 && status >= 500 && status <= 599;
3143
3246
  const transientGateway = gateway5xx || isTransientGatewayText(haystack);
3144
3247
  const missing = isModelMissingText(haystack);
3145
- if (!limited && !transientGateway && !auth && !missing) return;
3248
+ if (!limited && !transientGateway && !auth && !missing) {
3249
+ const ownMsgUnclassified = claimFor(info.sessionID);
3250
+ if (ownMsgUnclassified) {
3251
+ recordFailure(s, {
3252
+ ts: Date.now(),
3253
+ source: "observed",
3254
+ base: ownMsgUnclassified.claim.v,
3255
+ slot: ownMsgUnclassified.claim.k,
3256
+ model: ownMsgUnclassified.claim.m,
3257
+ session: info.sessionID,
3258
+ reason: "provider error",
3259
+ elapsedMs: Date.now() - ownMsgUnclassified.claim.t,
3260
+ released: false,
3261
+ detail: haystack.slice(0, 400)
3262
+ });
3263
+ if (!observedErrors.has(ownMsgUnclassified.key)) {
3264
+ observedErrors.set(ownMsgUnclassified.key, { reason: "provider error", detail: haystack.slice(0, 400) });
3265
+ if (observedErrors.size > 32) {
3266
+ const oldest = observedErrors.keys().next().value;
3267
+ if (oldest !== void 0) observedErrors.delete(oldest);
3268
+ }
3269
+ }
3270
+ }
3271
+ return;
3272
+ }
3146
3273
  const model = `${info.providerID ?? "?"}/${info.modelID ?? "?"}`;
3147
3274
  if (info.sessionID && isProbe(s, info.sessionID)) {
3148
3275
  await log("debug", `probe of ${model} hit a limit (${isLimitText(haystack) ? "limit" : "transient"}); leaving the cooldown to the probe`);
@@ -3225,7 +3352,9 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
3225
3352
  load: (snap.counts.get(slot.index) ?? 0) + 1
3226
3353
  });
3227
3354
  output.args.subagent_type = variant;
3228
- acquire(s, requested, slot, input.sessionID, claimKey(input.sessionID, input.callID));
3355
+ const routeKey = claimKey(input.sessionID, input.callID);
3356
+ acquire(s, requested, slot, input.sessionID, routeKey);
3357
+ spawns.set(routeKey, { base: requested, slot: slot.index, model: slot.model, t: Date.now() });
3229
3358
  return;
3230
3359
  }
3231
3360
  const pinned = s.models.get(requested);
@@ -3242,15 +3371,30 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
3242
3371
  "tool.execute.after": async (input, output) => {
3243
3372
  if (input.tool !== "task") return;
3244
3373
  const key = claimKey(input.sessionID, input.callID);
3374
+ const spawn3 = spawns.get(key);
3245
3375
  release(s, key);
3376
+ const text = typeof output.output === "string" ? output.output : "";
3377
+ const failed = abortReports.has(key) || /task\s+cancel+ed/i.test(text);
3378
+ spawns.delete(key);
3246
3379
  const report = abortReports.get(key);
3247
3380
  if (!report) return;
3248
3381
  abortReports.delete(key);
3382
+ recordFailure(s, {
3383
+ ts: Date.now(),
3384
+ source: "pool",
3385
+ base: void 0,
3386
+ slot: report.slot,
3387
+ model: report.model,
3388
+ session: input.sessionID,
3389
+ reason: report.kind,
3390
+ elapsedMs: report.elapsedMs,
3391
+ released: report.released,
3392
+ detail: report.message
3393
+ });
3249
3394
  const detail = [
3250
3395
  `Task Cancelled by LaCode: ${report.message}`,
3251
3396
  `slot ${report.slot}; model ${report.model}; elapsed ${report.elapsedMs}ms; failure ${report.kind}; claim ${report.released ? "released" : "held"}`
3252
3397
  ].join("\n");
3253
- const text = typeof output.output === "string" ? output.output : "";
3254
3398
  output.output = /task\s+cancel+ed/i.test(text) ? detail : text ? `${text}
3255
3399
 
3256
3400
  ${detail}` : detail;
@@ -3339,7 +3483,38 @@ one ordered pass in slot order: each slot fills to its weight, then overflow goe
3339
3483
  // different situations and this is the line that tells them apart.
3340
3484
  probeMs > 0 ? `cooldown probe: last ${s.lastProbe ? `${Math.max(0, Math.round((now - s.lastProbe) / 1e3))}s ago` : coolingNow.length > 0 ? "never in this process (cooldowns came from disk)" : "never (nothing has been cooling)"}, every ${Math.round(probeMs / 6e4)}m; call the pool_recheck tool (or run /pool-recheck) to ask now.` : "cooldown probe: off (probeMs: 0); call the pool_recheck tool (or run /pool-recheck) to ask now.",
3341
3485
  watchMs > 0 ? `pool config watch: ${sources.join(", ")} every ${watchMs}ms; a change reloads the slot table and validates every slot.` : "pool config watch: off (watchMs: 0).",
3342
- coolingNow.length === 0 ? "no models cooling down." : `unavailable/cooling: ${coolingNow.map(([m, st]) => `${m} for ${Math.ceil((st.until - now) / 1e3)}s`).join(", ")}`
3486
+ coolingNow.length === 0 ? "no models cooling down." : `unavailable/cooling: ${coolingNow.map(([m, st]) => `${m} for ${Math.ceil((st.until - now) / 1e3)}s`).join(", ")}`,
3487
+ (() => {
3488
+ const recent = readFailures(s, 5);
3489
+ if (recent.length === 0) {
3490
+ return "recent failures: none recorded";
3491
+ }
3492
+ const all = readFailures(s, 1e3);
3493
+ const total2 = all.length;
3494
+ const fmtAge = (ts) => {
3495
+ const diff = Math.max(0, Date.now() - ts);
3496
+ const sAge = Math.floor(diff / 1e3);
3497
+ if (sAge < 60) return `${sAge}s`;
3498
+ const mAge = Math.floor(sAge / 60);
3499
+ if (mAge < 60) return `${mAge}m`;
3500
+ const hAge = Math.floor(mAge / 60);
3501
+ if (hAge < 24) return `${hAge}h`;
3502
+ const dAge = Math.floor(hAge / 24);
3503
+ return `${dAge}d`;
3504
+ };
3505
+ const lines = [`recent failures: ${total2} recorded in ${s.dir}/failures.jsonl`];
3506
+ const ordered = [...recent].reverse();
3507
+ for (const rec of ordered) {
3508
+ const age = fmtAge(rec.ts);
3509
+ const slot = rec.slot === void 0 || rec.slot === null ? "-" : rec.slot;
3510
+ const model = rec.model;
3511
+ const reason = rec.reason;
3512
+ const source = rec.source;
3513
+ const suffix = source === "observed" ? " (unclassified)" : "";
3514
+ lines.push(` ${age} ago slot ${slot} ${model} ${reason} [${source}]${suffix}`);
3515
+ }
3516
+ return lines.join("\n");
3517
+ })()
3343
3518
  ].join("\n")
3344
3519
  };
3345
3520
  }
@@ -6160,7 +6335,7 @@ import { writeFileSync as writeFileSync8, renameSync as renameSync6, rmSync as r
6160
6335
  import { tool as tool2 } from "@opencode-ai/plugin";
6161
6336
 
6162
6337
  // src/lib/timeline.ts
6163
- import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync10, rmSync as rmSync2 } from "node:fs";
6338
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync10, rmSync as rmSync2 } from "node:fs";
6164
6339
  import { homedir as homedir7 } from "node:os";
6165
6340
  import { dirname as dirname5, join as join11, resolve as resolve4 } from "node:path";
6166
6341
  function timelineDir() {
@@ -6209,7 +6384,7 @@ function appendEntriesAt(file, texts, now, through) {
6209
6384
  const ts = now ?? Date.now();
6210
6385
  const line = { ts, text: t };
6211
6386
  if (validThrough) line.through = validThrough;
6212
- appendFileSync(file, JSON.stringify(line) + "\n");
6387
+ appendFileSync2(file, JSON.stringify(line) + "\n");
6213
6388
  count++;
6214
6389
  }
6215
6390
  return count;
package/dist/lacode.js CHANGED
@@ -7,7 +7,7 @@ import { spawnSync as spawnSync3 } from "node:child_process";
7
7
 
8
8
  // src/tunnel/index.ts
9
9
  import { spawn, spawnSync } from "node:child_process";
10
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "node:fs";
10
+ import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "node:fs";
11
11
  import { homedir as homedir3 } from "node:os";
12
12
  import { join as join4 } from "node:path";
13
13
 
@@ -17,7 +17,7 @@ import { homedir as homedir2 } from "node:os";
17
17
  import { basename, join as join3 } from "node:path";
18
18
 
19
19
  // src/lib/pool.ts
20
- import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
20
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
21
21
  import { homedir } from "node:os";
22
22
  import { join as join2 } from "node:path";
23
23
 
@@ -264,7 +264,7 @@ var recordNotificationId = (correlationKey, id) => {
264
264
  if (!isNotificationId(id)) return false;
265
265
  try {
266
266
  mkdirSync3(notificationStateDir(), { recursive: true });
267
- appendFileSync(notificationIdFile(correlationKey), `${id}
267
+ appendFileSync2(notificationIdFile(correlationKey), `${id}
268
268
  `, "utf8");
269
269
  return true;
270
270
  } catch {
package/dist/pool.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/lib/pool.ts
2
- import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join as join2 } from "node:path";
5
5
 
@@ -802,6 +802,56 @@ function coolProvider(s, provider, until, reason) {
802
802
  }
803
803
  return affected;
804
804
  }
805
+ function recordFailure(s, f) {
806
+ try {
807
+ const path = join2(s.dir, "failures.jsonl");
808
+ const trimSize = 64 * 1024;
809
+ const keepLines = 200;
810
+ if (existsSync(path)) {
811
+ const st = statSync(path);
812
+ if (typeof st.size === "number" && st.size > trimSize) {
813
+ try {
814
+ const content = readFileSync2(path, "utf8");
815
+ const parts = content.split("\n");
816
+ const lines = parts.filter((line) => line.length > 0);
817
+ if (lines.length > keepLines) {
818
+ const kept = lines.slice(lines.length - keepLines);
819
+ writeFileSync(`${path}.tmp`, kept.join("\n") + "\n");
820
+ renameSync(`${path}.tmp`, path);
821
+ } else if (lines.length === 0 && parts.length >= 1) {
822
+ }
823
+ } catch {
824
+ }
825
+ }
826
+ }
827
+ appendFileSync(path, JSON.stringify(f) + "\n");
828
+ } catch (error) {
829
+ console.error("[agent-pool] failed to record failure:", String(error));
830
+ }
831
+ }
832
+ function readFailures(s, limit) {
833
+ const path = join2(s.dir, "failures.jsonl");
834
+ try {
835
+ if (!existsSync(path)) return [];
836
+ const content = readFileSync2(path, "utf8");
837
+ const parts = content.split("\n");
838
+ const records = [];
839
+ for (let i = 0; i < parts.length; i++) {
840
+ const line = parts[i];
841
+ if (line.length === 0) continue;
842
+ try {
843
+ const parsed = JSON.parse(line);
844
+ records.push(parsed);
845
+ } catch {
846
+ }
847
+ }
848
+ if (limit <= 0) return [];
849
+ if (records.length <= limit) return records;
850
+ return records.slice(records.length - limit);
851
+ } catch {
852
+ return [];
853
+ }
854
+ }
805
855
  export {
806
856
  AUTH_FAILURE_COOLDOWN_MS,
807
857
  AUTH_FAILURE_REASON,
@@ -869,9 +919,11 @@ export {
869
919
  pruneOwn,
870
920
  pruneSessions,
871
921
  publish,
922
+ readFailures,
872
923
  readLimits,
873
924
  readSiblings,
874
925
  readSlotFile,
926
+ recordFailure,
875
927
  recordStrike,
876
928
  refreshCatalog,
877
929
  release,
package/dist/tui.js CHANGED
@@ -85,17 +85,17 @@ function createRecorder(deps) {
85
85
  const child = childHandle;
86
86
  const filePath = currentPath;
87
87
  if (!exited) {
88
- await new Promise((resolve) => {
88
+ await new Promise((resolve2) => {
89
89
  let done = false;
90
90
  let timer;
91
91
  const finish = () => {
92
92
  if (done) return;
93
93
  done = true;
94
94
  if (timer) clearTimeout(timer);
95
- if (resolveWait === resolve) resolveWait = null;
96
- resolve();
95
+ if (resolveWait === resolve2) resolveWait = null;
96
+ resolve2();
97
97
  };
98
- resolveWait = resolve;
98
+ resolveWait = resolve2;
99
99
  timer = setTimeout(() => {
100
100
  try {
101
101
  child.kill("SIGKILL");
@@ -821,6 +821,68 @@ function createVoiceClient(options, deps) {
821
821
  return client;
822
822
  }
823
823
 
824
+ // src/tui/timeline.tsx
825
+ import { createSignal, createEffect, onCleanup, For } from "solid-js";
826
+ import { readFileSync as readFileSync3 } from "node:fs";
827
+ import { homedir as homedir2 } from "node:os";
828
+ import { join as join3, resolve } from "node:path";
829
+ import { jsx, jsxs } from "@opentui/solid/jsx-runtime";
830
+ var registered = false;
831
+ var tui = async (api) => {
832
+ if (registered) return;
833
+ registered = true;
834
+ api.slots.register({
835
+ order: 150,
836
+ slots: {
837
+ sidebar_content(ctx, props) {
838
+ const [entries, setEntries] = createSignal([]);
839
+ const read = (id) => {
840
+ if (typeof id !== "string" || !/^[A-Za-z0-9_.-]+$/.test(id)) {
841
+ setEntries([]);
842
+ return;
843
+ }
844
+ const rawDir = process.env.LACODE_TIMELINE_DIR;
845
+ const dir = rawDir ? resolve(rawDir) : join3(homedir2(), ".local/share/opencode/lacode/timeline");
846
+ const file = join3(dir, id + ".jsonl");
847
+ try {
848
+ const raw = readFileSync3(file, "utf8");
849
+ const parsed = [];
850
+ for (const line of raw.split("\n")) {
851
+ const trimmed = line.trim();
852
+ if (!trimmed) continue;
853
+ try {
854
+ const obj = JSON.parse(trimmed);
855
+ if (obj && typeof obj.ts === "number" && typeof obj.text === "string") {
856
+ parsed.push(obj.text);
857
+ }
858
+ } catch {
859
+ }
860
+ }
861
+ setEntries(parsed.slice(-14).reverse());
862
+ } catch {
863
+ setEntries([]);
864
+ }
865
+ };
866
+ createEffect(() => {
867
+ const id = props.session_id;
868
+ read(id);
869
+ const timer = setInterval(() => read(id), 1e3);
870
+ onCleanup(() => clearInterval(timer));
871
+ });
872
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
873
+ /* @__PURE__ */ jsx("text", { fg: ctx.theme.current.textMuted, children: "Timeline" }),
874
+ entries().length === 0 ? /* @__PURE__ */ jsx("text", { fg: ctx.theme.current.textMuted, children: "No timeline yet" }) : /* @__PURE__ */ jsx(For, { each: entries(), children: (text) => /* @__PURE__ */ jsx("text", { children: `\u2022 ${text}` }) })
875
+ ] });
876
+ }
877
+ }
878
+ });
879
+ };
880
+ var plugin = {
881
+ id: "lacode.timeline",
882
+ tui
883
+ };
884
+ var timeline_default = plugin;
885
+
824
886
  // src/tui/index.ts
825
887
  var DEFAULT_MODEL = "whisper-large-v3-turbo";
826
888
  var DEFAULT_LANGUAGE = "en";
@@ -1049,7 +1111,7 @@ async function answerWithText(api, opts, text, request) {
1049
1111
  const labels = answers.map((answer) => answer.join(", ")).filter((answer) => answer.length > 0).join("; ");
1050
1112
  toast(api, { variant: "success", message: labels });
1051
1113
  }
1052
- var tui = async (api, options) => {
1114
+ var tui2 = async (api, options) => {
1053
1115
  const opts = resolveOptions(options);
1054
1116
  if (!opts.enabled) return;
1055
1117
  daemonConnected = false;
@@ -1271,10 +1333,18 @@ var tui = async (api, options) => {
1271
1333
  bindings.push({ key: opts.answerKey, cmd: "lacode.voice.answer", desc: "Voice answer" });
1272
1334
  }
1273
1335
  api.keymap.registerLayer({ mode: "base", commands, bindings });
1336
+ const timelineOpts = options ?? {};
1337
+ const timelineOff = timelineOpts.timeline === false || typeof timelineOpts.timeline === "object" && timelineOpts.timeline !== null && timelineOpts.timeline.enabled === false;
1338
+ if (!timelineOff) {
1339
+ try {
1340
+ await timeline_default.tui(api, options);
1341
+ } catch {
1342
+ }
1343
+ }
1274
1344
  };
1275
1345
  var mod = {
1276
1346
  id: "lacode.voice",
1277
- tui
1347
+ tui: tui2
1278
1348
  };
1279
1349
  var index_default = mod;
1280
1350
  export {
package/dist/tunnel.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/tunnel/index.ts
2
2
  import { spawn, spawnSync } from "node:child_process";
3
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "node:fs";
3
+ import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "node:fs";
4
4
  import { homedir as homedir3 } from "node:os";
5
5
  import { join as join4 } from "node:path";
6
6
 
@@ -10,7 +10,7 @@ import { homedir as homedir2 } from "node:os";
10
10
  import { basename, join as join3 } from "node:path";
11
11
 
12
12
  // src/lib/pool.ts
13
- import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
13
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import { join as join2 } from "node:path";
16
16
 
@@ -257,7 +257,7 @@ var recordNotificationId = (correlationKey, id) => {
257
257
  if (!isNotificationId(id)) return false;
258
258
  try {
259
259
  mkdirSync3(notificationStateDir(), { recursive: true });
260
- appendFileSync(notificationIdFile(correlationKey), `${id}
260
+ appendFileSync2(notificationIdFile(correlationKey), `${id}
261
261
  `, "utf8");
262
262
  return true;
263
263
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawaty/lacode",
3
- "version": "1.3.0-beta.0",
3
+ "version": "1.3.0-beta.2",
4
4
  "description": "Ultimate opencode setup for Go users — cost-controlled pooled subagents, hang recovery, and notifications for opencode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -64,7 +64,17 @@
64
64
  "esbuild": "^0.28.2"
65
65
  },
66
66
  "peerDependencies": {
67
- "@opencode-ai/plugin": "1.18.34"
67
+ "@opencode-ai/plugin": "1.18.34",
68
+ "solid-js": "*",
69
+ "@opentui/solid": "*"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "solid-js": {
73
+ "optional": true
74
+ },
75
+ "@opentui/solid": {
76
+ "optional": true
77
+ }
68
78
  },
69
79
  "optionalDependencies": {
70
80
  "@picovoice/porcupine-node": "^4.0.2",