@neosh/api 0.4.1 → 0.4.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neosh/api",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "The neosh plugin API. Types are generated from the Rust side and drift-checked in CI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -141,7 +141,18 @@ export type ApiCall =
141
141
  * work somewhere is not, for the same reason `SessionNew { activate: false }` exists.
142
142
  */
143
143
  activate: boolean;
144
+ /**
145
+ * Which strip to open it on. See [`TabInfo::group`].
146
+ *
147
+ * `None` inherits the tab you are on, which is what a new tab almost always wants: opened
148
+ * while reading a conversation, it is that conversation's. Saying one is for a caller
149
+ * putting a tab somewhere *else* — and a group nothing is currently showing is a tab that
150
+ * is made and is off the bar, which is a legitimate thing to want and a surprising thing
151
+ * to do by accident.
152
+ */
153
+ group?: string | null;
144
154
  }
155
+ | { "call": "tab_group"; tab: TabId; group: string | null }
145
156
  | { "call": "tab_close"; tab: TabId }
146
157
  | { "call": "tab_select"; tab: TabId }
147
158
  | { "call": "tab_step"; view?: ViewId | null; delta: number }
@@ -639,6 +650,16 @@ export type ApiCall =
639
650
  * Ask the network again rather than answering from the last check.
640
651
  */
641
652
  force: boolean;
653
+ /**
654
+ * Answer from what this machine already knows, and never reach the network.
655
+ *
656
+ * The two are not opposites and are never both set: `local` is checked first and wins.
657
+ * It exists because half of an update status is not about a registry at all —
658
+ * [`UpdateStatus::restart_pending`] is a `stat` of one file — and a caller that wants
659
+ * only that half should not be the reason a workspace makes an HTTP request. Which is
660
+ * what a panel polling for "has something replaced my binary" would otherwise be.
661
+ */
662
+ local: boolean;
642
663
  }
643
664
  | { "call": "update_apply" }
644
665
  | {
@@ -24,4 +24,20 @@ export type TabInfo = {
24
24
  * rather than in whichever pane happens to be first.
25
25
  */
26
26
  active_pane: PaneId;
27
+ /**
28
+ * Which strip this tab is on — the conversation it belongs to.
29
+ *
30
+ * **A tab belongs to a conversation, and the bar shows one conversation's tabs.** A shell
31
+ * opened while reading one is *that* conversation's shell: it was started in its directory,
32
+ * about its work, and a bar that goes on showing it after you have switched is a bar about
33
+ * the terminal rather than about what you are doing. The tabs of the conversations you are
34
+ * not in are not closed, and nothing in them stops — they are off the strip until you go
35
+ * back, which is what makes leaving a build running and returning to it work.
36
+ *
37
+ * Opaque here and in the editor, which only ever asks whether two of them are equal: the host
38
+ * fills it with a [`crate::SessionId`], and a plugin that grouped its tabs by project would be
39
+ * using it exactly as intended. `None` is a group of its own — where a tab nobody has filed
40
+ * sits, and what every tab was before this existed.
41
+ */
42
+ group?: string | null;
27
43
  };
@@ -81,4 +81,21 @@ export type UiEvent =
81
81
  }
82
82
  | { "type": "clipboard"; text: string }
83
83
  | { "type": "flush" }
