@paradow/ledge 0.18.0 → 0.19.1

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
@@ -42,11 +42,17 @@ ledge claude # start Claude Code in a tracked tmux session (tab renam
42
42
  ledge claude --name max # pick the session's Ledge name
43
43
  ledge status # list bots and what they wait for
44
44
  ledge attach <id> # put this terminal on a running session (what the overlay opens a tab with)
45
- ledge tui # follow and answer every session from this terminal (--notify bell|osc9|off)
45
+ ledge tui # follow and answer every session (--notify bell|osc9|off); see below for Windows Terminal
46
46
  ledge daemon # run the daemon in the foreground (normally run by the systemd service)
47
47
  ledge uninstall # undo ledge setup: hooks, service and ~/.local/bin/ledge (--purge: ~/.ledge too)
48
48
  ```
49
49
 
50
+ From a WSL tab of Windows Terminal, `ledge tui` opens in a tab of its own with the Ledge mascot as its icon. A tab's
51
+ icon comes only from its profile, so Ledge installs a `Ledge` profile as a fragment under
52
+ `%LOCALAPPDATA%\Microsoft\Windows Terminal\Fragments\Ledge` (no admin right needed). That profile runs
53
+ `~/.ledge/tui.sh`, which each `ledge tui` rewrites with its node and CLI. `ledge tui --here`, tmux, SSH and any other
54
+ terminal keep the TUI where it was started.
55
+
50
56
  When a Claude it launched exits, Ledge appends a `custom-title` « name - topic » to its transcript, so `claude --resume` lists the conversation that way, unless the user named it (`claude -n`, `/rename`).
51
57
 
52
58
  `ledge setup` installs `ledge-daemon.service` as a systemd user unit, starts it and enables linger, so the daemon runs from WSL boot and restarts after a crash. Rerun it after a node upgrade or a repository move: the unit is rewritten and the service restarted. Useful commands:
Binary file
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  attachEnv
3
- } from "./chunk-QQZALYNI.js";
4
- import "./chunk-7IQ6FV2F.js";
3
+ } from "./chunk-LOOOHYZR.js";
4
+ import "./chunk-WP77O5LC.js";
5
5
  import "./chunk-3EIRKAX7.js";
6
6
  import "./chunk-OLLQBK6N.js";
7
7
  import "./chunk-KKNE7BFZ.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  EFFORT_ARGS,
3
3
  MODEL_ARGS
4
- } from "./chunk-7IQ6FV2F.js";
4
+ } from "./chunk-WP77O5LC.js";
5
5
  import {
6
6
  autonomyArgs,
7
7
  findRole
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var LEDGE_VERSION = true ? "0.19.1" : "0.0.0-dev";
3
+
4
+ export {
5
+ LEDGE_VERSION
6
+ };
@@ -855,7 +855,7 @@ var cursorAdapter = {
855
855
  };
856
856
 
857
857
  // src/adapters/preview.ts
858
- var PREVIEW_LINES = 30;
858
+ var PREVIEW_LINES = 100;
859
859
  var PREVIEW_CAPTURE_LINES = 200;
860
860
  var isBlank4 = (l) => l.trim() === "";
861
861
  function tidyLines(lines) {
@@ -7,7 +7,7 @@ import {
7
7
  startServer,
8
8
  tasksJournal,
9
9
  teamsJournal
10
- } from "./chunk-7IQ6FV2F.js";
10
+ } from "./chunk-WP77O5LC.js";
11
11
  import "./chunk-3EIRKAX7.js";
12
12
  import "./chunk-OLLQBK6N.js";
13
13
  import {
@@ -45,7 +45,7 @@ import {
45
45
  } from "./chunk-HP3G2TYJ.js";
46
46
  import {
47
47
  LEDGE_VERSION
48
- } from "./chunk-G6K2NSQS.js";
48
+ } from "./chunk-TVIAHOOD.js";
49
49
  import {
50
50
  SERVICE,
51
51
  inDaemonService,
package/dist/cli/index.js CHANGED
@@ -14,7 +14,7 @@ ledge read <id> print a long messag
14
14
  ledge task <give|list|show|take|done|refuse|pass> \u2026 give another session work, take it, close it (ledge task: details)
15
15
  ledge handover "<bilan>" | show the relief brief, when Ledge asks for it; show: read your predecessor's
16
16
  ledge team <up|list> \u2026 start a team from a template, list templates, roles and live teams
17
- ledge tui [--notify bell|osc9|off] follow and answer the sessions in this terminal
17
+ ledge tui [--notify bell|osc9|off] [--here] follow and answer the sessions (in a Ledge tab of Windows Terminal unless --here)
18
18
  ledge bridge exchange Ledge messages over SSH stdio
19
19
  ledge --version print the installed version`;
