@neosh/api 0.3.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/package.json +1 -1
- package/src/generated/ApiCall.ts +114 -0
- package/src/generated/ApiOk.ts +10 -1
- package/src/generated/AscpMessage.ts +2 -0
- package/src/generated/AuthRef.ts +6 -1
- package/src/generated/ChecksState.ts +14 -0
- package/src/generated/CredentialSource.ts +1 -0
- package/src/generated/Direction.ts +6 -0
- package/src/generated/Dock.ts +10 -4
- package/src/generated/FloatConfig.ts +33 -0
- package/src/generated/ModelInfo.ts +16 -0
- package/src/generated/NodeCapabilities.ts +11 -0
- package/src/generated/PaneChild.ts +18 -0
- package/src/generated/PaneId.ts +15 -0
- package/src/generated/PaneNode.ts +17 -0
- package/src/generated/PluginEvent.ts +1 -0
- package/src/generated/PullRequest.ts +30 -0
- package/src/generated/PullState.ts +11 -0
- package/src/generated/RemoteProject.ts +18 -0
- package/src/generated/ScrollAmount.ts +16 -0
- package/src/generated/SplitDir.ts +12 -0
- package/src/generated/TabId.ts +10 -0
- package/src/generated/TabInfo.ts +27 -0
- package/src/generated/UiEvent.ts +4 -0
- package/src/generated/WindowLayout.ts +11 -0
- package/src/index.ts +444 -9
- package/src/ui.ts +403 -70
package/src/index.ts
CHANGED
|
@@ -17,6 +17,10 @@ import type { ApiError } from "./generated/ApiError";
|
|
|
17
17
|
import type { ApiOk } from "./generated/ApiOk";
|
|
18
18
|
import type { ApiResponse } from "./generated/ApiResponse";
|
|
19
19
|
import type { BranchInfo } from "./generated/BranchInfo";
|
|
20
|
+
import type { Direction } from "./generated/Direction";
|
|
21
|
+
import type { PaneId } from "./generated/PaneId";
|
|
22
|
+
import type { TabId } from "./generated/TabId";
|
|
23
|
+
import type { TabInfo } from "./generated/TabInfo";
|
|
20
24
|
import type { BufferId } from "./generated/BufferId";
|
|
21
25
|
import type { Capability } from "./generated/Capability";
|
|
22
26
|
import type { CommitInfo } from "./generated/CommitInfo";
|
|
@@ -32,6 +36,7 @@ import type { CredentialSource } from "./generated/CredentialSource";
|
|
|
32
36
|
import type { CursorMotion } from "./generated/CursorMotion";
|
|
33
37
|
import type { CursorShape } from "./generated/CursorShape";
|
|
34
38
|
import type { SelectShape } from "./generated/SelectShape";
|
|
39
|
+
import type { ScrollAmount } from "./generated/ScrollAmount";
|
|
35
40
|
import type { DiffTarget } from "./generated/DiffTarget";
|
|
36
41
|
import type { Dock } from "./generated/Dock";
|
|
37
42
|
import type { Gravity } from "./generated/Gravity";
|
|
@@ -132,12 +137,16 @@ import type { Viewport } from "./generated/Viewport";
|
|
|
132
137
|
import type { WindowId } from "./generated/WindowId";
|
|
133
138
|
import type { WindowInfo } from "./generated/WindowInfo";
|
|
134
139
|
import type { WindowLayout } from "./generated/WindowLayout";
|
|
140
|
+
import type { ChecksState } from "./generated/ChecksState";
|
|
141
|
+
import type { PullRequest } from "./generated/PullRequest";
|
|
142
|
+
import type { PullState } from "./generated/PullState";
|
|
135
143
|
import type { WorktreeInfo } from "./generated/WorktreeInfo";
|
|
136
144
|
|
|
137
145
|
export type {
|
|
138
146
|
AccountKind, Activity, ApiError, BranchInfo, Brand, BufferId, Capability, CommandEntry, CommitInfo,
|
|
139
147
|
AgentCommand, AgentState, AgentSummary,
|
|
140
|
-
Contribution, CredentialInfo, CredentialSource, CursorShape, DiffTarget, Dock, CursorMotion, ExtmarkId, ExtmarkInfo, ExtmarkOpts, FileChange, FileState, FloatConfig,
|
|
148
|
+
Contribution, CredentialInfo, CredentialSource, CursorShape, DiffTarget, Direction, Dock, CursorMotion, ExtmarkId, ExtmarkInfo, ExtmarkOpts, FileChange, FileState, FloatConfig,
|
|
149
|
+
PaneId, TabId, TabInfo,
|
|
141
150
|
Gravity, HighlightDef, HighlightEntry, HighlightSpec, Hint, HlTarget, HookName, HookOutcome, HookPayload, InstanceConfig, KeyContext,
|
|
142
151
|
AttachmentInfo,
|
|
143
152
|
KeymapEntry, KeymapScope, MessageLevel, Mode, ModelEntry, ModelInfo, ModelSelection, ModelTier, NamespaceId,
|
|
@@ -148,12 +157,15 @@ export type {
|
|
|
148
157
|
CostBasis, QuotaCredits, QuotaSample, QuotaSeverity, QuotaSnapshot, QuotaSource, QuotaWindow,
|
|
149
158
|
InstallMethod, UpdateOutcome, UpdateStatus,
|
|
150
159
|
UsageBucket, UsageHistory, UsageResolution, UsageScanSource,
|
|
151
|
-
Rect, RepoInfo, RepoStatus, SelectShape, SessionId, SessionInfo, StatusAlign, StatusSegment, StopReason,
|
|
160
|
+
Rect, RepoInfo, RepoStatus, ScrollAmount, SelectShape, SessionId, SessionInfo, StatusAlign, StatusSegment, StopReason,
|
|
152
161
|
SurfaceCell, SurfaceId, TextEdit, ToolCall, ToolDef, ToolResult, TurnRequest, Usage,
|
|
153
162
|
NodeCapabilities, NodeId, NodeInfo, ProjectKey, RemoteProject, StreamEvent,
|
|
154
163
|
SwarmAgent, SwarmNode, SwarmStranger,
|
|
155
164
|
VarScope, ViewId, ViewInfo, Viewport,
|
|
156
165
|
WindowId, WindowInfo, WindowLayout,
|
|
166
|
+
ChecksState,
|
|
167
|
+
PullRequest,
|
|
168
|
+
PullState,
|
|
157
169
|
WorktreeInfo,
|
|
158
170
|
};
|
|
159
171
|
|
|
@@ -363,6 +375,16 @@ export interface FloatOptions {
|
|
|
363
375
|
border?: FloatConfig["border"];
|
|
364
376
|
borderHl?: string;
|
|
365
377
|
title?: string;
|
|
378
|
+
/**
|
|
379
|
+
* A strip on the bottom border: what the keys here do.
|
|
380
|
+
*
|
|
381
|
+
* On the border rather than in the buffer, because a key strip written as a row of content is one
|
|
382
|
+
* that scrolls away exactly when it is wanted, and is the first thing clipped on a terminal too
|
|
383
|
+
* short for the panel. It costs no content row and cannot be scrolled off.
|
|
384
|
+
*
|
|
385
|
+
* Clipped to the border's width like the title, so it is for a legend and not for prose.
|
|
386
|
+
*/
|
|
387
|
+
footer?: string;
|
|
366
388
|
closeOnBlur?: boolean;
|
|
367
389
|
focusable?: boolean;
|
|
368
390
|
/**
|
|
@@ -379,12 +401,34 @@ export interface FloatOptions {
|
|
|
379
401
|
* control sheet. Not for a hint or a hover card.
|
|
380
402
|
*/
|
|
381
403
|
modal?: boolean;
|
|
404
|
+
/**
|
|
405
|
+
* This panel is a place you can move in when it does not fit.
|
|
406
|
+
*
|
|
407
|
+
* You ask for a height and get whatever there is: `{ kind: "max", n: 30 }` on a twenty-row
|
|
408
|
+
* terminal is ten rows of panel and twenty rows of content drawn nowhere, with no key pointed at
|
|
409
|
+
* them and nothing on screen to say they exist. Set this and the workspace's scroll keys resolve
|
|
410
|
+
* here — `j`/`k`, `^D`/`^U`, `^F`/`^B`, `gg`/`G`, the arrows and the paging keys — along with the
|
|
411
|
+
* mouse wheel, and a bar appears down the right border while anything is hidden.
|
|
412
|
+
*
|
|
413
|
+
* They are ordinary bindings on `{ kind: "buf_kind", name: "neosh.scroll" }`, so `^Z` lists them
|
|
414
|
+
* and `init.ts` moves them. That scope sits *below* your buffer's own kind, so a key you bind on
|
|
415
|
+
* your panel is still yours — and above `global`, so it works under `modal` too.
|
|
416
|
+
*
|
|
417
|
+
* For a panel of rows you read: a key sheet, a diff, a status, a help screen. **Not** for one with
|
|
418
|
+
* a cursor of its own — a picker, a list — which already scrolls to keep the cursor on screen and
|
|
419
|
+
* whose filter would lose `j` and `G` to this.
|
|
420
|
+
*/
|
|
421
|
+
scroll?: boolean;
|
|
382
422
|
}
|
|
383
423
|
|
|
384
424
|
export interface Neosh {
|
|
385
425
|
readonly version: number;
|
|
386
426
|
readonly buf: BufferApi;
|
|
387
427
|
readonly win: WindowApi;
|
|
428
|
+
/** The panes of the main region. See {@link PaneApi}. */
|
|
429
|
+
readonly pane: PaneApi;
|
|
430
|
+
/** The tabs each terminal has. See {@link TabApi}. */
|
|
431
|
+
readonly tab: TabApi;
|
|
388
432
|
readonly float: FloatApi;
|
|
389
433
|
readonly edit: EditApi;
|
|
390
434
|
readonly ns: NamespaceApi;
|
|
@@ -406,6 +450,8 @@ export interface Neosh {
|
|
|
406
450
|
readonly opt: OptionApi;
|
|
407
451
|
readonly state: StateApi;
|
|
408
452
|
readonly vars: VarApi;
|
|
453
|
+
/** The directories somebody works in. See {@link ProjectApi}. */
|
|
454
|
+
readonly project: ProjectApi;
|
|
409
455
|
readonly ext: ExtensionApi;
|
|
410
456
|
readonly event: EventApi;
|
|
411
457
|
readonly swarm: SwarmApi;
|
|
@@ -558,6 +604,84 @@ export interface BufferApi {
|
|
|
558
604
|
): Promise<Disposable>;
|
|
559
605
|
}
|
|
560
606
|
|
|
607
|
+
/**
|
|
608
|
+
* The panes of the main region, and the tabs they live in.
|
|
609
|
+
*
|
|
610
|
+
* A pane is *where*, not *what*: it owns no buffer and draws nothing. You split one, then dock
|
|
611
|
+
* windows into it with {@link WindowApi.open} — which is why splitting a chat and splitting a
|
|
612
|
+
* terminal are the same two calls.
|
|
613
|
+
*
|
|
614
|
+
* Everything here is what the workspace's own `<C-w>` keys are built from. There is no private
|
|
615
|
+
* path: `<C-w>v` is `pane.split(await pane.active(), "right")` and nothing else, so a plugin that
|
|
616
|
+
* wants a different window manager can have one without forking anything.
|
|
617
|
+
*/
|
|
618
|
+
export interface PaneApi {
|
|
619
|
+
/** The pane this terminal's keys belong to. */
|
|
620
|
+
active(view?: ViewId): Promise<PaneId>;
|
|
621
|
+
/**
|
|
622
|
+
* Divide a pane in two, and answer with the new one.
|
|
623
|
+
*
|
|
624
|
+
* The new pane is *empty* — nothing is drawn in it until something is docked into it. Focus does
|
|
625
|
+
* not move either: a plugin splitting to show you something and a person pressing a key to go
|
|
626
|
+
* and work there are different intentions, and only the second one wants the keyboard.
|
|
627
|
+
*/
|
|
628
|
+
split(pane: PaneId, dir: Direction): Promise<PaneId>;
|
|
629
|
+
/**
|
|
630
|
+
* Close a pane and every window docked in it.
|
|
631
|
+
*
|
|
632
|
+
* Rejects on the last pane of the last tab: a terminal with nowhere to draw has no key that
|
|
633
|
+
* would bring a pane back.
|
|
634
|
+
*/
|
|
635
|
+
close(pane: PaneId): Promise<void>;
|
|
636
|
+
focus(pane: PaneId): Promise<void>;
|
|
637
|
+
/**
|
|
638
|
+
* Move the keyboard to the nearest pane in a direction, and answer with it — or `null` at the
|
|
639
|
+
* edge of the layout, which is an ordinary outcome of holding a key down rather than an error.
|
|
640
|
+
* There is deliberately no wrap-around.
|
|
641
|
+
*/
|
|
642
|
+
focusDir(dir: Direction, view?: ViewId): Promise<PaneId | null>;
|
|
643
|
+
/**
|
|
644
|
+
* Move the boundary on a pane's `dir` side. `delta` is in weight units — an even split is
|
|
645
|
+
* {@link WEIGHT} each — so a resize means the same thing at every terminal size and survives the
|
|
646
|
+
* terminal being resized under it. A pane with no boundary on that side is a no-op.
|
|
647
|
+
*/
|
|
648
|
+
resize(pane: PaneId, dir: Direction, delta: number): Promise<void>;
|
|
649
|
+
/**
|
|
650
|
+
* Exchange two panes' places, keeping what is in each. The weights belong to the slots, so
|
|
651
|
+
* swapping into the wide one is how you make a pane wide.
|
|
652
|
+
*/
|
|
653
|
+
swap(pane: PaneId, withPane: PaneId): Promise<void>;
|
|
654
|
+
/**
|
|
655
|
+
* Send a pane to the far edge of its tab — the counterpart to {@link PaneApi.focusDir}, which
|
|
656
|
+
* moves the keyboard rather than the pane. It arrives with an even share rather than the size it
|
|
657
|
+
* had: it is among different neighbours now, and carrying the old number over would make it the
|
|
658
|
+
* odd one out for a reason nobody could see.
|
|
659
|
+
*/
|
|
660
|
+
moveToEdge(pane: PaneId, dir: Direction): Promise<void>;
|
|
661
|
+
/** Give every pane of the active tab an equal share, at every level. */
|
|
662
|
+
equalize(view?: ViewId): Promise<void>;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** The tabs of one terminal. Each is a title and a tree of panes. */
|
|
666
|
+
export interface TabApi {
|
|
667
|
+
/** Every tab of a terminal, its panes, and which one is on screen. */
|
|
668
|
+
list(view?: ViewId): Promise<{ tabs: TabInfo[]; active: TabId }>;
|
|
669
|
+
/**
|
|
670
|
+
* Open a tab with one empty pane. `activate` goes to it — `false` is for putting work somewhere
|
|
671
|
+
* without moving the screen out from under whoever is reading it.
|
|
672
|
+
*/
|
|
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. */
|
|
675
|
+
close(tab: TabId): Promise<void>;
|
|
676
|
+
select(tab: TabId): Promise<void>;
|
|
677
|
+
/** Go `delta` tabs along, wrapping — a tab bar is a list you can see all of. */
|
|
678
|
+
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. */
|
|
680
|
+
move(tab: TabId, to: number): Promise<void>;
|
|
681
|
+
/** Name a tab, or pass `null` to give it back its derived name. */
|
|
682
|
+
rename(tab: TabId, title: string | null): Promise<void>;
|
|
683
|
+
}
|
|
684
|
+
|
|
561
685
|
export interface WindowApi {
|
|
562
686
|
/**
|
|
563
687
|
* `gravity` is which end short content settles against: `"start"` (the default) pins it to the
|
|
@@ -568,11 +692,17 @@ export interface WindowApi {
|
|
|
568
692
|
* wrapped a long path would reflow every row below it — but a text field is prose and wants
|
|
569
693
|
* this on. A bottom dock that wraps also grows to show the folded rows, `size` acting as its
|
|
570
694
|
* floor.
|
|
695
|
+
*
|
|
696
|
+
* `pane` docks the window inside one pane of the main region rather than against the screen.
|
|
697
|
+
* The same four edges either way — a composer at the foot of a pane is this call with a `pane`,
|
|
698
|
+
* and a status line along the foot of the terminal is this call without one. Omit it for
|
|
699
|
+
* anything that belongs to the whole terminal: a sidebar docked into a pane would be a sidebar
|
|
700
|
+
* that disappears when you close that split.
|
|
571
701
|
*/
|
|
572
702
|
open(
|
|
573
703
|
buf: BufferId,
|
|
574
704
|
dock: Dock,
|
|
575
|
-
opts?: { size?: number; gravity?: Gravity; wrap?: boolean },
|
|
705
|
+
opts?: { size?: number; gravity?: Gravity; wrap?: boolean; pane?: PaneId },
|
|
576
706
|
): Promise<WindowId>;
|
|
577
707
|
close(win: WindowId): Promise<void>;
|
|
578
708
|
/**
|
|
@@ -596,6 +726,19 @@ export interface WindowApi {
|
|
|
596
726
|
* its first row at `0`. Anything else shows the same thing either way.
|
|
597
727
|
*/
|
|
598
728
|
scrollTo(win: WindowId, topLine: number | null): Promise<void>;
|
|
729
|
+
/**
|
|
730
|
+
* Move a window's scroll by a line, a half screen, a screen, or all the way to an end.
|
|
731
|
+
*
|
|
732
|
+
* What {@link scrollTo} cannot express, because the arithmetic is not yours to do: "half a screen"
|
|
733
|
+
* is counted in the buffer rows the frontend actually drew — which on a wrapping window is fewer
|
|
734
|
+
* than its height — and the floor is the last screenful rather than the last line, so the bottom
|
|
735
|
+
* is a full panel and not one row above an empty one. Both numbers are a frame old by the time
|
|
736
|
+
* {@link viewport} hands them to you; this reads them where they live.
|
|
737
|
+
*
|
|
738
|
+
* The keys bound on `neosh.scroll` run exactly this. Call it directly for a scroll a key did not
|
|
739
|
+
* ask for — following output, or a button on a panel of your own.
|
|
740
|
+
*/
|
|
741
|
+
scroll(win: WindowId, amount: ScrollAmount): Promise<void>;
|
|
599
742
|
/**
|
|
600
743
|
* How big this window actually is, in cells.
|
|
601
744
|
*
|
|
@@ -1059,12 +1202,52 @@ export interface GitApi {
|
|
|
1059
1202
|
stage(paths?: string[]): Promise<void>;
|
|
1060
1203
|
unstage(paths?: string[]): Promise<void>;
|
|
1061
1204
|
commit(message: string): Promise<CommitInfo>;
|
|
1205
|
+
/**
|
|
1206
|
+
* `git fetch --prune`, answering with where the tree stands *after* it.
|
|
1207
|
+
*
|
|
1208
|
+
* **The one call that makes `ahead` and `behind` mean anything.** `status()` reads them off
|
|
1209
|
+
* `git status --branch`, which compares HEAD with the remote-tracking ref sitting on this disk —
|
|
1210
|
+
* so a panel drawing `↓0` from it is reporting the state of the world as of whenever this
|
|
1211
|
+
* checkout last spoke to a remote. Call this first and the number is news; do not, and it is
|
|
1212
|
+
* archaeology.
|
|
1213
|
+
*
|
|
1214
|
+
* It answers with the fresh {@link RepoStatus} rather than nothing, so "fetch and see where I am"
|
|
1215
|
+
* is one round trip. Prunes, because a picker offering six branches that were deleted when their
|
|
1216
|
+
* pull requests merged is worse than one that is a fetch behind.
|
|
1217
|
+
*
|
|
1218
|
+
* A write, and the reason is the network rather than the working tree, which does not move:
|
|
1219
|
+
* this contacts a remote and writes refs. It fails rather than hangs when there are no
|
|
1220
|
+
* credentials — stdin is closed and every askpass unset — which is what makes it safe to put on
|
|
1221
|
+
* a timer. Expect it to reject routinely: no remote, no network, a key the host will not take.
|
|
1222
|
+
*/
|
|
1223
|
+
fetch(opts?: { cwd?: string }): Promise<RepoStatus>;
|
|
1224
|
+
/**
|
|
1225
|
+
* Every pull request the forge knows about for this repository, open and closed.
|
|
1226
|
+
*
|
|
1227
|
+
* **One call answers for every checkout.** They come back keyed by
|
|
1228
|
+
* {@link PullRequest.branch}, so a repository with six worktrees is one request and six lookups
|
|
1229
|
+
* — which is the difference between a panel that costs a round trip every few minutes and one
|
|
1230
|
+
* that costs six.
|
|
1231
|
+
*
|
|
1232
|
+
* Answered by `gh`, the CLI the user has already signed into, so tokens, enterprise hosts, SSO
|
|
1233
|
+
* and rate limits are already handled and the answer is the one they get in their own terminal.
|
|
1234
|
+
*
|
|
1235
|
+
* **Rejects rather than answering empty** when it cannot know — no `gh`, not signed in, no
|
|
1236
|
+
* remote on a forge. An empty list is the claim "this repository has no pull requests", and
|
|
1237
|
+
* three of the four ways this fails would be making it falsely. Catch it, say it once, and do
|
|
1238
|
+
* not say it again every tick.
|
|
1239
|
+
*/
|
|
1240
|
+
pulls(opts?: { cwd?: string }): Promise<PullRequest[]>;
|
|
1062
1241
|
/**
|
|
1063
1242
|
* `git pull`, answering with git's own summary — "Already up to date.", the fast-forward range —
|
|
1064
1243
|
* because those are different answers and a caller showing neither is a caller nobody trusts.
|
|
1065
1244
|
* `cwd` picks the repository, as everywhere; absent means the conversation's own.
|
|
1245
|
+
*
|
|
1246
|
+
* `rebase` replays this branch's commits on top of what arrived instead of merging them. It is
|
|
1247
|
+
* the answer to a diverged branch and it rewrites local commits, so ask before you set it — the
|
|
1248
|
+
* caller this exists for is a panel that has just said "diverged" and offered the choice.
|
|
1066
1249
|
*/
|
|
1067
|
-
pull(opts?: { cwd?: string }): Promise<string>;
|
|
1250
|
+
pull(opts?: { cwd?: string; rebase?: boolean }): Promise<string>;
|
|
1068
1251
|
addWorktree(
|
|
1069
1252
|
path: string,
|
|
1070
1253
|
branch: string,
|
|
@@ -1076,6 +1259,60 @@ export interface GitApi {
|
|
|
1076
1259
|
* exactly that one.
|
|
1077
1260
|
*/
|
|
1078
1261
|
removeWorktree(path: string, opts?: { force?: boolean; cwd?: string }): Promise<void>;
|
|
1262
|
+
/**
|
|
1263
|
+
* Move a worktree to `dest` — `git worktree move`, and everything that has to follow it.
|
|
1264
|
+
*
|
|
1265
|
+
* A worktree's path is an identity here, not a coordinate: it is the key of the project facts
|
|
1266
|
+
* every list draws, of the conversations living in it, of the project vars holding a pin and a
|
|
1267
|
+
* fold state, and of the working directory each vendor CLI was started in. This call moves all
|
|
1268
|
+
* of them, which is why it is a call rather than a `git worktree move` you could have run
|
|
1269
|
+
* yourself — the git part is the part that was never hard.
|
|
1270
|
+
*
|
|
1271
|
+
* `dest` is the full path it lands at. Its parent is created; the leaf must not exist.
|
|
1272
|
+
*
|
|
1273
|
+
* Refused while a turn is running anywhere in the tree, because a CLI holds its working
|
|
1274
|
+
* directory from the moment it is spawned and would otherwise write the file it is editing into
|
|
1275
|
+
* the place the tree used to be. Interrupt it and ask again.
|
|
1276
|
+
*
|
|
1277
|
+
* `cwd` names the repository, as everywhere.
|
|
1278
|
+
*/
|
|
1279
|
+
moveWorktree(path: string, dest: string, opts?: { cwd?: string }): Promise<void>;
|
|
1280
|
+
/**
|
|
1281
|
+
* Clone `url` into `path`, resolving to the path once it is there.
|
|
1282
|
+
*
|
|
1283
|
+
* The one call here with no `cwd`: everything else asks a repository a question, and this one
|
|
1284
|
+
* arrives before there is a repository to ask. `path` is absolute and its parent need not
|
|
1285
|
+
* exist — cloning into a location you have just invented is the ordinary case.
|
|
1286
|
+
*
|
|
1287
|
+
* **It reports progress while it runs**, on the bus as {@link CLONE_EVENT}, so a caller that
|
|
1288
|
+
* wants to draw a clone rather than block on one subscribes before awaiting this. Keyed by
|
|
1289
|
+
* `path`, because a workspace may be cloning two things at once.
|
|
1290
|
+
*
|
|
1291
|
+
* Needs `vcs_write` in the manifest, like every other call here that changes a disk.
|
|
1292
|
+
*/
|
|
1293
|
+
clone(url: string, path: string): Promise<string>;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* What {@link GitApi.clone} says about itself while it runs, on {@link EventsApi.on}.
|
|
1298
|
+
*
|
|
1299
|
+
* `phase` is git's own word for what it is doing — `Receiving objects`, `Resolving deltas` — and
|
|
1300
|
+
* `percent` is absent for the phases that have no total, which draw as a spinner rather than as a
|
|
1301
|
+
* bar. `done` arrives exactly once per clone, on success and on failure alike, because a panel
|
|
1302
|
+
* drawing itself from these has no other way to learn it may stop.
|
|
1303
|
+
*/
|
|
1304
|
+
export const CLONE_EVENT = "neosh.git.clone";
|
|
1305
|
+
|
|
1306
|
+
/** One {@link CLONE_EVENT} payload. */
|
|
1307
|
+
export interface CloneProgress {
|
|
1308
|
+
/** Which clone this is about. The key, since two may be running. */
|
|
1309
|
+
path: string;
|
|
1310
|
+
url: string;
|
|
1311
|
+
phase: string;
|
|
1312
|
+
percent?: number | null;
|
|
1313
|
+
done?: boolean;
|
|
1314
|
+
/** Present, with git's own last word in it, only when the clone failed. */
|
|
1315
|
+
error?: string;
|
|
1079
1316
|
}
|
|
1080
1317
|
|
|
1081
1318
|
/**
|
|
@@ -1097,6 +1334,32 @@ export interface GenApi {
|
|
|
1097
1334
|
* do not each reimplement that. Rejects if there is no JSON in the response at all.
|
|
1098
1335
|
*/
|
|
1099
1336
|
json<T = unknown>(prompt: string, opts?: { system?: string; selection?: ModelSelection }): Promise<T>;
|
|
1337
|
+
/**
|
|
1338
|
+
* One value, asked for as JSON and accepted however it comes back.
|
|
1339
|
+
*
|
|
1340
|
+
* The shape almost every generating plugin actually wants: a branch name, a thread title, a PR
|
|
1341
|
+
* subject — one string, asked for as `{"branch": …}` because that is how you pin a model down,
|
|
1342
|
+
* and answered as a bare `fix/composer-paste` often enough to matter. Both are the same answer,
|
|
1343
|
+
* and {@link GenApi.json} rejects the second one — which is a correct name thrown away, with
|
|
1344
|
+
* nothing on screen to say so.
|
|
1345
|
+
*
|
|
1346
|
+
* So the key is passed down and a bare reply is read as its value. Everything
|
|
1347
|
+
* {@link GenApi.json} tolerates is tolerated first and unchanged; this is only what happens when
|
|
1348
|
+
* there is no JSON at all. Rejects on an empty answer, or on prose it will not guess at.
|
|
1349
|
+
*
|
|
1350
|
+
* For one value only. A commit message is a subject and a body, and there is no answering the
|
|
1351
|
+
* question of which one a lone paragraph is — that stays {@link GenApi.json}.
|
|
1352
|
+
*
|
|
1353
|
+
* And only for a value you would know was wrong on sight. A branch name is that, and a wrong one
|
|
1354
|
+
* is one rename away; a thread title is any short line, and so is a refusal or a driver's own
|
|
1355
|
+
* error message — where nothing distinguishes an answer from a remark, the envelope is the
|
|
1356
|
+
* evidence, and {@link GenApi.json} is the call.
|
|
1357
|
+
*/
|
|
1358
|
+
field(
|
|
1359
|
+
prompt: string,
|
|
1360
|
+
key: string,
|
|
1361
|
+
opts?: { system?: string; selection?: ModelSelection },
|
|
1362
|
+
): Promise<string>;
|
|
1100
1363
|
}
|
|
1101
1364
|
|
|
1102
1365
|
/**
|
|
@@ -1356,6 +1619,27 @@ export interface VarApi {
|
|
|
1356
1619
|
): Disposable;
|
|
1357
1620
|
}
|
|
1358
1621
|
|
|
1622
|
+
/**
|
|
1623
|
+
* The directories somebody works in, and the one thing that can happen to one.
|
|
1624
|
+
*
|
|
1625
|
+
* A project *is* its path everywhere else in this API — `projectScope` keys vars by it, a panel's
|
|
1626
|
+
* list is a list of them, `SessionInfo.cwd` names one. Which is exactly why a path that changes
|
|
1627
|
+
* needs saying out loud rather than inferring: from the outside, a worktree that moved and a
|
|
1628
|
+
* project that was deleted while another was added are the same two facts in the same order.
|
|
1629
|
+
*/
|
|
1630
|
+
export interface ProjectApi {
|
|
1631
|
+
/**
|
|
1632
|
+
* A project's directory is now somewhere else — a worktree that was relocated.
|
|
1633
|
+
*
|
|
1634
|
+
* The host has already moved everything it owns by the time this arrives: the conversations, the
|
|
1635
|
+
* project vars, the names and branches every list draws. What is left is whatever *you* keyed by
|
|
1636
|
+
* the old path — a list of directories, a cache, a decoration target — and the point of getting
|
|
1637
|
+
* both ends in one event is that you can re-key in place instead of dropping a row and gaining a
|
|
1638
|
+
* stranger.
|
|
1639
|
+
*/
|
|
1640
|
+
onMove(cb: (e: { from: string; to: string }) => void): Disposable;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1359
1643
|
/** Sugar for the two scopes anything with a panel spends its time in. */
|
|
1360
1644
|
export function projectScope(cwd: string): VarScope {
|
|
1361
1645
|
return { scope: "project", cwd };
|
|
@@ -1612,8 +1896,29 @@ export interface SwarmApi {
|
|
|
1612
1896
|
* with the owner's reason when it says no. A node may refuse anything; `NodeCapabilities` on its
|
|
1613
1897
|
* {@link SwarmNode} says in advance what it is likely to accept, so a menu can grey out a verb
|
|
1614
1898
|
* rather than offering one that will bounce.
|
|
1899
|
+
*
|
|
1900
|
+
* Answers with the conversation a `new_session` created, and `null` for everything else — which
|
|
1901
|
+
* is what makes starting something over there a place you can then go: pass it to
|
|
1902
|
+
* {@link subscribe}, or to `swarm.open`, without waiting for the next inventory to notice it.
|
|
1903
|
+
*/
|
|
1904
|
+
command(node: NodeId, session: string, command: AgentCommand): Promise<string | null>;
|
|
1905
|
+
/**
|
|
1906
|
+
* Which directories on another machine start with `prefix` — `path.complete`, asked over there.
|
|
1907
|
+
*
|
|
1908
|
+
* The same answers in the same shape as the local one: each entry as you would have typed it,
|
|
1909
|
+
* trailing separator and all, so a single field can complete against either machine and the only
|
|
1910
|
+
* thing that changes is which one is asked. Directory names only.
|
|
1911
|
+
*
|
|
1912
|
+
* {@link NodeCapabilities.projects} is what a machine *offers*, and it is the short list of
|
|
1913
|
+
* places it already works in; this is how you reach the directory over there that neither machine
|
|
1914
|
+
* has ever opened. Rejects when the peer is not connected, when it does not accept commands, and
|
|
1915
|
+
* when it is running a neosh too old to have heard of the question — the message says which, and
|
|
1916
|
+
* an empty list is a directory with nothing in it rather than a failure.
|
|
1917
|
+
*
|
|
1918
|
+
* Passing this machine's own node id answers locally, so a picker that walks `nodes()` needs no
|
|
1919
|
+
* special case for the row that is itself.
|
|
1615
1920
|
*/
|
|
1616
|
-
|
|
1921
|
+
browse(node: NodeId, prefix?: string): Promise<string[]>;
|
|
1617
1922
|
/**
|
|
1618
1923
|
* Watch a remote conversation: its history now, then everything as it happens, delivered to
|
|
1619
1924
|
* {@link onStream}.
|
|
@@ -1706,8 +2011,15 @@ export interface PathApi {
|
|
|
1706
2011
|
*
|
|
1707
2012
|
* `~` expands against the home directory. A prefix with no `/` completes against the active
|
|
1708
2013
|
* conversation's directory, which is what someone typing `src` means.
|
|
2014
|
+
*
|
|
2015
|
+
* `denied` is set when the list is empty *because the operating system refused*, rather than
|
|
2016
|
+
* because nothing matched. The two are the same empty array and they are not the same answer:
|
|
2017
|
+
* macOS grants folder access to the **terminal**, so `~/Documents` full of projects reads as
|
|
2018
|
+
* empty until somebody ticks a box, and a field that draws "no directory matches" over that
|
|
2019
|
+
* sends people to go and check a path that was right all along. When it is set it says which
|
|
2020
|
+
* folder, which application and which System Settings pane, and it is meant to be shown.
|
|
1709
2021
|
*/
|
|
1710
|
-
complete(prefix: string): Promise<string[]>;
|
|
2022
|
+
complete(prefix: string): Promise<{ paths: string[]; denied?: string }>;
|
|
1711
2023
|
}
|
|
1712
2024
|
|
|
1713
2025
|
export interface TimerApi {
|
|
@@ -1755,6 +2067,7 @@ interface Registered {
|
|
|
1755
2067
|
composerListeners: Array<(e: { text: string }) => void>;
|
|
1756
2068
|
activityListeners: Array<(e: { session: SessionId; turn: string; activity: Activity }) => void>;
|
|
1757
2069
|
varListeners: Array<(e: { scope: VarScope; key: string; value: unknown }) => void>;
|
|
2070
|
+
projectMovedListeners: Array<(e: { from: string; to: string }) => void>;
|
|
1758
2071
|
swarmListeners: Array<() => void>;
|
|
1759
2072
|
quotaListeners: Array<(snapshot: QuotaSnapshot) => void>;
|
|
1760
2073
|
swarmStreamListeners: Array<
|
|
@@ -1801,6 +2114,7 @@ function reg(plugin: string): Registered {
|
|
|
1801
2114
|
composerListeners: [],
|
|
1802
2115
|
activityListeners: [],
|
|
1803
2116
|
varListeners: [],
|
|
2117
|
+
projectMovedListeners: [],
|
|
1804
2118
|
swarmListeners: [],
|
|
1805
2119
|
quotaListeners: [],
|
|
1806
2120
|
swarmStreamListeners: [],
|
|
@@ -1852,9 +2166,11 @@ function floatConfig(o: FloatOptions = {}): FloatConfig {
|
|
|
1852
2166
|
border: o.border ?? "rounded",
|
|
1853
2167
|
border_hl: o.borderHl ?? null,
|
|
1854
2168
|
title: o.title ?? null,
|
|
2169
|
+
footer: o.footer ?? null,
|
|
1855
2170
|
close_on_blur: o.closeOnBlur ?? false,
|
|
1856
2171
|
focusable: o.focusable ?? true,
|
|
1857
2172
|
modal: o.modal ?? false,
|
|
2173
|
+
scroll: o.scroll ?? false,
|
|
1858
2174
|
};
|
|
1859
2175
|
}
|
|
1860
2176
|
|
|
@@ -1979,7 +2295,8 @@ function build(
|
|
|
1979
2295
|
},
|
|
1980
2296
|
path: {
|
|
1981
2297
|
async complete(prefix) {
|
|
1982
|
-
|
|
2298
|
+
const answer = expect(await c({ call: "path_complete", prefix }), "paths");
|
|
2299
|
+
return { paths: answer.paths, denied: answer.denied ?? undefined };
|
|
1983
2300
|
},
|
|
1984
2301
|
},
|
|
1985
2302
|
state: {
|
|
@@ -2012,6 +2329,11 @@ function build(
|
|
|
2012
2329
|
return listener(r.varListeners, cb);
|
|
2013
2330
|
},
|
|
2014
2331
|
},
|
|
2332
|
+
project: {
|
|
2333
|
+
onMove(cb) {
|
|
2334
|
+
return listener(r.projectMovedListeners, cb);
|
|
2335
|
+
},
|
|
2336
|
+
},
|
|
2015
2337
|
ext: {
|
|
2016
2338
|
async contribute(point, id, item, opts) {
|
|
2017
2339
|
await c({ call: "ext_contribute", point, id, item, priority: opts?.priority ?? 0 });
|
|
@@ -2080,7 +2402,13 @@ function build(
|
|
|
2080
2402
|
return expect(await c({ call: "swarm_hosts_of", project }), "names").names;
|
|
2081
2403
|
},
|
|
2082
2404
|
async command(node, session, command) {
|
|
2083
|
-
await c({ call: "swarm_command", node, session, command });
|
|
2405
|
+
const done = await c({ call: "swarm_command", node, session, command });
|
|
2406
|
+
// Only `new_session` says anything, so an `ok` of any other shape is an older peer
|
|
2407
|
+
// answering a command that had nothing to report — which is a success, not a mismatch.
|
|
2408
|
+
return done.ok === "swarm_commanded" ? done.session ?? null : null;
|
|
2409
|
+
},
|
|
2410
|
+
async browse(node, prefix) {
|
|
2411
|
+
return expect(await c({ call: "swarm_browse", node, prefix: prefix ?? "" }), "paths").paths;
|
|
2084
2412
|
},
|
|
2085
2413
|
async subscribe(node, session) {
|
|
2086
2414
|
await c({ call: "swarm_subscribe", node, session });
|
|
@@ -2272,10 +2600,79 @@ function build(
|
|
|
2272
2600
|
return listener(list, cb);
|
|
2273
2601
|
},
|
|
2274
2602
|
},
|
|
2603
|
+
pane: {
|
|
2604
|
+
async active(view) {
|
|
2605
|
+
const { tabs, active } = expect(await c({ call: "tab_list", view }), "tabs");
|
|
2606
|
+
// The active tab, or the first — a terminal always has at least one, and a `tabs` that
|
|
2607
|
+
// came back empty is a workspace that has gone wrong in a way this cannot paper over.
|
|
2608
|
+
const tab = tabs.find((t) => t.id === active) ?? tabs[0];
|
|
2609
|
+
if (!tab) throw new Error("this terminal has no tabs");
|
|
2610
|
+
return tab.active_pane;
|
|
2611
|
+
},
|
|
2612
|
+
async split(pane, dir) {
|
|
2613
|
+
const p = expect(await c({ call: "pane_split", pane, dir }), "pane").pane;
|
|
2614
|
+
if (p === null) throw new Error(`pane ${pane} could not be split`);
|
|
2615
|
+
return p;
|
|
2616
|
+
},
|
|
2617
|
+
async close(pane) {
|
|
2618
|
+
await c({ call: "pane_close", pane });
|
|
2619
|
+
},
|
|
2620
|
+
async focus(pane) {
|
|
2621
|
+
await c({ call: "pane_focus", pane });
|
|
2622
|
+
},
|
|
2623
|
+
async focusDir(dir, view) {
|
|
2624
|
+
return expect(await c({ call: "pane_focus_dir", dir, view }), "pane").pane;
|
|
2625
|
+
},
|
|
2626
|
+
async resize(pane, dir, delta) {
|
|
2627
|
+
await c({ call: "pane_resize", pane, dir, delta });
|
|
2628
|
+
},
|
|
2629
|
+
async swap(pane, withPane) {
|
|
2630
|
+
await c({ call: "pane_swap", pane, with: withPane });
|
|
2631
|
+
},
|
|
2632
|
+
async moveToEdge(pane, dir) {
|
|
2633
|
+
await c({ call: "pane_move_edge", pane, dir });
|
|
2634
|
+
},
|
|
2635
|
+
async equalize(view) {
|
|
2636
|
+
await c({ call: "pane_equalize", view });
|
|
2637
|
+
},
|
|
2638
|
+
},
|
|
2639
|
+
tab: {
|
|
2640
|
+
async list(view) {
|
|
2641
|
+
const r = expect(await c({ call: "tab_list", view }), "tabs");
|
|
2642
|
+
return { tabs: r.tabs, active: r.active };
|
|
2643
|
+
},
|
|
2644
|
+
async create(opts) {
|
|
2645
|
+
return expect(
|
|
2646
|
+
await c({
|
|
2647
|
+
call: "tab_new",
|
|
2648
|
+
view: opts?.view,
|
|
2649
|
+
title: opts?.title,
|
|
2650
|
+
activate: opts?.activate ?? true,
|
|
2651
|
+
}),
|
|
2652
|
+
"tab",
|
|
2653
|
+
).tab;
|
|
2654
|
+
},
|
|
2655
|
+
async close(tab) {
|
|
2656
|
+
await c({ call: "tab_close", tab });
|
|
2657
|
+
},
|
|
2658
|
+
async select(tab) {
|
|
2659
|
+
await c({ call: "tab_select", tab });
|
|
2660
|
+
},
|
|
2661
|
+
async step(delta, view) {
|
|
2662
|
+
await c({ call: "tab_step", delta, view });
|
|
2663
|
+
},
|
|
2664
|
+
async move(tab, to) {
|
|
2665
|
+
await c({ call: "tab_move", tab, to });
|
|
2666
|
+
},
|
|
2667
|
+
async rename(tab, title) {
|
|
2668
|
+
await c({ call: "tab_rename", tab, title });
|
|
2669
|
+
},
|
|
2670
|
+
},
|
|
2275
2671
|
win: {
|
|
2276
2672
|
async open(buf, dock, opts) {
|
|
2277
2673
|
const layout: WindowLayout = {
|
|
2278
2674
|
kind: "docked",
|
|
2675
|
+
pane: opts?.pane ?? null,
|
|
2279
2676
|
dock,
|
|
2280
2677
|
size: opts?.size ?? null,
|
|
2281
2678
|
gravity: opts?.gravity ?? "start",
|
|
@@ -2302,6 +2699,9 @@ function build(
|
|
|
2302
2699
|
async scrollTo(win, topLine) {
|
|
2303
2700
|
await c({ call: "win_scroll_to", win, top_line: topLine });
|
|
2304
2701
|
},
|
|
2702
|
+
async scroll(win, amount) {
|
|
2703
|
+
await c({ call: "win_scroll", win, amount });
|
|
2704
|
+
},
|
|
2305
2705
|
async viewport(win) {
|
|
2306
2706
|
return expect(await c({ call: "win_get_viewport", win }), "viewport").viewport ?? null;
|
|
2307
2707
|
},
|
|
@@ -2620,8 +3020,16 @@ function build(
|
|
|
2620
3020
|
cwd: opts?.cwd ?? null,
|
|
2621
3021
|
});
|
|
2622
3022
|
},
|
|
3023
|
+
async fetch(opts) {
|
|
3024
|
+
const v = await c({ call: "git_fetch", cwd: opts?.cwd ?? null });
|
|
3025
|
+
return expect(v, "status").status;
|
|
3026
|
+
},
|
|
3027
|
+
async pulls(opts) {
|
|
3028
|
+
const v = await c({ call: "forge_pulls", cwd: opts?.cwd ?? null });
|
|
3029
|
+
return expect(v, "pulls").pulls;
|
|
3030
|
+
},
|
|
2623
3031
|
async pull(opts) {
|
|
2624
|
-
const v = await c({ call: "git_pull", cwd: opts?.cwd ?? null });
|
|
3032
|
+
const v = await c({ call: "git_pull", cwd: opts?.cwd ?? null, rebase: opts?.rebase ?? false });
|
|
2625
3033
|
return expect(v, "text").text;
|
|
2626
3034
|
},
|
|
2627
3035
|
async removeWorktree(path, opts) {
|
|
@@ -2632,6 +3040,13 @@ function build(
|
|
|
2632
3040
|
cwd: opts?.cwd ?? null,
|
|
2633
3041
|
});
|
|
2634
3042
|
},
|
|
3043
|
+
async moveWorktree(path, dest, opts) {
|
|
3044
|
+
await c({ call: "git_move_worktree", path, dest, cwd: opts?.cwd ?? null });
|
|
3045
|
+
},
|
|
3046
|
+
async clone(url, path) {
|
|
3047
|
+
const v = await c({ call: "git_clone", url, path });
|
|
3048
|
+
return expect(v, "text").text;
|
|
3049
|
+
},
|
|
2635
3050
|
},
|
|
2636
3051
|
gen: {
|
|
2637
3052
|
async complete(prompt, opts) {
|
|
@@ -2654,6 +3069,23 @@ function build(
|
|
|
2654
3069
|
});
|
|
2655
3070
|
return expect(v, "json").value as never;
|
|
2656
3071
|
},
|
|
3072
|
+
async field(prompt, key, opts) {
|
|
3073
|
+
const v = await c({
|
|
3074
|
+
call: "gen_complete",
|
|
3075
|
+
prompt,
|
|
3076
|
+
system: opts?.system ?? null,
|
|
3077
|
+
json: true,
|
|
3078
|
+
field: key,
|
|
3079
|
+
selection: opts?.selection ?? null,
|
|
3080
|
+
});
|
|
3081
|
+
const value = (expect(v, "json").value as Record<string, unknown>)?.[key];
|
|
3082
|
+
// A key that came back as something other than a non-empty string is an answer to a
|
|
3083
|
+
// different question, and the caller is about to name a branch after it.
|
|
3084
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
3085
|
+
throw new Error(`the model returned no ${key}`);
|
|
3086
|
+
}
|
|
3087
|
+
return value.trim();
|
|
3088
|
+
},
|
|
2657
3089
|
},
|
|
2658
3090
|
view: {
|
|
2659
3091
|
async list() {
|
|
@@ -2969,6 +3401,9 @@ async function dispatchEvent(
|
|
|
2969
3401
|
for (const cb of r.varListeners)
|
|
2970
3402
|
cb({ scope: ev.scope, key: ev.key, value: ev.value });
|
|
2971
3403
|
break;
|
|
3404
|
+
case "project_moved":
|
|
3405
|
+
for (const cb of r.projectMovedListeners) cb({ from: ev.from, to: ev.to });
|
|
3406
|
+
break;
|
|
2972
3407
|
case "quota":
|
|
2973
3408
|
for (const cb of [...r.quotaListeners]) cb(ev.snapshot);
|
|
2974
3409
|
break;
|