@neosh/api 0.4.0 → 0.4.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/src/index.ts CHANGED
@@ -36,6 +36,7 @@ import type { CredentialSource } from "./generated/CredentialSource";
36
36
  import type { CursorMotion } from "./generated/CursorMotion";
37
37
  import type { CursorShape } from "./generated/CursorShape";
38
38
  import type { SelectShape } from "./generated/SelectShape";
39
+ import type { ScrollAmount } from "./generated/ScrollAmount";
39
40
  import type { DiffTarget } from "./generated/DiffTarget";
40
41
  import type { Dock } from "./generated/Dock";
41
42
  import type { Gravity } from "./generated/Gravity";
@@ -136,6 +137,9 @@ import type { Viewport } from "./generated/Viewport";
136
137
  import type { WindowId } from "./generated/WindowId";
137
138
  import type { WindowInfo } from "./generated/WindowInfo";
138
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";
139
143
  import type { WorktreeInfo } from "./generated/WorktreeInfo";
140
144
 
141
145
  export type {
@@ -153,12 +157,15 @@ export type {
153
157
  CostBasis, QuotaCredits, QuotaSample, QuotaSeverity, QuotaSnapshot, QuotaSource, QuotaWindow,
154
158
  InstallMethod, UpdateOutcome, UpdateStatus,
155
159
  UsageBucket, UsageHistory, UsageResolution, UsageScanSource,
156
- Rect, RepoInfo, RepoStatus, SelectShape, SessionId, SessionInfo, StatusAlign, StatusSegment, StopReason,
160
+ Rect, RepoInfo, RepoStatus, ScrollAmount, SelectShape, SessionId, SessionInfo, StatusAlign, StatusSegment, StopReason,
157
161
  SurfaceCell, SurfaceId, TextEdit, ToolCall, ToolDef, ToolResult, TurnRequest, Usage,
158
162
  NodeCapabilities, NodeId, NodeInfo, ProjectKey, RemoteProject, StreamEvent,
159
163
  SwarmAgent, SwarmNode, SwarmStranger,
160
164
  VarScope, ViewId, ViewInfo, Viewport,
161
165
  WindowId, WindowInfo, WindowLayout,
166
+ ChecksState,
167
+ PullRequest,
168
+ PullState,
162
169
  WorktreeInfo,
163
170
  };
164
171
 
@@ -368,6 +375,16 @@ export interface FloatOptions {
368
375
  border?: FloatConfig["border"];
369
376
  borderHl?: string;
370
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;
371
388
  closeOnBlur?: boolean;
372
389
  focusable?: boolean;
373
390
  /**
@@ -384,6 +401,24 @@ export interface FloatOptions {
384
401
  * control sheet. Not for a hint or a hover card.
385
402
  */
386
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;
387
422
  }
388
423
 
389
424
  export interface Neosh {
@@ -415,6 +450,8 @@ export interface Neosh {
415
450
  readonly opt: OptionApi;
416
451
  readonly state: StateApi;
417
452
  readonly vars: VarApi;
453
+ /** The directories somebody works in. See {@link ProjectApi}. */
454
+ readonly project: ProjectApi;
418
455
  readonly ext: ExtensionApi;
419
456
  readonly event: EventApi;
420
457
  readonly swarm: SwarmApi;
@@ -625,21 +662,63 @@ export interface PaneApi {
625
662
  equalize(view?: ViewId): Promise<void>;
626
663
  }
627
664
 
628
- /** 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
+ */
629
678
  export interface TabApi {
630
- /** 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
+ */
631
685
  list(view?: ViewId): Promise<{ tabs: TabInfo[]; active: TabId }>;
632
686
  /**
633
687
  * Open a tab with one empty pane. `activate` goes to it — `false` is for putting work somewhere
634
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.
635
705
  */
636
- create(opts?: { title?: string; activate?: boolean; view?: ViewId }): Promise<TabId>;
637
- /** 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. */
638
708
  close(tab: TabId): Promise<void>;
639
709
  select(tab: TabId): Promise<void>;
640
- /** 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
+ */
641
715
  step(delta: number, view?: ViewId): Promise<void>;
642
- /** 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
+ */
643
722
  move(tab: TabId, to: number): Promise<void>;
644
723
  /** Name a tab, or pass `null` to give it back its derived name. */
645
724
  rename(tab: TabId, title: string | null): Promise<void>;
@@ -689,6 +768,19 @@ export interface WindowApi {
689
768
  * its first row at `0`. Anything else shows the same thing either way.
690
769
  */
691
770
  scrollTo(win: WindowId, topLine: number | null): Promise<void>;
771
+ /**
772
+ * Move a window's scroll by a line, a half screen, a screen, or all the way to an end.
773
+ *
774
+ * What {@link scrollTo} cannot express, because the arithmetic is not yours to do: "half a screen"
775
+ * is counted in the buffer rows the frontend actually drew — which on a wrapping window is fewer
776
+ * than its height — and the floor is the last screenful rather than the last line, so the bottom
777
+ * is a full panel and not one row above an empty one. Both numbers are a frame old by the time
778
+ * {@link viewport} hands them to you; this reads them where they live.
779
+ *
780
+ * The keys bound on `neosh.scroll` run exactly this. Call it directly for a scroll a key did not
781
+ * ask for — following output, or a button on a panel of your own.
782
+ */
783
+ scroll(win: WindowId, amount: ScrollAmount): Promise<void>;
692
784
  /**
693
785
  * How big this window actually is, in cells.
694
786
  *
@@ -1152,12 +1244,52 @@ export interface GitApi {
1152
1244
  stage(paths?: string[]): Promise<void>;
1153
1245
  unstage(paths?: string[]): Promise<void>;
1154
1246
  commit(message: string): Promise<CommitInfo>;
1247
+ /**
1248
+ * `git fetch --prune`, answering with where the tree stands *after* it.
1249
+ *
1250
+ * **The one call that makes `ahead` and `behind` mean anything.** `status()` reads them off
1251
+ * `git status --branch`, which compares HEAD with the remote-tracking ref sitting on this disk —
1252
+ * so a panel drawing `↓0` from it is reporting the state of the world as of whenever this
1253
+ * checkout last spoke to a remote. Call this first and the number is news; do not, and it is
1254
+ * archaeology.
1255
+ *
1256
+ * It answers with the fresh {@link RepoStatus} rather than nothing, so "fetch and see where I am"
1257
+ * is one round trip. Prunes, because a picker offering six branches that were deleted when their
1258
+ * pull requests merged is worse than one that is a fetch behind.
1259
+ *
1260
+ * A write, and the reason is the network rather than the working tree, which does not move:
1261
+ * this contacts a remote and writes refs. It fails rather than hangs when there are no
1262
+ * credentials — stdin is closed and every askpass unset — which is what makes it safe to put on
1263
+ * a timer. Expect it to reject routinely: no remote, no network, a key the host will not take.
1264
+ */
1265
+ fetch(opts?: { cwd?: string }): Promise<RepoStatus>;
1266
+ /**
1267
+ * Every pull request the forge knows about for this repository, open and closed.
1268
+ *
1269
+ * **One call answers for every checkout.** They come back keyed by
1270
+ * {@link PullRequest.branch}, so a repository with six worktrees is one request and six lookups
1271
+ * — which is the difference between a panel that costs a round trip every few minutes and one
1272
+ * that costs six.
1273
+ *
1274
+ * Answered by `gh`, the CLI the user has already signed into, so tokens, enterprise hosts, SSO
1275
+ * and rate limits are already handled and the answer is the one they get in their own terminal.
1276
+ *
1277
+ * **Rejects rather than answering empty** when it cannot know — no `gh`, not signed in, no
1278
+ * remote on a forge. An empty list is the claim "this repository has no pull requests", and
1279
+ * three of the four ways this fails would be making it falsely. Catch it, say it once, and do
1280
+ * not say it again every tick.
1281
+ */
1282
+ pulls(opts?: { cwd?: string }): Promise<PullRequest[]>;
1155
1283
  /**
1156
1284
  * `git pull`, answering with git's own summary — "Already up to date.", the fast-forward range —
1157
1285
  * because those are different answers and a caller showing neither is a caller nobody trusts.
1158
1286
  * `cwd` picks the repository, as everywhere; absent means the conversation's own.
1287
+ *
1288
+ * `rebase` replays this branch's commits on top of what arrived instead of merging them. It is
1289
+ * the answer to a diverged branch and it rewrites local commits, so ask before you set it — the
1290
+ * caller this exists for is a panel that has just said "diverged" and offered the choice.
1159
1291
  */
1160
- pull(opts?: { cwd?: string }): Promise<string>;
1292
+ pull(opts?: { cwd?: string; rebase?: boolean }): Promise<string>;
1161
1293
  addWorktree(
1162
1294
  path: string,
1163
1295
  branch: string,
@@ -1169,6 +1301,60 @@ export interface GitApi {
1169
1301
  * exactly that one.
1170
1302
  */
1171
1303
  removeWorktree(path: string, opts?: { force?: boolean; cwd?: string }): Promise<void>;
1304
+ /**
1305
+ * Move a worktree to `dest` — `git worktree move`, and everything that has to follow it.
1306
+ *
1307
+ * A worktree's path is an identity here, not a coordinate: it is the key of the project facts
1308
+ * every list draws, of the conversations living in it, of the project vars holding a pin and a
1309
+ * fold state, and of the working directory each vendor CLI was started in. This call moves all
1310
+ * of them, which is why it is a call rather than a `git worktree move` you could have run
1311
+ * yourself — the git part is the part that was never hard.
1312
+ *
1313
+ * `dest` is the full path it lands at. Its parent is created; the leaf must not exist.
1314
+ *
1315
+ * Refused while a turn is running anywhere in the tree, because a CLI holds its working
1316
+ * directory from the moment it is spawned and would otherwise write the file it is editing into
1317
+ * the place the tree used to be. Interrupt it and ask again.
1318
+ *
1319
+ * `cwd` names the repository, as everywhere.
1320
+ */
1321
+ moveWorktree(path: string, dest: string, opts?: { cwd?: string }): Promise<void>;
1322
+ /**
1323
+ * Clone `url` into `path`, resolving to the path once it is there.
1324
+ *
1325
+ * The one call here with no `cwd`: everything else asks a repository a question, and this one
1326
+ * arrives before there is a repository to ask. `path` is absolute and its parent need not
1327
+ * exist — cloning into a location you have just invented is the ordinary case.
1328
+ *
1329
+ * **It reports progress while it runs**, on the bus as {@link CLONE_EVENT}, so a caller that
1330
+ * wants to draw a clone rather than block on one subscribes before awaiting this. Keyed by
1331
+ * `path`, because a workspace may be cloning two things at once.
1332
+ *
1333
+ * Needs `vcs_write` in the manifest, like every other call here that changes a disk.
1334
+ */
1335
+ clone(url: string, path: string): Promise<string>;
1336
+ }
1337
+
1338
+ /**
1339
+ * What {@link GitApi.clone} says about itself while it runs, on {@link EventsApi.on}.
1340
+ *
1341
+ * `phase` is git's own word for what it is doing — `Receiving objects`, `Resolving deltas` — and
1342
+ * `percent` is absent for the phases that have no total, which draw as a spinner rather than as a
1343
+ * bar. `done` arrives exactly once per clone, on success and on failure alike, because a panel
1344
+ * drawing itself from these has no other way to learn it may stop.
1345
+ */
1346
+ export const CLONE_EVENT = "neosh.git.clone";
1347
+
1348
+ /** One {@link CLONE_EVENT} payload. */
1349
+ export interface CloneProgress {
1350
+ /** Which clone this is about. The key, since two may be running. */
1351
+ path: string;
1352
+ url: string;
1353
+ phase: string;
1354
+ percent?: number | null;
1355
+ done?: boolean;
1356
+ /** Present, with git's own last word in it, only when the clone failed. */
1357
+ error?: string;
1172
1358
  }
1173
1359
 
1174
1360
  /**
@@ -1190,6 +1376,32 @@ export interface GenApi {
1190
1376
  * do not each reimplement that. Rejects if there is no JSON in the response at all.
1191
1377
  */
1192
1378
  json<T = unknown>(prompt: string, opts?: { system?: string; selection?: ModelSelection }): Promise<T>;
1379
+ /**
1380
+ * One value, asked for as JSON and accepted however it comes back.
1381
+ *
1382
+ * The shape almost every generating plugin actually wants: a branch name, a thread title, a PR
1383
+ * subject — one string, asked for as `{"branch": …}` because that is how you pin a model down,
1384
+ * and answered as a bare `fix/composer-paste` often enough to matter. Both are the same answer,
1385
+ * and {@link GenApi.json} rejects the second one — which is a correct name thrown away, with
1386
+ * nothing on screen to say so.
1387
+ *
1388
+ * So the key is passed down and a bare reply is read as its value. Everything
1389
+ * {@link GenApi.json} tolerates is tolerated first and unchanged; this is only what happens when
1390
+ * there is no JSON at all. Rejects on an empty answer, or on prose it will not guess at.
1391
+ *
1392
+ * For one value only. A commit message is a subject and a body, and there is no answering the
1393
+ * question of which one a lone paragraph is — that stays {@link GenApi.json}.
1394
+ *
1395
+ * And only for a value you would know was wrong on sight. A branch name is that, and a wrong one
1396
+ * is one rename away; a thread title is any short line, and so is a refusal or a driver's own
1397
+ * error message — where nothing distinguishes an answer from a remark, the envelope is the
1398
+ * evidence, and {@link GenApi.json} is the call.
1399
+ */
1400
+ field(
1401
+ prompt: string,
1402
+ key: string,
1403
+ opts?: { system?: string; selection?: ModelSelection },
1404
+ ): Promise<string>;
1193
1405
  }
1194
1406
 
1195
1407
  /**
@@ -1449,6 +1661,27 @@ export interface VarApi {
1449
1661
  ): Disposable;
1450
1662
  }
1451
1663
 
1664
+ /**
1665
+ * The directories somebody works in, and the one thing that can happen to one.
1666
+ *
1667
+ * A project *is* its path everywhere else in this API — `projectScope` keys vars by it, a panel's
1668
+ * list is a list of them, `SessionInfo.cwd` names one. Which is exactly why a path that changes
1669
+ * needs saying out loud rather than inferring: from the outside, a worktree that moved and a
1670
+ * project that was deleted while another was added are the same two facts in the same order.
1671
+ */
1672
+ export interface ProjectApi {
1673
+ /**
1674
+ * A project's directory is now somewhere else — a worktree that was relocated.
1675
+ *
1676
+ * The host has already moved everything it owns by the time this arrives: the conversations, the
1677
+ * project vars, the names and branches every list draws. What is left is whatever *you* keyed by
1678
+ * the old path — a list of directories, a cache, a decoration target — and the point of getting
1679
+ * both ends in one event is that you can re-key in place instead of dropping a row and gaining a
1680
+ * stranger.
1681
+ */
1682
+ onMove(cb: (e: { from: string; to: string }) => void): Disposable;
1683
+ }
1684
+
1452
1685
  /** Sugar for the two scopes anything with a panel spends its time in. */
1453
1686
  export function projectScope(cwd: string): VarScope {
1454
1687
  return { scope: "project", cwd };
@@ -1600,6 +1833,17 @@ export interface UpdateApi {
1600
1833
  * {@link UpdateStatus.error} when it is there.
1601
1834
  */
1602
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>;
1603
1847
  /**
1604
1848
  * Update, by whichever route this install takes.
1605
1849
  *
@@ -1876,6 +2120,7 @@ interface Registered {
1876
2120
  composerListeners: Array<(e: { text: string }) => void>;
1877
2121
  activityListeners: Array<(e: { session: SessionId; turn: string; activity: Activity }) => void>;
1878
2122
  varListeners: Array<(e: { scope: VarScope; key: string; value: unknown }) => void>;
2123
+ projectMovedListeners: Array<(e: { from: string; to: string }) => void>;
1879
2124
  swarmListeners: Array<() => void>;
1880
2125
  quotaListeners: Array<(snapshot: QuotaSnapshot) => void>;
1881
2126
  swarmStreamListeners: Array<
@@ -1922,6 +2167,7 @@ function reg(plugin: string): Registered {
1922
2167
  composerListeners: [],
1923
2168
  activityListeners: [],
1924
2169
  varListeners: [],
2170
+ projectMovedListeners: [],
1925
2171
  swarmListeners: [],
1926
2172
  quotaListeners: [],
1927
2173
  swarmStreamListeners: [],
@@ -1973,9 +2219,11 @@ function floatConfig(o: FloatOptions = {}): FloatConfig {
1973
2219
  border: o.border ?? "rounded",
1974
2220
  border_hl: o.borderHl ?? null,
1975
2221
  title: o.title ?? null,
2222
+ footer: o.footer ?? null,
1976
2223
  close_on_blur: o.closeOnBlur ?? false,
1977
2224
  focusable: o.focusable ?? true,
1978
2225
  modal: o.modal ?? false,
2226
+ scroll: o.scroll ?? false,
1979
2227
  };
1980
2228
  }
1981
2229
 
@@ -2134,6 +2382,11 @@ function build(
2134
2382
  return listener(r.varListeners, cb);
2135
2383
  },
2136
2384
  },
2385
+ project: {
2386
+ onMove(cb) {
2387
+ return listener(r.projectMovedListeners, cb);
2388
+ },
2389
+ },
2137
2390
  ext: {
2138
2391
  async contribute(point, id, item, opts) {
2139
2392
  await c({ call: "ext_contribute", point, id, item, priority: opts?.priority ?? 0 });
@@ -2225,7 +2478,16 @@ function build(
2225
2478
  },
2226
2479
  update: {
2227
2480
  async check(force) {
2228
- 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;
2229
2491
  },
2230
2492
  async apply() {
2231
2493
  return expect(await c({ call: "update_apply" }), "update_applied").outcome;
@@ -2448,10 +2710,14 @@ function build(
2448
2710
  view: opts?.view,
2449
2711
  title: opts?.title,
2450
2712
  activate: opts?.activate ?? true,
2713
+ group: opts?.group,
2451
2714
  }),
2452
2715
  "tab",
2453
2716
  ).tab;
2454
2717
  },
2718
+ async group(tab, group) {
2719
+ await c({ call: "tab_group", tab, group });
2720
+ },
2455
2721
  async close(tab) {
2456
2722
  await c({ call: "tab_close", tab });
2457
2723
  },
@@ -2499,6 +2765,9 @@ function build(
2499
2765
  async scrollTo(win, topLine) {
2500
2766
  await c({ call: "win_scroll_to", win, top_line: topLine });
2501
2767
  },
2768
+ async scroll(win, amount) {
2769
+ await c({ call: "win_scroll", win, amount });
2770
+ },
2502
2771
  async viewport(win) {
2503
2772
  return expect(await c({ call: "win_get_viewport", win }), "viewport").viewport ?? null;
2504
2773
  },
@@ -2817,8 +3086,16 @@ function build(
2817
3086
  cwd: opts?.cwd ?? null,
2818
3087
  });
2819
3088
  },
3089
+ async fetch(opts) {
3090
+ const v = await c({ call: "git_fetch", cwd: opts?.cwd ?? null });
3091
+ return expect(v, "status").status;
3092
+ },
3093
+ async pulls(opts) {
3094
+ const v = await c({ call: "forge_pulls", cwd: opts?.cwd ?? null });
3095
+ return expect(v, "pulls").pulls;
3096
+ },
2820
3097
  async pull(opts) {
2821
- const v = await c({ call: "git_pull", cwd: opts?.cwd ?? null });
3098
+ const v = await c({ call: "git_pull", cwd: opts?.cwd ?? null, rebase: opts?.rebase ?? false });
2822
3099
  return expect(v, "text").text;
2823
3100
  },
2824
3101
  async removeWorktree(path, opts) {
@@ -2829,6 +3106,13 @@ function build(
2829
3106
  cwd: opts?.cwd ?? null,
2830
3107
  });
2831
3108
  },
3109
+ async moveWorktree(path, dest, opts) {
3110
+ await c({ call: "git_move_worktree", path, dest, cwd: opts?.cwd ?? null });
3111
+ },
3112
+ async clone(url, path) {
3113
+ const v = await c({ call: "git_clone", url, path });
3114
+ return expect(v, "text").text;
3115
+ },
2832
3116
  },
2833
3117
  gen: {
2834
3118
  async complete(prompt, opts) {
@@ -2851,6 +3135,23 @@ function build(
2851
3135
  });
2852
3136
  return expect(v, "json").value as never;
2853
3137
  },
3138
+ async field(prompt, key, opts) {
3139
+ const v = await c({
3140
+ call: "gen_complete",
3141
+ prompt,
3142
+ system: opts?.system ?? null,
3143
+ json: true,
3144
+ field: key,
3145
+ selection: opts?.selection ?? null,
3146
+ });
3147
+ const value = (expect(v, "json").value as Record<string, unknown>)?.[key];
3148
+ // A key that came back as something other than a non-empty string is an answer to a
3149
+ // different question, and the caller is about to name a branch after it.
3150
+ if (typeof value !== "string" || value.trim() === "") {
3151
+ throw new Error(`the model returned no ${key}`);
3152
+ }
3153
+ return value.trim();
3154
+ },
2854
3155
  },
2855
3156
  view: {
2856
3157
  async list() {
@@ -3166,6 +3467,9 @@ async function dispatchEvent(
3166
3467
  for (const cb of r.varListeners)
3167
3468
  cb({ scope: ev.scope, key: ev.key, value: ev.value });
3168
3469
  break;
3470
+ case "project_moved":
3471
+ for (const cb of r.projectMovedListeners) cb({ from: ev.from, to: ev.to });
3472
+ break;
3169
3473
  case "quota":
3170
3474
  for (const cb of [...r.quotaListeners]) cb(ev.snapshot);
3171
3475
  break;