20
20
  async function hookMain(args) {
@@ -35,7 +35,7 @@ async function main(argv) {
35
35
  const [cmd, ...rest] = argv;
36
36
  switch (cmd) {
37
37
  case "daemon": {
38
- const { startDaemon } = await import("./daemon-SKC7VKYQ.js");
38
+ const { startDaemon } = await import("./daemon-YHRAZSRS.js");
39
39
  await startDaemon();
40
40
  await new Promise(() => {
41
41
  });
@@ -43,7 +43,7 @@ async function main(argv) {
43
43
  }
44
44
  case "--version":
45
45
  case "version": {
46
- const { LEDGE_VERSION } = await import("./version-AEN2CRT2.js");
46
+ const { LEDGE_VERSION } = await import("./version-HB3S4IVG.js");
47
47
  console.log(LEDGE_VERSION);
48
48
  return 0;
49
49
  }
@@ -104,7 +104,7 @@ async function main(argv) {
104
104
  return r.code;
105
105
  }
106
106
  case "tui": {
107
- const { runTui } = await import("./tui-MQLMUUJB.js");
107
+ const { runTui } = await import("./tui-5XPPX4GR.js");
108
108
  return runTui(rest);
109
109
  }
110
110
  case "bridge": {
@@ -115,11 +115,11 @@ async function main(argv) {
115
115
  case "claude":
116
116
  case "codex":
117
117
  case "cursor": {
118
- const { launch } = await import("./launch-T5FJ42XS.js");
118
+ const { launch } = await import("./launch-YQYCDZ7N.js");
119
119
  return launch(cmd, rest);
120
120
  }
121
121
  case "attach": {
122
- const { attach } = await import("./attach-I6TTCGNV.js");
122
+ const { attach } = await import("./attach-3BKQSEWC.js");
123
123
  return attach(rest.find((a) => !a.startsWith("--")) ?? "", { readOnly: rest.includes("--read-only") });
124
124
  }
125
125
  default:
@@ -6,8 +6,8 @@ import {
6
6
  launch,
7
7
  parseLaunchArgs,
8
8
  roleArgs
9
- } from "./chunk-QQZALYNI.js";
10
- import "./chunk-7IQ6FV2F.js";
9
+ } from "./chunk-LOOOHYZR.js";
10
+ import "./chunk-WP77O5LC.js";
11
11
  import "./chunk-3EIRKAX7.js";
12
12
  import "./chunk-OLLQBK6N.js";
13
13
  import "./chunk-KKNE7BFZ.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  attachEnv
3
- } from "./chunk-QQZALYNI.js";
4
- import "./chunk-7IQ6FV2F.js";
3
+ } from "./chunk-LOOOHYZR.js";
4
+ import "./chunk-WP77O5LC.js";
5
5
  import "./chunk-3EIRKAX7.js";
6
6
  import "./chunk-OLLQBK6N.js";
7
7
  import "./chunk-KKNE7BFZ.js";
@@ -1797,7 +1797,7 @@ var fr = {
1797
1797
  question: (n, text) => `${n} a une question : ${text}`,
1798
1798
  title: (n) => n > 0 ? `ledge \xB7 ${n} t\u2019${plural(n, "attend", "attendent")}` : "ledge"
1799
1799
  },
1800
- cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)" }
1800
+ cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)", movedToTab: "ledge tui s\u2019ouvre dans l\u2019onglet Ledge.", loadingProfile: "Windows Terminal charge le profil Ledge\u2026" }
1801
1801
  };
1802
1802
 
1803
1803
  // src/tui/i18n/en.ts
@@ -2032,7 +2032,7 @@ var en = {
2032
2032
  question: (n, text) => `${n} has a question: ${text}`,
2033
2033
  title: (n) => n > 0 ? `ledge \xB7 ${n} waiting` : "ledge"
2034
2034
  },
2035
- cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)" }
2035
+ cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)", movedToTab: "ledge tui opens in the Ledge tab.", loadingProfile: "Windows Terminal is loading the Ledge profile\u2026" }
2036
2036
  };
2037
2037
 
2038
2038
  // src/tui/i18n/lang.tsx
@@ -3140,12 +3140,12 @@ function SessionView({ s }) {
3140
3140
  offers(s.tool, p.kind, "note") ? /* @__PURE__ */ jsx11(Button, { label: t.overview.denyWithNote, k: "r" }) : null
3141
3141
  ] }) })
3142
3142
  ] }) : null,