84
- | { "type": "shutdown" };
84
+ | {
85
+ "type": "shutdown";
86
+ /**
87
+ * Whether it is coming straight back, and this terminal should follow it.
88
+ *
89
+ * `neosh stop` and a restart are the same shutdown — conversations flushed, plugins torn
90
+ * down — and they differ entirely in what the *terminal* should do next. Stopping is
91
+ * somebody finishing; a restart is the second half of an update, and a terminal that
92
+ * exits to the shell there has turned "finish updating" into "and now type `neosh`
93
+ * again", which is the step nobody knows is owed. It cannot be worked out at the far end
94
+ * either: from a closed socket, a workspace that stopped and a workspace that is
95
+ * restarting look identical.
96
+ *
97
+ * `#[serde(default)]` so an older terminal reads it as an ordinary stop, which is the
98
+ * behaviour it already has.
99
+ */
100
+ restarting: boolean;
101
+ };
@@ -33,7 +33,27 @@ export type UpdateStatus = {
33
33
  */
34
34
  error?: string | null;
35
35
  /**
36
- * Whether an update has been downloaded and is waiting for a restart.
36
+ * Whether the binary on disk is not the one this process is running.
37
+ *
38
+ * Not "whether neosh downloaded something": most installs are updated by somebody else's
39
+ * package manager, in another terminal, and the workspace goes on executing the inode it
40
+ * started with — which is the whole failure this field exists to name. `brew upgrade neosh`
41
+ * finishes, says so, and leaves a workspace running the old code with nothing on screen to
42
+ * say why the thing you just installed is not there. So this is a `stat` of the running
43
+ * executable against the stamp taken at startup, and it is true for a self-update, a
44
+ * `brew upgrade`, an `npm install -g` and a re-run of `install.sh` alike.
45
+ *
46
+ * Never set when there is no binary left to restart onto: a promise of a restart that cannot
47
+ * come back is worse than saying nothing.
37
48
  */
38
49
  restart_pending: boolean;
50
+ /**
51
+ * What is waiting on disk, when it could be read.
52
+ *
53
+ * Asked of the binary itself (`neosh --version`) rather than assumed to be
54
+ * [`latest`](UpdateStatus::latest): somebody who ran `brew upgrade` got whatever Homebrew had,
55
+ * which is not always the newest release, and a row naming the wrong number is a row that
56
+ * teaches you not to read it. `None` means *something* changed and it would not say what.
57
+ */
58
+ restart_version?: string | null;
39
59
  };
package/src/index.ts CHANGED
@@ -662,21 +662,63 @@ export interface PaneApi {
662
662
  equalize(view?: ViewId): Promise<void>;
663
663
  }
664
664
 
