opencode-courier 0.4.0 → 0.4.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
@@ -116,7 +116,7 @@ line to the server log naming both ([more](docs/reference.md#the-opencode-versio
116
116
 
117
117
  | opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
118
118
  |---|---|
119
- | 0.4.0 | 2.0.28 |
119
+ | 0.4.0, 0.4.1 | 2.0.28 |
120
120
  | 0.2.3, 0.3.0 | 2.0.26 |
121
121
  | 0.2.2 | 2.0.24 |
122
122
  | 0.2.1 | 2.0.23 |
@@ -264,14 +264,19 @@ courier 2 running · 1 waiting on you · group pair 1/2
264
264
  by depth, with what it does, its report and its group's standing; a session that started others gets
265
265
  a bar of the parts reported, a leaf how long it ran, its last tool call and its cost. `↑`/`↓` select,
266
266
  `enter` opens the session, `s` stops it and everything under it after you confirm, as `courier_stop`
267
- would, and `q` closes the panel; `/courier stop <session>` stops from the prompt.
267
+ would, and `q` closes the panel; `/courier stop <session>` stops from the prompt. While open, the panel
268
+ follows you between sessions: into one of its tree it keeps that tree, the session on screen marked
269
+ `(on screen)`; elsewhere it shows that session's tree, or closes if it started none.
268
270
 
269
- `ctrl+x k` (the leader key, then `k`), `/asks`, the palette's "Courier: what waits on you" or a click
271
+ `ctrl+x k` (the leader key, then `k`), `/asks` (or `/ask`), the palette's "Courier: what waits on you" or a click
270
272
  on "waiting on you" opens the asks inbox: every permission request, question and form waiting
271
273
  anywhere in the tree, oldest first, with where it comes from, answered in a dialog with no model in
272
274
  between; the session that started the tree hears once that you settled it. The TUI plays OpenCode's
273
275
  sounds for a child's report, a group's release and a new ask ([more](docs/reference.md#in-the-terminal)).
274
276
 
277
+ While a permission prompt or form is shown in the session, OpenCode hides the footer, so open the
278
+ inbox with `ctrl+x k` (or `/asks` once the prompt is back).
279
+
275
280
  ## Native child sessions
276
281
 
277
282
  Off by default. With `"nativeChildren": true` in the plugin's `options`, a child started without
@@ -41,8 +41,8 @@ export declare function stoppedLine({ stopped, cancelled }: Stopped): string;
41
41
  /** The usage the `/courier` command shows for an input it does not know. */
42
42
  export declare const USAGE = "/courier tree opens the session tree; /courier stop [session] stops a session and those under it";
43
43
  /**
44
- * Adds the tree panel to the `session.panel` slot, opened by `/courier tree` or the command palette, and `/courier
45
- * stop <session>`, which stops a session under the open one after the person confirms.
44
+ * Adds the tree panel to the `session.panel` slot, opened by `/courier tree` or the palette, which follows the person
45
+ * between sessions while open, and `/courier stop <session>`, which stops a session under the open one once confirmed.
46
46
  */
47
47
  export declare function addTreePanel(ctx: Context): () => void;
48
48
  /**
package/dist/tui.js CHANGED
@@ -518,6 +518,7 @@ function addAsksInbox(ctx) {
518
518
  }, SETTLE_MS);
519
519
  });
520
520
  ctx.keymap.layer(() => ({
521
+ mode: "global",
521
522
  commands: [{
522
523
  id: INBOX_COMMAND,
523
524
  title: "Courier: what waits on you",
@@ -526,7 +527,8 @@ function addAsksInbox(ctx) {
526
527
  bind: "<leader>k",
527
528
  palette: true,
528
529
  slash: {
529
- name: "asks"
530
+ name: "asks",
531
+ aliases: ["ask"]
530
532
  },
531
533
  run: () => void open()
532
534
  }]
@@ -550,7 +552,7 @@ import { createTextNode as _$createTextNode } from "@opentui/solid";
550
552
  import { insertNode as _$insertNode } from "@opentui/solid";
551
553
  import { setProp as _$setProp } from "@opentui/solid";
552
554
  import { createElement as _$createElement } from "@opentui/solid";
553
- import { createEffect, createResource, createSignal, For, onCleanup, Show } from "solid-js";
555
+ import { createEffect, createResource, createRoot, createSignal, For, onCleanup, onMount, Show, untrack } from "solid-js";
554
556
 
555
557
  // src/tree/rpc.ts
556
558
  import { Rpc as Rpc2 } from "@opencode/plugin/rpc";
@@ -813,10 +815,12 @@ async function confirmStop(ctx, courier, location, by, target) {
813
815
  }
814
816
  return true;
815
817
  }
818
+ var FOCUS_PROMPT = "pane.focus.left";
816
819
  function TreePanel(props) {
817
820
  const {
818
821
  ctx,
819
822
  input,
823
+ view,
820
824
  courier,
821
825
  location
822
826
  } = props;
@@ -824,7 +828,7 @@ function TreePanel(props) {
824
828
  const [read, {
825
829
  refetch
826
830
  }] = createResource(() => ({
827
- sessionID: input.sessionID,
831
+ sessionID: view.root,
828
832
  changed: props.changed()
829
833
  }), ({
830
834
  sessionID
@@ -839,12 +843,40 @@ function TreePanel(props) {
839
843
  sessionID,
840
844
  error: reason(error)
841
845
  })));
842
- const latest = () => read.state === "ready" || read.state === "refreshing" ? read.latest : undefined;
846
+ const latest = () => read.state === "ready" || read.state === "refreshing" ? read.latest : view.tree && {
847
+ sessionID: view.root,
848
+ tree: view.tree
849
+ };
843
850
  const rows = () => latest()?.tree?.rows ?? [];
851
+ createEffect(() => {
852
+ const tree = latest()?.tree;
853
+ if (tree)
854
+ view.note({
855
+ tree
856
+ });
857
+ });
858
+ createEffect(() => view.note({
859
+ presentation: input.presentation,
860
+ focused: input.focused
861
+ }));
862
+ onMount(() => {
863
+ if (!view.refocus)
864
+ return;
865
+ const giveBack = setTimeout(() => ctx.keymap.dispatch(FOCUS_PROMPT));
866
+ onCleanup(() => clearTimeout(giveBack));
867
+ });
844
868
  const [now, setNow] = createSignal(Date.now());
845
869
  const tick = setInterval(() => setNow(Date.now()), TICK_MS);
846
870
  onCleanup(() => clearInterval(tick));
847
871
  const [selected, setSelected] = createSignal(0);
872
+ let placed = false;
873
+ createEffect(() => {
874
+ const index = placed ? -1 : rows().findIndex((row) => row.sessionID === input.sessionID);
875
+ if (index < 0)
876
+ return;
877
+ placed = true;
878
+ setSelected(index);
879
+ });
848
880
  const current = () => rows()[Math.min(selected(), rows().length - 1)];
849
881
  let scroll;
850
882
  createEffect(() => {
@@ -864,7 +896,7 @@ function TreePanel(props) {
864
896
  const stopSelected = () => {
865
897
  const row = current();
866
898
  if (row)
867
- confirmStop(ctx, courier, location, input.sessionID, {
899
+ confirmStop(ctx, courier, location, view.root, {
868
900
  sessionID: row.sessionID,
869
901
  title: row.title,
870
902
  under: reach(rows(), row.sessionID) - 1
@@ -920,7 +952,7 @@ function TreePanel(props) {
920
952
  run: () => input.close()
921
953
  }]
922
954
  }));
923
- const root = () => parts(rows(), input.sessionID);
955
+ const root = () => parts(rows(), view.root);
924
956
  const rowColor = (row) => {
925
957
  if (row.state === "waiting")
926
958
  return theme.text.feedback.warning.base;
@@ -1010,6 +1042,7 @@ function TreePanel(props) {
1010
1042
  _$insert(_el$17, indent, _el$18);
1011
1043
  _$insert(_el$17, () => chosen() ? "\u25B8 " : " ", _el$18);
1012
1044
  _$insert(_el$17, () => row.title, _el$18);
1045
+ _$insert(_el$17, () => row.sessionID === input.sessionID ? " (on screen)" : "", _el$18);
1013
1046
  _$insert(_el$19, () => statusLine(row));
1014
1047
  _$insert(_el$16, _$createComponent(For, {
1015
1048
  get each() {
@@ -1066,6 +1099,23 @@ function addTreePanel(ctx) {
1066
1099
  location
1067
1100
  } = courierOf(ctx);
1068
1101
  const [changed, stopListening] = changes(ctx, PANEL_CHANGES, PANEL_SETTLE_MS);
1102
+ let following;
1103
+ let attempt = 0;
1104
+ const viewOf = (sessionID) => {
1105
+ const here = following?.at === sessionID ? following : undefined;
1106
+ const refocus = here?.refocus === true;
1107
+ if (here)
1108
+ here.refocus = false;
1109
+ return {
1110
+ root: here?.root ?? sessionID,
1111
+ tree: here?.tree,
1112
+ refocus,
1113
+ note: (fields) => {
1114
+ if (here && following === here)
1115
+ Object.assign(here, fields);
1116
+ }
1117
+ };
1118
+ };
1069
1119
  const release = ctx.ui.slot({
1070
1120
  append: "session.panel",
1071
1121
  render: (input) => _$createComponent(Show, {
@@ -1076,6 +1126,9 @@ function addTreePanel(ctx) {
1076
1126
  return _$createComponent(TreePanel, {
1077
1127
  ctx,
1078
1128
  input,
1129
+ get view() {
1130
+ return viewOf(input.sessionID);
1131
+ },
1079
1132
  courier,
1080
1133
  location,
1081
1134
  changed
@@ -1083,13 +1136,82 @@ function addTreePanel(ctx) {
1083
1136
  }
1084
1137
  })
1085
1138
  });
1086
- const openPanel = () => {
1087
- if (!ctx.ui.panel.open(PANEL))
1088
- ctx.ui.toast.show({
1089
- title: "courier",
1090
- message: "Open a session first",
1091
- variant: "warning"
1139
+ const show = (from, at, root, tree) => {
1140
+ following = {
1141
+ ...from,
1142
+ root,
1143
+ at,
1144
+ to: undefined,
1145
+ tree,
1146
+ refocus: !from.focused
1147
+ };
1148
+ ctx.ui.panel.open(PANEL, {
1149
+ presentation: from.presentation
1150
+ });
1151
+ };
1152
+ const treeOf = (sessionID) => courier.tree({
1153
+ sessionID
1154
+ }, {
1155
+ location: location(sessionID)
1156
+ }).catch(() => {
1157
+ return;
1158
+ });
1159
+ const follow = (from, sessionID) => {
1160
+ const token = ++attempt;
1161
+ const under = (tree) => tree?.rows.some((row) => row.sessionID === sessionID) === true;
1162
+ Object.assign(from, {
1163
+ at: undefined,
1164
+ to: sessionID
1165
+ });
1166
+ if (sessionID === from.root || under(from.tree))
1167
+ return show(from, sessionID, from.root, from.tree);
1168
+ (async () => {
1169
+ const inside = await treeOf(from.root);
1170
+ const own = under(inside) ? undefined : await treeOf(sessionID);
1171
+ if (token !== attempt || following !== from)
1172
+ return;
1173
+ if (under(inside))
1174
+ show(from, sessionID, from.root, inside);
1175
+ else if (own?.rows.length)
1176
+ show(from, sessionID, sessionID, own);
1177
+ else
1178
+ following = undefined;
1179
+ })();
1180
+ };
1181
+ const stopFollowing = createRoot((dispose) => {
1182
+ createEffect(() => {
1183
+ const route = ctx.ui.router.current();
1184
+ const sessionID = route.type === "session" ? route.sessionID : undefined;
1185
+ const shown2 = ctx.ui.panel.current();
1186
+ untrack(() => {
1187
+ if (!following || shown2 && shown2.sessionID === sessionID || !following.at && following.to === sessionID)
1188
+ return;
1189
+ if (!shown2 && following.at === sessionID || !sessionID)
1190
+ following = undefined;
1191
+ else
1192
+ follow(following, sessionID);
1092
1193
  });
1194
+ });
1195
+ return dispose;
1196
+ });
1197
+ const openPanel = () => {
1198
+ const route = ctx.ui.router.current();
1199
+ const before = following;
1200
+ if (route.type === "session" && following?.at !== route.sessionID)
1201
+ following = {
1202
+ root: route.sessionID,
1203
+ at: route.sessionID,
1204
+ presentation: "panel",
1205
+ focused: true
1206
+ };
1207
+ if (ctx.ui.panel.open(PANEL))
1208
+ return;
1209
+ following = before;
1210
+ ctx.ui.toast.show({
1211
+ title: "courier",
1212
+ message: "Open a session first",
1213
+ variant: "warning"
1214
+ });
1093
1215
  };
1094
1216
  const stopUnder = async (target) => {
1095
1217
  const route = ctx.ui.router.current();
@@ -1153,6 +1275,7 @@ function addTreePanel(ctx) {
1153
1275
  }]
1154
1276
  }));
1155
1277
  return () => {
1278
+ stopFollowing();
1156
1279
  stopListening();
1157
1280
  release();
1158
1281
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-courier",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Multi-session orchestration for OpenCode V2: spawn child sessions, be woken by them. No polling.",
5
5
  "license": "MIT",
6
6
  "author": "Ivo Pogace <71701666+ivopogace@users.noreply.github.com>",