3143
- /* @__PURE__ */ jsx11(TitledBox, { title: mode === "note" ? t.overview.denyWithNote : t.keys.write, color: mode === "read" ? C.line : C.coral, children: /* @__PURE__ */ jsxs8(Text10, { wrap: "truncate", children: [
3143
+ /* @__PURE__ */ jsx11(TitledBox, { title: mode === "note" ? t.overview.denyWithNote : t.keys.write, color: mode === "read" ? C.line : C.coral, children: /* @__PURE__ */ jsxs8(Box9, { children: [
3144
3144
  /* @__PURE__ */ jsxs8(Text10, { color: C.coral, children: [
3145
3145
  g.cursor,
3146
3146
  " "
3147
3147
  ] }),
3148
- /* @__PURE__ */ jsx11(
3148
+ /* @__PURE__ */ jsx11(Box9, { flexGrow: 1, overflow: "hidden", children: /* @__PURE__ */ jsx11(
3149
3149
  TextInput,
3150
3150
  {
3151
3151
  value: draft,
@@ -3155,7 +3155,7 @@ function SessionView({ s }) {
3155
3155
  active: mode === "reply" || mode === "note",
3156
3156
  placeholder: mode === "note" ? t.session.noteFor(s.name) : t.session.replyTo(s.name)
3157
3157
  }
3158
- )
3158
+ ) })
3159
3159
  ] }) })
3160
3160
  ] });
3161
3161
  }
@@ -3310,14 +3310,14 @@ function Ask() {
3310
3310
  return /* @__PURE__ */ jsxs9(Frame, { crumb: `${t.tabs.ask}`, keys: [["\u21B5", t.keys.send], ["tab", t.keys.nextCard], ["s", t.keys.save], ["ctrl+n", t.keys.newChat], ["ctrl+c", t.keys.stopAnswer], ["esc", t.keys.back]], children: [
3311
3311
  /* @__PURE__ */ jsx12(TitledBox, { title: t.ask.operator(sessions, Math.max(1, machines.length)), flexGrow: 1, children: lines.length === 0 ? /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: t.ask.empty }) : /* @__PURE__ */ jsx12(Lines, { lines }) }),
3312
3312
  cards.slice(0, 2).map((c, i) => /* @__PURE__ */ jsx12(CardBox, { card: c, turn: d.chat.find((x) => x.qid === c.qid), focused: i === focus, busy: busy?.qid === c.qid }, `${c.kind}:${c.qid}`)),
3313
- /* @__PURE__ */ jsx12(TitledBox, { title: t.ask.question, flexShrink: 0, color: focus === -1 ? C.coral : C.line, children: /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
3313
+ /* @__PURE__ */ jsx12(TitledBox, { title: t.ask.question, flexShrink: 0, color: focus === -1 ? C.coral : C.line, children: /* @__PURE__ */ jsxs9(Box10, { children: [
3314
3314
  /* @__PURE__ */ jsxs9(Text11, { color: C.coral, children: [
3315
3315
  g.cursor,
3316
3316
  " "
3317
3317
  ] }),
3318
- /* @__PURE__ */ jsx12(TextInput, { value: draft, onChange: setDraft, onSubmit: ask, onEscape: () => {
3318
+ /* @__PURE__ */ jsx12(Box10, { flexGrow: 1, overflow: "hidden", children: /* @__PURE__ */ jsx12(TextInput, { value: draft, onChange: setDraft, onSubmit: ask, onEscape: () => {
3319
3319
  if (!draft) go("overview");
3320
- }, active: focus === -1 })
3320
+ }, active: focus === -1 }) })
3321
3321
  ] }) })
3322
3322
  ] });
3323
3323
  }
@@ -4007,7 +4007,7 @@ function App(props) {
4007
4007
  const [lang, setLangState] = useState9(props.lang);
4008
4008
  return /* @__PURE__ */ jsx16(I18nProvider, { lang, children: /* @__PURE__ */ jsx16(Shell, { ...props, lang, setLang: setLangState }) });
4009
4009
  }
4010
- function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs, env, run }) {
4010
+ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs, env, run: run2 }) {
4011
4011
  const { exit, suspendTerminal } = useApp();
4012
4012
  const { stdout } = useStdout();
4013
4013
  const write = useCallback((seq) => {
@@ -4074,10 +4074,10 @@ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs,
4074
4074
  return;
4075
4075
  }
4076
4076
  void suspendTerminal(async () => {
4077
- const code = await (run ?? runAttached)(cmd.argv);
4077
+ const code = await (run2 ?? runAttached)(cmd.argv);
4078
4078
  if (code !== 0) toast({ text: t.toast.attachFailed(code), tone: "error" });
4079
4079
  });
4080
- }, [fleet, suspendTerminal, run, t, toast]);
4080
+ }, [fleet, suspendTerminal, run2, t, toast]);
4081
4081
  const tui = {
4082
4082
  client,
4083
4083
  fleet,
@@ -4260,18 +4260,123 @@ function saveSettings(settings) {
4260
4260
  renameSync(tmp, path);
4261
4261
  }
4262
4262
 
4263
+ // src/tui/terminalProfile.ts
4264
+ import { execFile, spawn as spawn2 } from "child_process";
4265
+ import { mkdir, readFile, realpath, writeFile } from "fs/promises";
4266
+ import { dirname, join as join2 } from "path";
4267
+ import { fileURLToPath } from "url";
4268
+ import { promisify } from "util";
4269
+ var PROFILE = "Ledge";
4270
+ var ICON = fileURLToPath(new URL("../../assets/ledge.ico", import.meta.url));
4271
+ var SETTINGS = [
4272
+ "Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json",
4273
+ "Packages/Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe/LocalState/settings.json",
4274
+ "Microsoft/Windows Terminal/settings.json"
4275
+ ];
4276
+ var fragmentDir = (localAppData) => join2(localAppData, "Microsoft", "Windows Terminal", "Fragments", PROFILE);
4277
+ var PROFILE_ID = "{d10fee13-0978-595f-a851-b60ec80f11c9}";
4278
+ function movesToLedgeTab(env, here, script) {
4279
+ return !here && !!env.WT_SESSION && !!env.WSL_DISTRO_NAME && !env.TMUX && env.WT_PROFILE_ID !== PROFILE_ID && !script.endsWith(".ts");
4280
+ }
4281
+ function winArg(s) {
4282
+ if (!s || /["\u0000-\u001f]/.test(s) || s.endsWith("\\")) return null;
4283
+ return /\s/.test(s) ? `"${s}"` : s;
4284
+ }
4285
+ function profileCommandline(distro, launcher) {
4286
+ const args = ["wsl.exe", "-d", distro, "--cd", "~", "--exec", "/bin/sh", launcher].map(winArg);
4287
+ return args.every((a) => a !== null) ? args.join(" ") : null;
4288
+ }
4289
+ var shQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
4290
+ function launcherScript(node, script, ledgeHome) {
4291
+ const home = ledgeHome ? `export LEDGE_HOME=${shQuote(ledgeHome)}
4292
+ ` : "";
4293
+ return `#!/bin/sh
4294
+ # Written by \`ledge tui\`: the Ledge profile of Windows Terminal runs it.
4295
+ ${home}exec ${shQuote(node)} ${shQuote(script)} tui --here
4296
+ `;
4297
+ }
4298
+ function fragment(commandline, iconWin) {
4299
+ return `${JSON.stringify({ profiles: [{ name: PROFILE, commandline, icon: iconWin }] }, null, 2)}
4300
+ `;
4301
+ }
4302
+ async function profileLoaded(localAppData) {
4303
+ for (const rel of SETTINGS) {
4304
+ const text = await readFile(join2(localAppData, rel), "utf8").catch(() => "");
4305
+ if (new RegExp(`"source"\\s*:\\s*"${PROFILE}"`).test(text)) return true;
4306
+ }
4307
+ return false;
4308
+ }
4309
+ async function writeIfChanged(path, content) {
4310
+ const current = await readFile(path).catch(() => null);
4311
+ if (current && current.equals(Buffer.from(content))) return false;
4312
+ await writeFile(path, content);
4313
+ return true;
4314
+ }
4315
+ async function openInLedgeTab(o) {
4316
+ const commandline = profileCommandline(o.distro, o.launcher);
4317
+ if (!commandline) return false;
4318
+ await mkdir(dirname(o.launcher), { recursive: true });
4319
+ await writeIfChanged(o.launcher, launcherScript(o.node, o.script, o.ledgeHome));
4320
+ const dir = fragmentDir(o.localAppData.linux);
4321
+ await mkdir(dir, { recursive: true });
4322
+ await writeIfChanged(join2(dir, "ledge.ico"), await readFile(o.icon ?? ICON));
4323
+ const iconWin = `${o.localAppData.win}\\Microsoft\\Windows Terminal\\Fragments\\${PROFILE}\\ledge.ico`;
4324
+ await writeIfChanged(join2(dir, "ledge.json"), fragment(commandline, iconWin));
4325
+ const deadline = Date.now() + (o.waitMs ?? 15e3);
4326
+ for (let first = true; !await profileLoaded(o.localAppData.linux); first = false) {
4327
+ if (Date.now() > deadline) return false;
4328
+ if (first) o.onWait?.();
4329
+ await new Promise((r) => setTimeout(r, 100));
4330
+ }
4331
+ await o.wt(["-w", "0", "new-tab", "-p", PROFILE]);
4332
+ return true;
4333
+ }
4334
+ var run = promisify(execFile);
4335
+ async function openTuiInLedgeTab(env, onWait) {
4336
+ try {
4337
+ const cmd = "/mnt/c/Windows/System32/cmd.exe";
4338
+ const win = (await run(cmd, ["/d", "/c", "echo %LOCALAPPDATA%"], { cwd: "/mnt/c", timeout: 5e3 })).stdout.trim();
4339
+ if (!/^[A-Za-z]:\\/.test(win)) return false;
4340
+ const linux = (await run("wslpath", ["-u", win], { timeout: 5e3 })).stdout.trim();
4341
+ return await openInLedgeTab({
4342
+ localAppData: { win, linux },
4343
+ distro: env.WSL_DISTRO_NAME,
4344
+ node: await realpath(process.execPath),
4345
+ script: await realpath(process.argv[1]),
4346
+ launcher: join2(ledgeDir(), "tui.sh"),
4347
+ ledgeHome: env.LEDGE_HOME,
4348
+ onWait,
4349
+ wt: (args) => new Promise((resolve, reject) => {
4350
+ const child = spawn2(join2(linux, "Microsoft", "WindowsApps", "wt.exe"), args, { cwd: "/mnt/c", stdio: "ignore", detached: true });
4351
+ child.once("error", reject);
4352
+ child.once("spawn", () => {
4353
+ child.unref();
4354
+ resolve();
4355
+ });
4356
+ })
4357
+ });
4358
+ } catch {
4359
+ return false;
4360
+ }
4361
+ }
4362
+
4263
4363
  // src/tui/index.tsx
4264
4364
  import { jsx as jsx17 } from "react/jsx-runtime";
4265
- var TUI_USAGE = "ledge tui [--notify bell|osc9|off]";
4365
+ var TUI_USAGE = "ledge tui [--notify bell|osc9|off] [--here]";
4266
4366
  function tuiArgs(args) {
4267
4367
  let notify = "bell";
4368
+ let here = false;
4268
4369
  for (let i = 0; i < args.length; i++) {
4269
4370
  const a = args[i];
4371
+ if (a === "--here") {
4372
+ here = true;
4373
+ continue;
4374
+ }
4270
4375
  const value = a.startsWith("--notify=") ? a.slice("--notify=".length) : a === "--notify" ? args[++i] : void 0;
4271
4376
  if (value === void 0 || !NOTIFY_MODES.includes(value)) return null;
4272
4377
  notify = value;
4273
4378
  }
4274
- return { notify };
4379
+ return { notify, here };
4275
4380
  }
4276
4381
  async function runTui(args) {
4277
4382
  const parsed = tuiArgs(args);
@@ -4285,6 +4390,10 @@ async function runTui(args) {
4285
4390
  console.error(MESSAGES[lang].cli.notTty);
4286
4391
  return 2;
4287
4392
  }
4393
+ if (movesToLedgeTab(process.env, parsed.here, process.argv[1] ?? "") && await openTuiInLedgeTab(process.env, () => console.log(MESSAGES[lang].cli.loadingProfile))) {
4394
+ console.log(MESSAGES[lang].cli.movedToTab);
4395
+ return 0;
4396
+ }
4288
4397
  const client = createTuiFleet();
4289
4398
  const detach = attachClient(client);
4290
4399
  client.connect();
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  LEDGE_VERSION
3
- } from "./chunk-G6K2NSQS.js";
3
+ } from "./chunk-TVIAHOOD.js";
4
4
  export {
5
5
  LEDGE_VERSION
6
6
  };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@paradow/ledge",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "description": "Follow and answer your WSL agent sessions (Claude Code, Codex, Cursor Agent) from the Ledge island on Windows.",
5
5
  "type": "module",
6
6
  "bin": { "ledge": "dist/cli/index.js" },
7
- "files": ["dist"],
7
+ "files": ["dist", "assets/ledge.ico"],
8
8
  "engines": { "node": ">=22" },
9
9
  "repository": { "type": "git", "url": "git+https://github.com/Lyssaen/ledge.git", "directory": "packages/ledge" },
10
10
  "homepage": "https://github.com/Lyssaen/ledge#readme",
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var LEDGE_VERSION = true ? "0.18.0" : "0.0.0-dev";
3
-
4
- export {
5
- LEDGE_VERSION
6
- };