@agimon-ai/po-git 0.0.0-alpha.1 → 0.0.0-alpha.11

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.
@@ -0,0 +1,1079 @@
1
+ import { createApiClient, defineActivityGroup, defineChannelFile, defineFill, defineGlobalStore, defineNativePlugin, defineSessionStore, defineWebLifecycle, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
2
+ import { BranchIcon, Button, Dot, Input, ListRow, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, Textarea, fontOf } from "@agimon-ai/po-native-components";
3
+ import { createElement, useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { View } from "react-native-unistyles/components/native/View";
5
+ import { Text } from "react-native-unistyles/components/native/Text";
6
+ import { ScrollView } from "react-native-unistyles/components/native/ScrollView";
7
+ import { StyleSheet } from "react-native-unistyles";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ //#region src/types/webWorktrees.ts
10
+ /**
11
+ * The cockpit's view of this session's worktrees.
12
+ *
13
+ * DESIGN PATTERNS:
14
+ * - Declarations only. The channel frame type is a constant so the hub and the
15
+ * page cannot drift apart on a string.
16
+ * - A view is the subset of a record the dock needs, not the record. The
17
+ * registry may hold paths and provenance the page has no business rendering.
18
+ *
19
+ * AVOID:
20
+ * - Adding behaviour here. This file is imported by browser code, so it must
21
+ * stay free of node builtins.
22
+ */
23
+ /** The data channel carrying one session's worktrees to the page. */
24
+ const GIT_WORKTREES_TYPE = "git_worktrees";
25
+ //#endregion
26
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/worktreesActivityStore.native.ts
27
+ /**
28
+ * The dock's view of this session's worktrees, and the commands it sends.
29
+ *
30
+ * DESIGN PATTERNS:
31
+ * - Session-scoped store fed by one hub channel. The hub is authoritative; the
32
+ * page never reads a registry and never guesses.
33
+ * - Actions are channel frames, not prompts. The hub half owns the tool's
34
+ * operations, so the panel names the work and the hub performs it; a
35
+ * worktree made from the dock is the same object the tool makes, without
36
+ * spending a turn of the conversation to ask for it.
37
+ * - The sender lives in a store rather than a module variable, because a web
38
+ * plugin keeps no mutable module state and the page may reconnect.
39
+ *
40
+ * AVOID:
41
+ * - Calling the hub package API from here. A dock slot gets no sealed request
42
+ * transport, and raw fetch breaks the remote cockpit.
43
+ * - `sendSessionFrame` for a command. It envelopes the frame as a session
44
+ * command bound for the agent, which never reaches a channel's `receive`.
45
+ */
46
+ const worktreeActivity = defineSessionStore({
47
+ worktrees: [],
48
+ pending: void 0,
49
+ error: void 0,
50
+ errorTarget: void 0
51
+ });
52
+ /** Captures the clicked card, so a focus change cannot retarget an open dialog. */
53
+ const worktreeCreateDialog = defineGlobalStore(null);
54
+ /**
55
+ * The page's hub socket sender, held for as long as the plugin runs.
56
+ *
57
+ * Undefined before `startWorktreeRuntime` and after it disposes; a command
58
+ * sent in that window is dropped rather than queued, because a queue would
59
+ * replay a worktree create against a socket that has since changed sessions.
60
+ */
61
+ const worktreeSender = defineGlobalStore(void 0);
62
+ /** Binds the page's hub socket. The plugin host calls this once per page. */
63
+ function startWorktreeRuntime(runtime) {
64
+ const send = (frame) => {
65
+ runtime.sendHubFrame(frame);
66
+ };
67
+ worktreeSender.update(() => send);
68
+ return () => {
69
+ worktreeSender.update((current) => current === send ? void 0 : current);
70
+ worktreeCreateDialog.update(() => null);
71
+ };
72
+ }
73
+ /**
74
+ * Whether this session has worktree work in flight.
75
+ *
76
+ * A standing worktree is state, not work: it sits there until someone closes
77
+ * it, so counting it would leave the session reading as busy forever, with a
78
+ * "background work is still running" notice under a conversation where
79
+ * nothing is running. Only a create or close actually in progress counts.
80
+ *
81
+ * The group stays visible either way. Declaring an active source at all is
82
+ * what keeps it in the dock; this answers how it is drawn, not whether it is
83
+ * there.
84
+ */
85
+ const worktreeActivitySource = {
86
+ subscribe(listener) {
87
+ const subscription = worktreeActivity.store.subscribe(listener);
88
+ return () => subscription.unsubscribe();
89
+ },
90
+ isActive(sessionId) {
91
+ return worktreeActivity.select(worktreeActivity.store.state, sessionId).pending !== void 0;
92
+ }
93
+ };
94
+ function isRecord$2(value) {
95
+ return typeof value === "object" && value !== null && !Array.isArray(value);
96
+ }
97
+ function optionalText$1(value) {
98
+ return typeof value === "string" && value !== "" ? value : void 0;
99
+ }
100
+ function optionalErrorTarget$1(value) {
101
+ if (!isRecord$2(value)) return void 0;
102
+ if (value.action === "create") return { action: "create" };
103
+ if (value.action === "close" && typeof value.id === "string" && value.id !== "") return {
104
+ action: "close",
105
+ id: value.id
106
+ };
107
+ }
108
+ /** The plugin's session data channel: 'git_worktrees' payloads into the store. */
109
+ const worktreesChannel = worktreeActivity.channel({
110
+ channel: GIT_WORKTREES_TYPE,
111
+ parse(input) {
112
+ if (!isRecord$2(input) || !Array.isArray(input.worktrees)) return null;
113
+ return {
114
+ worktrees: input.worktrees.filter(isRecord$2),
115
+ pending: optionalText$1(input.pending),
116
+ error: optionalText$1(input.error),
117
+ errorTarget: optionalErrorTarget$1(input.errorTarget)
118
+ };
119
+ },
120
+ reduce(_current, next) {
121
+ return next;
122
+ }
123
+ });
124
+ /** Sends one command to the session's worktrees channel. */
125
+ function command(sessionId, payload) {
126
+ worktreeSender.store.state?.({
127
+ type: GIT_WORKTREES_TYPE,
128
+ sessionId,
129
+ payload
130
+ });
131
+ }
132
+ /** Asks the hub for a worktree. The hub answers on the same channel. */
133
+ function requestWorktreeCreate(sessionId, branch, baseRef, reservationId) {
134
+ const trimmedBase = baseRef.trim();
135
+ command(sessionId, {
136
+ action: "create",
137
+ branch: branch.trim(),
138
+ ...trimmedBase === "" ? {} : { baseRef: trimmedBase },
139
+ ...reservationId === void 0 ? {} : { reservationId }
140
+ });
141
+ }
142
+ /** Asks the hub to close one worktree. Refusal on a dirty tree stays the hub's call. */
143
+ function requestWorktreeClose(sessionId, id) {
144
+ command(sessionId, {
145
+ action: "close",
146
+ id
147
+ });
148
+ }
149
+ //#endregion
150
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/WorktreesPanel.native.tsx
151
+ function useWorktrees(sessionId) {
152
+ const subscribe = useCallback((listener) => {
153
+ const sub = worktreeActivity.store.subscribe(listener);
154
+ return () => sub.unsubscribe();
155
+ }, []);
156
+ return useSyncExternalStore(subscribe, () => worktreeActivity.select(worktreeActivity.store.state, sessionId));
157
+ }
158
+ function worktreesTab() {
159
+ return {
160
+ id: "git-worktrees",
161
+ label: "worktrees",
162
+ panel: WorktreesPanel
163
+ };
164
+ }
165
+ function WorktreesPanel({ sessionId, sessionReservationId, openSession }) {
166
+ const state = useWorktrees(sessionId);
167
+ const [creating, setCreating] = useState(false);
168
+ const [branch, setBranch] = useState("");
169
+ const [baseRef, setBaseRef] = useState("");
170
+ const [closing, setClosing] = useState();
171
+ const busy = state.pending !== void 0;
172
+ return /* @__PURE__ */ jsxs(ScrollView, {
173
+ contentContainerStyle: styles$1.body,
174
+ children: [
175
+ /* @__PURE__ */ jsx(Button, {
176
+ testID: "git-worktree-create",
177
+ disabled: sessionId === null || busy,
178
+ onPress: () => setCreating(true),
179
+ children: "Create a worktree"
180
+ }),
181
+ state.pending ? /* @__PURE__ */ jsx(Text, {
182
+ style: styles$1.meta,
183
+ children: state.pending
184
+ }) : null,
185
+ state.error ? /* @__PURE__ */ jsx(Text, {
186
+ style: styles$1.error,
187
+ children: state.error
188
+ }) : null,
189
+ state.worktrees.map((worktree) => /* @__PURE__ */ jsxs(View, {
190
+ style: styles$1.card,
191
+ children: [
192
+ /* @__PURE__ */ jsx(ListRow, {
193
+ title: worktree.branch,
194
+ description: `${worktree.path}${worktree.orphaned ? "\nOrphaned" : ""}${worktree.unowned ? "\nUnowned" : ""}`
195
+ }),
196
+ worktree.sessionId && openSession ? /* @__PURE__ */ jsx(Button, {
197
+ variant: "outline",
198
+ onPress: () => openSession(worktree.sessionId),
199
+ children: "Open session"
200
+ }) : null,
201
+ /* @__PURE__ */ jsx(Button, {
202
+ variant: "danger-outline",
203
+ disabled: sessionId === null || busy,
204
+ testID: `git-worktree-close-${worktree.id}`,
205
+ onPress: () => setClosing(worktree.id),
206
+ children: "Close worktree"
207
+ })
208
+ ]
209
+ }, worktree.id)),
210
+ state.worktrees.length === 0 ? /* @__PURE__ */ jsx(Text, {
211
+ style: styles$1.meta,
212
+ children: "No worktrees yet. A worktree is its own checkout, branch and session."
213
+ }) : null,
214
+ /* @__PURE__ */ jsx(Sheet, {
215
+ open: creating,
216
+ onOpenChange: setCreating,
217
+ children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "Create a worktree" }) }), /* @__PURE__ */ jsx(SheetBody, { children: /* @__PURE__ */ jsxs(View, {
218
+ style: styles$1.body,
219
+ children: [
220
+ /* @__PURE__ */ jsx(Input, {
221
+ accessibilityLabel: "Branch",
222
+ placeholder: "wt/fix-auth",
223
+ value: branch,
224
+ onChangeText: setBranch
225
+ }),
226
+ /* @__PURE__ */ jsx(Input, {
227
+ accessibilityLabel: "Base ref",
228
+ placeholder: "Base (current)",
229
+ value: baseRef,
230
+ onChangeText: setBaseRef
231
+ }),
232
+ /* @__PURE__ */ jsx(Text, {
233
+ style: styles$1.meta,
234
+ children: "Creates the worktree outside the repository and starts a session nested under this one."
235
+ }),
236
+ /* @__PURE__ */ jsx(Button, {
237
+ disabled: sessionId === null || busy || branch.trim() === "",
238
+ onPress: () => {
239
+ if (sessionId === null) return;
240
+ requestWorktreeCreate(sessionId, branch, baseRef, sessionReservationId);
241
+ setCreating(false);
242
+ if (sessionReservationId === void 0) {
243
+ setBranch("");
244
+ setBaseRef("");
245
+ }
246
+ },
247
+ children: "Create"
248
+ })
249
+ ]
250
+ }) })] })
251
+ }),
252
+ /* @__PURE__ */ jsx(Sheet, {
253
+ open: closing !== void 0,
254
+ onOpenChange: (open) => {
255
+ if (!open) setClosing(void 0);
256
+ },
257
+ children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "Close this worktree?" }) }), /* @__PURE__ */ jsxs(SheetBody, { children: [/* @__PURE__ */ jsx(Text, {
258
+ style: styles$1.meta,
259
+ children: "The checkout is removed. Git refuses a dirty worktree."
260
+ }), /* @__PURE__ */ jsx(Button, {
261
+ variant: "danger-outline",
262
+ disabled: busy,
263
+ onPress: () => {
264
+ if (sessionId !== null && closing !== void 0) requestWorktreeClose(sessionId, closing);
265
+ setClosing(void 0);
266
+ },
267
+ children: "Close worktree"
268
+ })] })] })
269
+ })
270
+ ]
271
+ });
272
+ }
273
+ const styles$1 = StyleSheet.create((theme) => ({
274
+ body: {
275
+ padding: theme.spacing(3),
276
+ gap: theme.spacing(2),
277
+ uni__dependencies: [0]
278
+ },
279
+ card: {
280
+ borderWidth: 1,
281
+ borderColor: theme.colors["border-soft"],
282
+ borderRadius: theme.borderRadius.md,
283
+ backgroundColor: theme.colors.panel,
284
+ padding: theme.spacing(2),
285
+ gap: theme.spacing(1),
286
+ uni__dependencies: [0]
287
+ },
288
+ meta: {
289
+ color: theme.colors.dim,
290
+ fontSize: theme.fontSize.sm,
291
+ uni__dependencies: [0]
292
+ },
293
+ error: {
294
+ color: theme.colors.red,
295
+ fontSize: theme.fontSize.sm,
296
+ uni__dependencies: [0]
297
+ }
298
+ }));
299
+ //#endregion
300
+ //#region src/types/gitReview.ts
301
+ /**
302
+ * The cockpit's view of what this session changed against its base: the counts
303
+ * the '# diff' group shows, what the review tab lists and draws, and the manual
304
+ * sync that sits beside them.
305
+ *
306
+ * DESIGN PATTERNS:
307
+ * - Declarations only, shared by the hub and the page.
308
+ * - Its own channel, apart from worktrees. Worktrees are repository
309
+ * management; the diff is this session's code change.
310
+ * - A diff row carries one line number whose file follows from its marker: the
311
+ * new file's for context and additions, the old file's for removals. It is
312
+ * the same shape the shared DiffView draws.
313
+ *
314
+ * AVOID:
315
+ * - Behaviour or node builtins. Browser code imports this file.
316
+ */
317
+ /** The session channel carrying this checkout's changes and the sync around them. */
318
+ const GIT_CHANGES_TYPE = "git_changes";
319
+ //#endregion
320
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/gitChangesStore.native.ts
321
+ /**
322
+ * This session's code change against its base, and the manual sync beside it.
323
+ *
324
+ * DESIGN PATTERNS:
325
+ * - Its own session store and hub channel, apart from worktrees. Worktrees are
326
+ * repository management; this is the session's diff.
327
+ * - The hub is authoritative: it computes the counts, runs pull, push and
328
+ * rebase one at a time, and reports pending, errors and a paused rebase here.
329
+ * - The sender lives in a store rather than a module variable, because a web
330
+ * plugin keeps no mutable module state and the page may reconnect.
331
+ *
332
+ * AVOID:
333
+ * - `sendSessionFrame` for a sync command. That frame is bound for the agent
334
+ * and never reaches a channel's `receive`.
335
+ */
336
+ const gitChanges = defineSessionStore({
337
+ changes: void 0,
338
+ pending: void 0,
339
+ error: void 0,
340
+ errorTarget: void 0
341
+ });
342
+ /** The page's hub socket sender; undefined before start and after dispose, when a command is dropped. */
343
+ const changesSender = defineGlobalStore(void 0);
344
+ /** Binds the page's hub socket for sync commands. The plugin host calls this once per page. */
345
+ function startGitChangesRuntime(runtime) {
346
+ const send = (frame) => {
347
+ runtime.sendHubFrame(frame);
348
+ };
349
+ changesSender.update(() => send);
350
+ return () => {
351
+ changesSender.update((current) => current === send ? void 0 : current);
352
+ };
353
+ }
354
+ /** Whether a sync command is running for this session. */
355
+ const gitChangesActivitySource = {
356
+ subscribe(listener) {
357
+ const subscription = gitChanges.store.subscribe(listener);
358
+ return () => subscription.unsubscribe();
359
+ },
360
+ isActive(sessionId) {
361
+ return gitChanges.select(gitChanges.store.state, sessionId).pending !== void 0;
362
+ }
363
+ };
364
+ function isRecord$1(value) {
365
+ return typeof value === "object" && value !== null && !Array.isArray(value);
366
+ }
367
+ function optionalText(value) {
368
+ return typeof value === "string" && value !== "" ? value : void 0;
369
+ }
370
+ function count(value) {
371
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
372
+ }
373
+ /** The page never trusts a frame's shape: a malformed field drops, the rest still renders. */
374
+ const MAX_CONFLICTS = 100;
375
+ function optionalChanges(value) {
376
+ if (!isRecord$1(value)) return void 0;
377
+ const added = count(value.added);
378
+ const removed = count(value.removed);
379
+ const files = count(value.files);
380
+ if (added === void 0 || removed === void 0 || files === void 0) return void 0;
381
+ const branch = optionalText(value.branch);
382
+ const base = optionalText(value.base);
383
+ const upstream = isRecord$1(value.upstream) ? value.upstream : void 0;
384
+ const ahead = count(upstream?.ahead);
385
+ const behind = count(upstream?.behind);
386
+ const upstreamRef = optionalText(upstream?.ref);
387
+ const conflicts = isRecord$1(value.rebase) && Array.isArray(value.rebase.conflicts) ? value.rebase.conflicts.filter((file) => typeof file === "string").slice(0, MAX_CONFLICTS) : void 0;
388
+ return {
389
+ added,
390
+ removed,
391
+ files,
392
+ ...branch === void 0 ? {} : { branch },
393
+ ...base === void 0 ? {} : { base },
394
+ ...value.truncated === true ? { truncated: true } : {},
395
+ ...upstreamRef === void 0 || ahead === void 0 || behind === void 0 ? {} : { upstream: {
396
+ ref: upstreamRef,
397
+ ahead,
398
+ behind
399
+ } },
400
+ ...conflicts === void 0 ? {} : { rebase: { conflicts } }
401
+ };
402
+ }
403
+ function optionalErrorTarget(value) {
404
+ if (!isRecord$1(value)) return void 0;
405
+ if (value.action === "pull" || value.action === "rebase" || value.action === "abort-rebase") return { action: value.action };
406
+ if (value.action === "push") return value.forceRequired === true ? {
407
+ action: "push",
408
+ forceRequired: true
409
+ } : { action: "push" };
410
+ }
411
+ /** The session data channel: 'git_changes' payloads into the store. */
412
+ const gitChangesChannel = gitChanges.channel({
413
+ channel: GIT_CHANGES_TYPE,
414
+ parse(input) {
415
+ if (!isRecord$1(input)) return null;
416
+ return {
417
+ changes: optionalChanges(input.changes),
418
+ pending: optionalText(input.pending),
419
+ error: optionalText(input.error),
420
+ errorTarget: optionalErrorTarget(input.errorTarget)
421
+ };
422
+ },
423
+ reduce(_current, next) {
424
+ return next;
425
+ }
426
+ });
427
+ /**
428
+ * Asks the hub to pull, push, rebase or abort a paused rebase. `force` is only
429
+ * ever sent after the reader confirmed a lease force for a rejected push.
430
+ */
431
+ function requestGitSync(sessionId, action, force = false) {
432
+ const payload = action === "push" && force ? {
433
+ action: "push",
434
+ force: true
435
+ } : { action };
436
+ changesSender.store.state?.({
437
+ type: GIT_CHANGES_TYPE,
438
+ sessionId,
439
+ payload
440
+ });
441
+ }
442
+ //#endregion
443
+ //#region src/extensions/workspaces/sessions/(frontend)/activity-group/git.native.ts
444
+ var git_native_default = defineActivityGroup({
445
+ keys: "g w",
446
+ icon: () => createElement(BranchIcon),
447
+ activeSource: {
448
+ subscribe(listener) {
449
+ const stopWorktrees = worktreeActivitySource.subscribe(listener);
450
+ const stopChanges = gitChangesActivitySource.subscribe(listener);
451
+ return () => {
452
+ stopWorktrees();
453
+ stopChanges();
454
+ };
455
+ },
456
+ isActive(sessionId) {
457
+ return worktreeActivitySource.isActive(sessionId) || gitChangesActivitySource.isActive(sessionId);
458
+ }
459
+ },
460
+ marksBackgroundWork: false,
461
+ transientTab: worktreesTab,
462
+ order: 30
463
+ });
464
+ //#endregion
465
+ //#region src/extensions/workspaces/sessions/(frontend)/channel/git-changes.native.ts
466
+ var git_changes_native_default = defineChannelFile(gitChangesChannel);
467
+ //#endregion
468
+ //#region src/extensions/workspaces/sessions/(frontend)/channel/git-worktrees.native.ts
469
+ var git_worktrees_native_default = defineChannelFile(worktreesChannel);
470
+ const api = createApiClient({
471
+ /** Workspace: the new-session dialog's branch list. */
472
+ branches: {
473
+ method: "GET",
474
+ path: "/branches",
475
+ response: void 0
476
+ },
477
+ /** Workspace: a top-level worktree session on an existing or new branch. */
478
+ createSession: {
479
+ method: "POST",
480
+ path: "/sessions",
481
+ response: void 0
482
+ },
483
+ /** Workspace: that workspace's saved remote auth, never the token itself. */
484
+ auth: {
485
+ method: "GET",
486
+ path: "/auth",
487
+ query: ["repositoryId"],
488
+ response: void 0
489
+ },
490
+ /** Workspace: replaces that workspace's remote auth. Passkey step-up gated for remote devices. */
491
+ saveAuth: {
492
+ method: "PUT",
493
+ path: "/auth",
494
+ query: ["repositoryId"],
495
+ response: void 0
496
+ },
497
+ /** Session: the files this session's checkout changed against its base. */
498
+ review: {
499
+ method: "GET",
500
+ path: "/review",
501
+ response: void 0
502
+ },
503
+ /** Session: one changed file's hunks. */
504
+ reviewFile: {
505
+ method: "GET",
506
+ path: "/review/file",
507
+ query: ["path"],
508
+ response: void 0
509
+ }
510
+ }, {
511
+ scopes: [
512
+ "global",
513
+ "workspace",
514
+ "session"
515
+ ],
516
+ basePath: "git",
517
+ transport: nativeApiTransport,
518
+ sessionAddress: sessionApiAddress
519
+ });
520
+ //#endregion
521
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewApi.native.ts
522
+ /**
523
+ * The review tab's half of the session API: the changed-file list and one
524
+ * file's hunks.
525
+ *
526
+ * The generated client owns the URL and the sealed transport, so nothing here
527
+ * spells a route or reaches for `fetch`. What travels is file contents, which a
528
+ * tunnel's relay must never see in the clear.
529
+ */
530
+ const UNREACHABLE = "The session is unreachable.";
531
+ function isRecord(value) {
532
+ return typeof value === "object" && value !== null && !Array.isArray(value);
533
+ }
534
+ /** The message a reader is shown: the route's own words, or why there were none. */
535
+ function messageOf(result) {
536
+ if (result.status === 0) return UNREACHABLE;
537
+ return result.error === "" ? `The session answered ${String(result.status)}.` : result.error;
538
+ }
539
+ async function fetchReviewSummary(sessionId, signal) {
540
+ const result = await api.session(sessionId).review(signal === void 0 ? {} : { signal });
541
+ if (!result.ok) return {
542
+ ok: false,
543
+ error: messageOf(result)
544
+ };
545
+ if (!isRecord(result.data) || !Array.isArray(result.data.files)) return {
546
+ ok: false,
547
+ error: "The session answered with no file list."
548
+ };
549
+ return {
550
+ ok: true,
551
+ summary: result.data
552
+ };
553
+ }
554
+ async function fetchReviewFile(sessionId, path, signal) {
555
+ const result = await api.session(sessionId).reviewFile({
556
+ query: { path },
557
+ ...signal === void 0 ? {} : { signal }
558
+ });
559
+ if (!result.ok) return {
560
+ ok: false,
561
+ error: messageOf(result)
562
+ };
563
+ if (!isRecord(result.data) || !Array.isArray(result.data.hunks)) return {
564
+ ok: false,
565
+ error: "The session answered with no diff."
566
+ };
567
+ return {
568
+ ok: true,
569
+ diff: result.data
570
+ };
571
+ }
572
+ //#endregion
573
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewStore.native.ts
574
+ const gitReview = defineSessionStore({ comments: [] });
575
+ function addReviewComment(sessionId, comment) {
576
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
577
+ gitReview.update(sessionId, (current) => ({ comments: [...current.comments, {
578
+ ...comment,
579
+ id
580
+ }] }));
581
+ }
582
+ function removeReviewComment(sessionId, id) {
583
+ gitReview.update(sessionId, (current) => ({ comments: current.comments.filter((comment) => comment.id !== id) }));
584
+ }
585
+ function clearReviewComments(sessionId) {
586
+ gitReview.update(sessionId, () => ({ comments: [] }));
587
+ }
588
+ //#endregion
589
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewView.ts
590
+ /**
591
+ * Pure wording for what the review tab sends the agent, and the one send.
592
+ *
593
+ * Both messages go as `follow_up`: the host starts a turn when idle, or after
594
+ * the current one, without treating a review send as an explicit Queue action.
595
+ * A `prompt` frame instead fails while a turn runs.
596
+ */
597
+ /** The opening line of a review, naming what was reviewed. */
598
+ function reviewHeading(count, branch, base) {
599
+ return `I reviewed ${branch === void 0 ? "this checkout" : `branch ${branch}`} (${base === void 0 ? "its uncommitted changes" : `its changes against ${base}`}) and left ${count === 1 ? "one review comment" : `${String(count)} review comments`}. ${count === 1 ? "Please address it." : "Please address them all."}`;
600
+ }
601
+ /** What "ask agent to resolve" sends: the files, and the steps that finish a paused rebase. */
602
+ function conflictPrompt(branch, conflicts) {
603
+ const files = conflicts.length === 0 ? "- (git reports no unmerged paths yet)" : conflicts.map((file) => `- ${file}`).join("\n");
604
+ return [
605
+ `A rebase of ${branch ?? "this checkout"} stopped on conflicts in:`,
606
+ "",
607
+ files,
608
+ "",
609
+ "Please resolve them:",
610
+ "1. Fix the conflict markers in each file, keeping the intent of both sides.",
611
+ "2. `git add` each resolved file.",
612
+ "3. Run `GIT_EDITOR=true git rebase --continue`, and repeat if it stops again.",
613
+ "",
614
+ "Do not push. I will push from the cockpit once the rebase is done."
615
+ ].join("\n");
616
+ }
617
+ /**
618
+ * Hands one deferred follow-up to the session. Returns the reason it failed,
619
+ * or undefined once sent. The sender throws when the session is disconnected.
620
+ */
621
+ function deliverToAgent(send, sessionId, message) {
622
+ try {
623
+ send(sessionId, {
624
+ type: "follow_up",
625
+ message
626
+ });
627
+ return;
628
+ } catch (error) {
629
+ return error instanceof Error ? error.message : "The session did not take the message.";
630
+ }
631
+ }
632
+ //#endregion
633
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/GitReviewPanel.native.tsx
634
+ function useGitChanges(sessionId) {
635
+ const subscribe = useCallback((listener) => {
636
+ const sub = gitChanges.store.subscribe(listener);
637
+ return () => sub.unsubscribe();
638
+ }, []);
639
+ return useSyncExternalStore(subscribe, () => gitChanges.select(gitChanges.store.state, sessionId));
640
+ }
641
+ function reviewTab() {
642
+ return {
643
+ id: "git-review",
644
+ label: "review",
645
+ panel: GitReviewPanel
646
+ };
647
+ }
648
+ function GitReviewPanel({ sessionId, sendSessionFrame }) {
649
+ const activity = useGitChanges(sessionId);
650
+ const subscribe = useCallback((listener) => {
651
+ const sub = gitReview.store.subscribe(listener);
652
+ return () => sub.unsubscribe();
653
+ }, []);
654
+ const comments = useSyncExternalStore(subscribe, () => gitReview.select(gitReview.store.state, sessionId).comments);
655
+ const [summary, setSummary] = useState();
656
+ const [file, setFile] = useState();
657
+ const [error, setError] = useState();
658
+ const [dismissedError, setDismissedError] = useState();
659
+ const [confirm, setConfirm] = useState();
660
+ const [draft, setDraft] = useState();
661
+ const [comment, setComment] = useState("");
662
+ const [start, setStart] = useState("");
663
+ const [end, setEnd] = useState("");
664
+ const request = useRef(void 0);
665
+ const fingerprint = JSON.stringify(activity.changes);
666
+ useEffect(() => {
667
+ if (sessionId === null) return void 0;
668
+ const controller = new AbortController();
669
+ fetchReviewSummary(sessionId, controller.signal).then((result) => {
670
+ if (!controller.signal.aborted) {
671
+ if (result.ok) setSummary(result.summary);
672
+ else setError(result.error);
673
+ }
674
+ }).catch((cause) => {
675
+ if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause));
676
+ });
677
+ return () => {
678
+ controller.abort();
679
+ request.current?.abort();
680
+ };
681
+ }, [sessionId, fingerprint]);
682
+ const loadFile = async (path) => {
683
+ if (sessionId === null) return;
684
+ request.current?.abort();
685
+ const controller = new AbortController();
686
+ request.current = controller;
687
+ setFile(void 0);
688
+ setError(void 0);
689
+ try {
690
+ const result = await fetchReviewFile(sessionId, path, controller.signal);
691
+ if (!controller.signal.aborted) {
692
+ if (result.ok) setFile(result.diff);
693
+ else setError(result.error);
694
+ }
695
+ } catch (cause) {
696
+ if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause));
697
+ }
698
+ };
699
+ if (sessionId === null) return null;
700
+ const changes = activity.changes;
701
+ const busy = activity.pending !== void 0;
702
+ const paused = changes?.rebase !== void 0;
703
+ const send = (message, onSent) => {
704
+ const failure = deliverToAgent(sendSessionFrame, sessionId, message);
705
+ setError(failure);
706
+ if (failure === void 0) onSent?.();
707
+ };
708
+ const sync = (action) => requestGitSync(sessionId, action);
709
+ const forceRequired = activity.error !== dismissedError && activity.errorTarget?.action === "push" && activity.errorTarget.forceRequired === true;
710
+ return /* @__PURE__ */ jsxs(ScrollView, {
711
+ contentContainerStyle: styles.body,
712
+ children: [
713
+ /* @__PURE__ */ jsx(Text, {
714
+ style: styles.title,
715
+ children: changes?.branch ?? "Detached HEAD"
716
+ }),
717
+ /* @__PURE__ */ jsxs(Text, {
718
+ style: styles.meta,
719
+ children: [
720
+ changes?.base ? `vs ${changes.base}` : "Uncommitted changes",
721
+ " · +",
722
+ changes?.added ?? 0,
723
+ " -",
724
+ changes?.removed ?? 0,
725
+ "\\n",
726
+ changes?.upstream ? `↑${changes.upstream.ahead} ↓${changes.upstream.behind} ${changes.upstream.ref}` : "No upstream yet"
727
+ ]
728
+ }),
729
+ /* @__PURE__ */ jsxs(View, {
730
+ style: styles.actions,
731
+ children: [
732
+ /* @__PURE__ */ jsx(Button, {
733
+ variant: "outline",
734
+ disabled: busy || paused || changes?.upstream === void 0,
735
+ onPress: () => sync("pull"),
736
+ children: "Pull"
737
+ }),
738
+ /* @__PURE__ */ jsx(Button, {
739
+ variant: "outline",
740
+ disabled: busy || paused || changes?.branch === void 0,
741
+ onPress: () => sync("push"),
742
+ children: "Push"
743
+ }),
744
+ /* @__PURE__ */ jsx(Button, {
745
+ variant: "outline",
746
+ disabled: busy || paused || changes?.base === void 0,
747
+ onPress: () => sync("rebase"),
748
+ children: "Rebase"
749
+ })
750
+ ]
751
+ }),
752
+ activity.pending ? /* @__PURE__ */ jsx(Text, {
753
+ style: styles.meta,
754
+ children: activity.pending
755
+ }) : null,
756
+ activity.error && activity.error !== dismissedError ? /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsx(Text, {
757
+ style: styles.error,
758
+ children: activity.error
759
+ }), /* @__PURE__ */ jsx(Button, {
760
+ variant: "ghost",
761
+ onPress: () => setDismissedError(activity.error),
762
+ children: "Dismiss"
763
+ })] }) : null,
764
+ forceRequired ? /* @__PURE__ */ jsx(Button, {
765
+ variant: "danger-outline",
766
+ disabled: busy,
767
+ onPress: () => setConfirm("force"),
768
+ children: "Force push with lease…"
769
+ }) : null,
770
+ paused ? /* @__PURE__ */ jsxs(View, {
771
+ style: styles.body,
772
+ children: [
773
+ /* @__PURE__ */ jsxs(Text, {
774
+ style: styles.error,
775
+ children: ["Rebase paused\\n", changes.rebase?.conflicts.join("\n")]
776
+ }),
777
+ /* @__PURE__ */ jsx(Button, {
778
+ disabled: busy,
779
+ onPress: () => send(conflictPrompt(changes.branch, changes.rebase?.conflicts ?? [])),
780
+ children: "Ask agent to resolve"
781
+ }),
782
+ /* @__PURE__ */ jsx(Button, {
783
+ variant: "danger-outline",
784
+ disabled: busy,
785
+ onPress: () => setConfirm("abort"),
786
+ children: "Abort rebase…"
787
+ })
788
+ ]
789
+ }) : null,
790
+ error ? /* @__PURE__ */ jsx(Text, {
791
+ style: styles.error,
792
+ children: error
793
+ }) : null,
794
+ summary ? summary.files.map((entry) => /* @__PURE__ */ jsxs(Button, {
795
+ variant: "ghost",
796
+ testID: `git-review-file-${entry.path}`,
797
+ onPress: () => void loadFile(entry.path),
798
+ children: [
799
+ entry.path,
800
+ " · ",
801
+ entry.status,
802
+ " · +",
803
+ entry.added,
804
+ " -",
805
+ entry.removed
806
+ ]
807
+ }, entry.path)) : /* @__PURE__ */ jsx(Text, {
808
+ style: styles.meta,
809
+ children: "Loading changes…"
810
+ }),
811
+ summary?.files.length === 0 ? /* @__PURE__ */ jsx(Text, {
812
+ style: styles.meta,
813
+ children: summary.repository ? "No changed files" : "Not a Git checkout"
814
+ }) : null,
815
+ summary?.truncated ? /* @__PURE__ */ jsx(Text, {
816
+ style: styles.meta,
817
+ children: "The file list was truncated."
818
+ }) : null,
819
+ file ? /* @__PURE__ */ jsxs(View, {
820
+ style: styles.body,
821
+ children: [
822
+ /* @__PURE__ */ jsx(Text, {
823
+ style: styles.title,
824
+ children: file.path
825
+ }),
826
+ file.binary || file.tooLarge ? /* @__PURE__ */ jsx(Text, {
827
+ style: styles.meta,
828
+ children: file.binary ? "Binary file" : "Diff too large to display"
829
+ }) : file.hunks.map((hunk, index) => /* @__PURE__ */ jsx(View, { children: hunk.rows.map((row, rowIndex) => /* @__PURE__ */ jsxs(Text, {
830
+ selectable: true,
831
+ style: styles.diff(row.marker),
832
+ onPress: () => {
833
+ setDraft({
834
+ path: file.path,
835
+ rows: hunk.rows
836
+ });
837
+ setStart(String(row.line));
838
+ setEnd(String(row.line));
839
+ },
840
+ children: [
841
+ row.marker,
842
+ " ",
843
+ row.line,
844
+ " ",
845
+ row.content
846
+ ]
847
+ }, rowIndex)) }, index)),
848
+ /* @__PURE__ */ jsx(Button, {
849
+ variant: "outline",
850
+ onPress: () => {
851
+ setDraft({
852
+ path: file.path,
853
+ rows: []
854
+ });
855
+ setStart("");
856
+ setEnd("");
857
+ },
858
+ children: "Comment on file"
859
+ })
860
+ ]
861
+ }) : null,
862
+ comments.map((item) => /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Text, {
863
+ style: styles.meta,
864
+ children: [
865
+ item.relPath,
866
+ item.startLine === void 0 ? "" : `:${item.startLine}-${item.endLine ?? item.startLine}`,
867
+ "\\n",
868
+ item.body
869
+ ]
870
+ }), /* @__PURE__ */ jsx(Button, {
871
+ variant: "ghost",
872
+ onPress: () => removeReviewComment(sessionId, item.id),
873
+ children: "Remove comment"
874
+ })] }, item.id)),
875
+ comments.length > 0 ? /* @__PURE__ */ jsxs(View, {
876
+ style: styles.actions,
877
+ children: [/* @__PURE__ */ jsx(Button, {
878
+ onPress: () => {
879
+ const body = comments.map((item) => {
880
+ return `### ${item.startLine === void 0 ? item.relPath : item.side === "old" ? `${item.relPath} (removed lines ${item.startLine}-${item.endLine ?? item.startLine})` : `${item.relPath}:${item.startLine}-${item.endLine ?? item.startLine}`}\n\n${item.snippet.split("\n").slice(0, 20).join("\n").slice(0, 2e3).split("\n").map((line) => `> ${line}`).join("\n")}\n\n${item.body.trim()}`;
881
+ }).join("\n\n");
882
+ send(`${reviewHeading(comments.length, summary?.branch, summary?.base)}\n\n${body}`, () => clearReviewComments(sessionId));
883
+ },
884
+ children: "Send review"
885
+ }), /* @__PURE__ */ jsx(Button, {
886
+ variant: "outline",
887
+ onPress: () => setConfirm("discard"),
888
+ children: "Discard review…"
889
+ })]
890
+ }) : null,
891
+ /* @__PURE__ */ jsx(Sheet, {
892
+ open: draft !== void 0,
893
+ onOpenChange: (open) => {
894
+ if (!open) setDraft(void 0);
895
+ },
896
+ children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "Review comment" }) }), /* @__PURE__ */ jsx(SheetBody, { children: /* @__PURE__ */ jsxs(View, {
897
+ style: styles.body,
898
+ children: [
899
+ draft?.rows.length ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Input, {
900
+ accessibilityLabel: "Start line",
901
+ value: start,
902
+ onChangeText: setStart,
903
+ keyboardType: "number-pad"
904
+ }), /* @__PURE__ */ jsx(Input, {
905
+ accessibilityLabel: "End line",
906
+ value: end,
907
+ onChangeText: setEnd,
908
+ keyboardType: "number-pad"
909
+ })] }) : null,
910
+ /* @__PURE__ */ jsx(Textarea, {
911
+ accessibilityLabel: "Comment",
912
+ value: comment,
913
+ onChangeText: setComment
914
+ }),
915
+ /* @__PURE__ */ jsx(Button, {
916
+ disabled: comment.trim() === "" || Boolean(draft?.rows.length) && (!Number.isInteger(Number(start)) || Number(start) < 1 || !Number.isInteger(Number(end)) || Number(end) < Number(start)),
917
+ onPress: () => {
918
+ if (!draft) return;
919
+ const rows = draft.rows.filter((row) => row.line >= Number(start) && row.line <= Number(end));
920
+ const kept = rows.filter((row) => row.marker !== "-");
921
+ const numbered = kept.length > 0 ? kept : rows;
922
+ addReviewComment(sessionId, {
923
+ path: draft.path,
924
+ relPath: draft.path,
925
+ body: comment.trim(),
926
+ snippet: numbered.map((row) => row.content).join("\n"),
927
+ ...numbered.length === 0 ? {} : {
928
+ side: kept.length > 0 ? "new" : "old",
929
+ startLine: Math.min(...numbered.map((row) => row.line)),
930
+ endLine: Math.max(...numbered.map((row) => row.line))
931
+ }
932
+ });
933
+ setDraft(void 0);
934
+ setComment("");
935
+ },
936
+ children: "Add comment"
937
+ })
938
+ ]
939
+ }) })] })
940
+ }),
941
+ /* @__PURE__ */ jsx(Sheet, {
942
+ open: confirm !== void 0,
943
+ onOpenChange: (open) => {
944
+ if (!open) setConfirm(void 0);
945
+ },
946
+ children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: confirm === "force" ? "Force push with lease?" : confirm === "abort" ? "Abort rebase?" : "Discard review?" }) }), /* @__PURE__ */ jsxs(SheetBody, { children: [/* @__PURE__ */ jsx(Text, {
947
+ style: styles.meta,
948
+ children: confirm === "force" ? "Git refuses this if the remote holds commits you never fetched and integrated." : confirm === "abort" ? "Abort discards any conflict resolution in progress." : "Your queued comments will be removed."
949
+ }), /* @__PURE__ */ jsx(Button, {
950
+ variant: "danger-outline",
951
+ onPress: () => {
952
+ if (confirm === "force") requestGitSync(sessionId, "push", true);
953
+ else if (confirm === "abort") sync("abort-rebase");
954
+ else clearReviewComments(sessionId);
955
+ setConfirm(void 0);
956
+ },
957
+ children: "Confirm"
958
+ })] })] })
959
+ })
960
+ ]
961
+ });
962
+ }
963
+ const styles = StyleSheet.create((theme) => ({
964
+ body: {
965
+ padding: theme.spacing(3),
966
+ gap: theme.spacing(2),
967
+ uni__dependencies: [0]
968
+ },
969
+ actions: {
970
+ flexDirection: "row",
971
+ flexWrap: "wrap",
972
+ gap: theme.spacing(1),
973
+ uni__dependencies: [0]
974
+ },
975
+ title: {
976
+ color: theme.colors.hi,
977
+ fontSize: theme.fontSize.base,
978
+ fontWeight: "600",
979
+ uni__dependencies: [0]
980
+ },
981
+ meta: {
982
+ color: theme.colors.dim,
983
+ fontSize: theme.fontSize.sm,
984
+ uni__dependencies: [0]
985
+ },
986
+ error: {
987
+ color: theme.colors.red,
988
+ fontSize: theme.fontSize.sm,
989
+ uni__dependencies: [0]
990
+ },
991
+ diff: (marker) => ({
992
+ fontFamily: fontOf(theme.fontFamily.mono),
993
+ fontSize: theme.fontSize.sm,
994
+ color: marker === "+" ? theme.colors.green : marker === "-" ? theme.colors.red : theme.colors.text,
995
+ paddingVertical: theme.spacing(1),
996
+ uni__dependencies: [0]
997
+ })
998
+ }));
999
+ //#endregion
1000
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/WorktreesActivitySection.native.tsx
1001
+ function WorktreesActivitySection({ sessionId, openTransientTab }) {
1002
+ const git = useGitChanges(sessionId);
1003
+ const state = useWorktrees(sessionId);
1004
+ const worktrees = state.worktrees.filter((item) => item.sessionId !== sessionId);
1005
+ return /* @__PURE__ */ jsxs(View, { children: [
1006
+ git.changes ? /* @__PURE__ */ jsxs(Button, {
1007
+ variant: "ghost",
1008
+ testID: "activity-diff-changes",
1009
+ onPress: () => openTransientTab(reviewTab()),
1010
+ children: [
1011
+ git.changes.branch ?? "Detached HEAD",
1012
+ " · +",
1013
+ git.changes.added,
1014
+ " -",
1015
+ git.changes.removed
1016
+ ]
1017
+ }) : null,
1018
+ state.pending ? /* @__PURE__ */ jsx(ListRow, { title: state.pending }) : null,
1019
+ worktrees.map((item) => /* @__PURE__ */ jsx(ListRow, {
1020
+ leading: /* @__PURE__ */ jsx(Dot, { tone: item.orphaned ? "red" : item.sessionId === null ? "muted" : "blue" }),
1021
+ title: /* @__PURE__ */ jsx(Button, {
1022
+ variant: "ghost",
1023
+ onPress: () => openTransientTab(worktreesTab()),
1024
+ children: item.branch
1025
+ }),
1026
+ description: [item.orphaned ? "Orphaned" : "", item.unowned ? "Unowned" : ""].filter(Boolean).join(" · ")
1027
+ }, item.id)),
1028
+ worktrees.length === 0 && state.pending === void 0 ? /* @__PURE__ */ jsx(Button, {
1029
+ variant: "ghost",
1030
+ testID: "activity-git-create",
1031
+ disabled: sessionId === null,
1032
+ onPress: () => openTransientTab(worktreesTab()),
1033
+ children: "Create a worktree"
1034
+ }) : null
1035
+ ] });
1036
+ }
1037
+ //#endregion
1038
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/Worktrees.activity.git.native.tsx
1039
+ var Worktrees_activity_git_native_default = defineFill({ component: WorktreesActivitySection });
1040
+ //#endregion
1041
+ //#region src/extensions/workspaces/sessions/(frontend)/lifecycle/start.native.ts
1042
+ var start_native_default = defineWebLifecycle((runtime) => {
1043
+ const stopWorktrees = startWorktreeRuntime(runtime);
1044
+ const stopChanges = startGitChangesRuntime(runtime);
1045
+ return () => {
1046
+ stopChanges();
1047
+ stopWorktrees();
1048
+ };
1049
+ });
1050
+ //#endregion
1051
+ //#region generated/native.ts
1052
+ const via = (identity, value) => value === void 0 ? void 0 : {
1053
+ ...identity,
1054
+ ...value
1055
+ };
1056
+ const nativePlugin = defineNativePlugin({
1057
+ id: "git",
1058
+ session: {
1059
+ get activityGroups() {
1060
+ return [via({ name: "git" }, git_native_default)];
1061
+ },
1062
+ get channels() {
1063
+ return [via({ channel: "git_changes" }, git_changes_native_default), via({ channel: "git_worktrees" }, git_worktrees_native_default)];
1064
+ },
1065
+ get fills() {
1066
+ return [via({
1067
+ slot: "activity.git",
1068
+ id: "worktrees"
1069
+ }, Worktrees_activity_git_native_default)];
1070
+ },
1071
+ get start() {
1072
+ return start_native_default;
1073
+ }
1074
+ }
1075
+ });
1076
+ //#endregion
1077
+ export { nativePlugin };
1078
+
1079
+ //# sourceMappingURL=native.mjs.map