665
- /** The tabs of one terminal. Each is a title and a tree of panes. */
665
+ /**
666
+ * The tabs of one terminal. Each is a title, a tree of panes, and the conversation it belongs to.
667
+ *
668
+ * **A tab belongs to a conversation, and the bar shows one conversation's tabs** — that is
669
+ * {@link TabInfo.group}, and it is why a shell opened while reading one conversation is not still
670
+ * on the bar after you have switched to another. The tabs of the conversations you are not in are
671
+ * not closed and nothing in them stops; they are off the strip until you go back.
672
+ *
673
+ * Which splits these calls in two. {@link TabApi.list} answers with *every* tab of a terminal,
674
+ * whatever bar it is on, because "what is open here" is a real question — and {@link TabApi.step},
675
+ * {@link TabApi.move} and the numbers on the strip all count the tabs of one conversation, because
676
+ * a key about where you are looking must not change what you are working on.
677
+ */
666
678
  export interface TabApi {
667
- /** Every tab of a terminal, its panes, and which one is on screen. */
679
+ /**
680
+ * Every tab of a terminal, its panes, and which one is on screen.
681
+ *
682
+ * All of them, including the ones on other conversations' bars: filter by {@link TabInfo.group}
683
+ * for what is drawn, which is the tabs sharing the active tab's group.
684
+ */
668
685
  list(view?: ViewId): Promise<{ tabs: TabInfo[]; active: TabId }>;
669
686
  /**
670
687
  * Open a tab with one empty pane. `activate` goes to it — `false` is for putting work somewhere
671
688
  * without moving the screen out from under whoever is reading it.
689
+ *
690
+ * On the bar you are looking at unless `group` says otherwise: a tab opened while reading a
691
+ * conversation is that conversation's, which is what a new tab almost always wants.
692
+ */
693
+ create(opts?: {
694
+ title?: string;
695
+ activate?: boolean;
696
+ view?: ViewId;
697
+ group?: string;
698
+ }): Promise<TabId>;
699
+ /**
700
+ * Move a tab to another conversation's bar, or to nobody's with `null`. See
701
+ * {@link TabInfo.group}.
702
+ *
703
+ * It keeps its panes, its windows and anything running in them — this says which bar it is on
704
+ * and nothing else, so moving the tab you are *in* changes which other tabs are beside it.
672
705
  */
673
- create(opts?: { title?: string; activate?: boolean; view?: ViewId }): Promise<TabId>;
674
- /** Close a tab and every pane in it. Rejects on a terminal's last tab. */
706
+ group(tab: TabId, group: string | null): Promise<void>;
707
+ /** Close a tab and every pane in it. Rejects on a conversation's last tab. */
675
708
  close(tab: TabId): Promise<void>;
676
709
  select(tab: TabId): Promise<void>;
677
- /** Go `delta` tabs along, wrapping — a tab bar is a list you can see all of. */
710
+ /**
711
+ * Go `delta` tabs along, wrapping — a tab bar is a list you can see all of. Along *this
712
+ * conversation's* tabs: stepping into another one's would be a key that changes what you are
713
+ * working on.
714
+ */
678
715
  step(delta: number, view?: ViewId): Promise<void>;
679
- /** Move a tab along the bar, clamped at the ends. This is a drag; a drag that teleports is a bug. */
716
+ /**
717
+ * Move a tab along the bar, clamped at the ends. This is a drag; a drag that teleports is a bug.
718
+ *
719
+ * `to` counts the tabs on the bar, which is the number printed on the strip — the tabs of other
720
+ * conversations sit between these and are not places a drag can land.
721
+ */
680
722
  move(tab: TabId, to: number): Promise<void>;
681
723
  /** Name a tab, or pass `null` to give it back its derived name. */
682
724
  rename(tab: TabId, title: string | null): Promise<void>;
@@ -1791,6 +1833,17 @@ export interface UpdateApi {
1791
1833
  * {@link UpdateStatus.error} when it is there.
1792
1834
  */
1793
1835
  check(force?: boolean): Promise<UpdateStatus>;
1836
+ /**
1837
+ * The same answer, from this machine alone, never touching the network.
1838
+ *
1839
+ * Which is enough for the half that matters most often. {@link UpdateStatus.restart_pending} is a
1840
+ * `stat` of one file — *is the binary on disk the binary this workspace is running* — and it goes
1841
+ * true when somebody runs `brew upgrade neosh` in another terminal, which is how most installs
1842
+ * are actually updated and the moment nothing used to say anything. Cheap enough to poll on a
1843
+ * tick; {@link check} is not, and a panel that used it for this would be a workspace making an
1844
+ * HTTP request every thirty seconds.
1845
+ */
1846
+ local(): Promise<UpdateStatus>;
1794
1847
  /**
1795
1848
  * Update, by whichever route this install takes.
1796
1849
  *
@@ -2425,7 +2478,16 @@ function build(
2425
2478
  },
2426
2479
  update: {
2427
2480
  async check(force) {
2428
- return expect(await c({ call: "update_check", force: force ?? false }), "update").update;
2481
+ return expect(
2482
+ await c({ call: "update_check", force: force ?? false, local: false }),
2483
+ "update",
2484
+ ).update;
2485
+ },
2486
+ async local() {
2487
+ return expect(
2488
+ await c({ call: "update_check", force: false, local: true }),
2489
+ "update",
2490
+ ).update;
2429
2491
  },
2430
2492
  async apply() {
2431
2493
  return expect(await c({ call: "update_apply" }), "update_applied").outcome;
@@ -2648,10 +2710,14 @@ function build(
2648
2710
  view: opts?.view,
2649
2711
  title: opts?.title,
2650
2712
  activate: opts?.activate ?? true,
2713
+ group: opts?.group,
2651
2714
  }),
2652
2715
  "tab",
2653
2716
  ).tab;
2654
2717
  },
2718
+ async group(tab, group) {
2719
+ await c({ call: "tab_group", tab, group });
2720
+ },
2655
2721
  async close(tab) {
2656
2722
  await c({ call: "tab_close", tab });
2657
2723
  },