@kud/gh-cockpit 0.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Erwann Mest
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # @kud/gh-cockpit
2
+
3
+ A configurable GitHub cockpit for the terminal — your PRs, reviews and issues in one Ink TUI, grouped by **whose move it is**.
4
+
5
+ A tab tells you which relationship you are looking at. It never told you whether there was anything to _do_ once you got there, so red CI, merge conflicts and drafts sat interleaved with the rows you could actually act on. This bands every tab into `Your move` / `Their move` instead.
6
+
7
+ ```
8
+ Review (20)
9
+
10
+ » Your move (2)
11
+ ── acme/api-gateway ─────
12
+ ◆ ← #1495 support statementPeriodIds… 6d
13
+ ── acme/event-bus ───────
14
+ · #290 delete the legacy path 14h
15
+
16
+ » Their move (9)
17
+ ── acme/monorepo ────────
18
+ ~ #10 add internalNotes field 1w
19
+ ── acme/api-gateway ─────
20
+ ✗ ← #1496 point history at the route 2d
21
+ ! #1449 guard null ids 6w
22
+ ```
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ npm install @kud/gh-cockpit
28
+ ```
29
+
30
+ It is a **library, not a CLI**: you write the thin entry point, because the interesting decisions — which searches are tabs, which repos rank first, what a check drills into — are yours and not portable.
31
+
32
+ ## Whose move is it
33
+
34
+ The band comes from the row's health _and_ where you stand relative to it. Those are different questions, and the same token means opposite things depending on the answer:
35
+
36
+ | | `authored`<br>your PR | `queued`<br>a review was asked of you | `spoken`<br>you already reviewed |
37
+ | ----------------------------------------------------- | --------------------- | ------------------------------------- | -------------------------------- |
38
+ | `✗` CI failing · `!` conflict · `±` changes requested | **you** | them | them |
39
+ | `·` awaiting review · `*` checks running | them | **you** | them |
40
+ | `✓` approved | **you** | **you** | them |
41
+ | `◆` open threads | **you** | **you** | **you** |
42
+ | `~` draft | them | them | them |
43
+
44
+ Red CI on a PR you wrote is your afternoon. The same red CI on one you were asked to review is the author's, and reviewing it is wasted. Nothing on the row records that difference — it is a property of the _search_ the row arrived from, which is why `standing` is declared per search rather than inferred.
45
+
46
+ > [!NOTE]
47
+ > Every state has its own glyph, never a colour alone. Colour reinforces; it never carries meaning by itself.
48
+
49
+ ## Configure
50
+
51
+ ```ts
52
+ import { defineCockpit } from "@kud/gh-cockpit"
53
+ import { delegateExtension } from "@kud/gh-cockpit/extensions"
54
+
55
+ export default defineCockpit({
56
+ repoPriority: ["acme/monorepo", "acme/", "me/"],
57
+ tabs: [
58
+ {
59
+ label: "Mine",
60
+ help: "your PRs, draft and open",
61
+ searches: ["is:open author:@me"],
62
+ },
63
+ {
64
+ label: "Review",
65
+ help: "theirs — asked of you, or you reviewed",
66
+ searches: [
67
+ { q: "is:open review-requested:@me", standing: "queued" },
68
+ {
69
+ q: "is:open reviewed-by:@me -author:@me -review-requested:@me",
70
+ standing: "spoken",
71
+ },
72
+ ],
73
+ },
74
+ ],
75
+ extensions: [delegateExtension],
76
+ })
77
+ ```
78
+
79
+ `defineCockpit` is identity, but typed — a config file is checked against the schema at build time rather than producing an empty tab at runtime.
80
+
81
+ ### Repo priority
82
+
83
+ Ordered, best first. An entry ending in `/` matches an owner; anything else must equal `owner/name`, so a single repo can outrank the owner containing it. Repos matching nothing sort last, together.
84
+
85
+ Grouping is a separate key from ranking: repos stay clustered whatever the priority list says, because repo headers depend on same-repo rows being adjacent.
86
+
87
+ ## Filtering
88
+
89
+ ```sh
90
+ gh-cockpit --include 'acme/*,me/acme-*'
91
+ ```
92
+
93
+ `*` matches within one path segment and never across the `/`, so `acme/*` cannot reach another owner and `acme` will not claim `acmecorp`. A bare owner is sugar for all its repos.
94
+
95
+ ```sh
96
+ gh-cockpit --exclude 'acme/legacy'
97
+ ```
98
+
99
+ Exclude is applied after include and wins, so you can take a whole org and drop one repo without listing the rest.
100
+
101
+ ```sh
102
+ gh-cockpit --here
103
+ ```
104
+
105
+ `--here` is not a filter. It resolves the repo from your git remote — `upstream` if present, else `origin` — and scopes every search **server-side** with `repo:owner/name`, so other repos are never fetched at all. On a fork that matters: `origin` is your copy and `upstream` is where the PRs actually live.
106
+
107
+ ## Extensions
108
+
109
+ Extensions live behind their own entry point because they are a choice, not a default — each one claims a keybinding on a surface where every key is already spoken for.
110
+
111
+ ```ts
112
+ import {
113
+ delegateExtension,
114
+ copyPromptExtension,
115
+ } from "@kud/gh-cockpit/extensions"
116
+ ```
117
+
118
+ Both act on the row under the cursor and shell out to nothing, so neither assumes anything about what you have installed.
119
+
120
+ ## Check drill-ins
121
+
122
+ Activating a CI check opens its log. GitHub Actions is built in; anything else is yours to register, and an unregistered check opens in a browser rather than drilling into a view that renders nothing.
123
+
124
+ ```ts
125
+ import { registerCheckDrills } from "@kud/gh-cockpit"
126
+
127
+ registerCheckDrills([
128
+ {
129
+ match: (url) => url.includes("ci.example"),
130
+ render: ({ url, name, onBack }) => <MyBuildView url={url} name={name} onBack={onBack} />,
131
+ },
132
+ ])
133
+ ```
134
+
135
+ ## Host configuration
136
+
137
+ The library holds no opinion about your machine. `configureInbox` (from `@kud/gh-ink`) is where you supply one, and **every default is empty** — an unconfigured host gets flat repo ranking and no local-checkout resolution, never a guess at where you keep your code.
138
+
139
+ ```ts
140
+ import { configureInbox } from "@kud/gh-cockpit"
141
+
142
+ configureInbox({
143
+ repoPriority: ["acme/", "me/"],
144
+ checkoutDir: `${process.env.HOME}/src`,
145
+ cacheNamespace: "my-cockpit",
146
+ cacheTtlMs: 20 * 60_000,
147
+ })
148
+ ```
149
+
150
+ > [!TIP]
151
+ > `cacheTtlMs` is the single knob between "always current" and "always slow, and drawing 502s from the API" — every launch past it pays the full query. It can be generous: acting on a row drops the cache entry outright, and `r` refetches on demand.
152
+
153
+ ## Refreshing
154
+
155
+ The cockpit does not poll. A poller spends quota on the long stretches where nothing changed and is still up to a full interval late when something did. Instead, pass `watchPath` and have whatever mutates GitHub write a byte to that file — every open cockpit refetches, debounced. The filesystem is the broker: no daemon, no socket, no fan-out to manage.
156
+
157
+ > [!IMPORTANT]
158
+ > Write a byte rather than `touch`ing. At second-granularity mtime with no size change, two touches inside the same second are indistinguishable and some watch backends coalesce them away.
159
+
160
+ ## Licence
161
+
162
+ MIT © Erwann Mest
@@ -0,0 +1,311 @@
1
+ import * as gh_ink_star from '@kud/gh-ink';
2
+ import { relativeTime, inboxConfig } from '@kud/gh-ink';
3
+ import { mkdirSync } from 'fs';
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+ import { buildInboxQuery, computeHealth as computeHealth$1, latestChecks, isPendingCheck, isFailCheck, isPassCheck } from '@kud/gh';
7
+ export { buildInboxQuery } from '@kud/gh';
8
+ import { useWindowSize, Box, Text, useInput } from 'ink';
9
+ import { Panel, colors, FooterHints, useListCursor } from '@kud/ink-ui';
10
+ import { jsx, jsxs } from 'react/jsx-runtime';
11
+ import { $ } from 'zx';
12
+ import { useRef, useEffect, useState } from 'react';
13
+
14
+ var __defProp = Object.defineProperty;
15
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
16
+ var __getOwnPropNames = Object.getOwnPropertyNames;
17
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
18
+ var __export = (target, all) => {
19
+ for (var name in all)
20
+ __defProp(target, name, { get: all[name], enumerable: true });
21
+ };
22
+ var __copyProps = (to, from, except, desc) => {
23
+ if (from && typeof from === "object" || typeof from === "function") {
24
+ for (let key of __getOwnPropNames(from))
25
+ if (!__hasOwnProp.call(to, key) && key !== except)
26
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
27
+ }
28
+ return to;
29
+ };
30
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
31
+
32
+ // src/lib.ts
33
+ var lib_exports = {};
34
+ __export(lib_exports, {
35
+ buildInboxQuery: () => buildInboxQuery,
36
+ computeHealth: () => computeHealth,
37
+ signalPath: () => signalPath,
38
+ toGHItem: () => toGHItem,
39
+ withRetry: () => withRetry
40
+ });
41
+ __reExport(lib_exports, gh_ink_star);
42
+ var withRetry = async (fn, attempts = 3, delayMs = 1e3) => {
43
+ for (let i = 0; i < attempts; i++) {
44
+ try {
45
+ return await fn();
46
+ } catch (err) {
47
+ const msg = err.message ?? "";
48
+ const isTransient = /50[234]/.test(msg);
49
+ if (!isTransient || i === attempts - 1) throw err;
50
+ await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
51
+ }
52
+ }
53
+ throw new Error("unreachable");
54
+ };
55
+ var computeHealth = (node) => computeHealth$1({
56
+ state: node.state,
57
+ isDraft: "isDraft" in node ? node.isDraft : void 0,
58
+ checks: node.statusCheckRollup?.contexts?.nodes ?? [],
59
+ mergeable: node.mergeable,
60
+ reviewDecision: node.reviewDecision,
61
+ unresolvedThreads: (node.reviewThreads?.nodes ?? []).filter(
62
+ (t) => t && !t.isResolved
63
+ ).length
64
+ });
65
+ var conversationOf = (node) => {
66
+ const events = [];
67
+ let count = node.comments?.totalCount ?? 0;
68
+ const isBot = (author) => author?.__typename === "Bot";
69
+ for (const c of node.comments?.nodes ?? [])
70
+ events.push({
71
+ at: c.createdAt,
72
+ login: c.author?.login,
73
+ bot: isBot(c.author)
74
+ });
75
+ for (const r of node.reviews?.nodes ?? [])
76
+ if (r.state !== "PENDING")
77
+ events.push({
78
+ at: r.submittedAt,
79
+ login: r.author?.login,
80
+ bot: isBot(r.author)
81
+ });
82
+ for (const t of node.reviewThreads?.nodes ?? []) {
83
+ count += t.comments?.totalCount ?? 0;
84
+ for (const c of t.comments?.nodes ?? [])
85
+ events.push({
86
+ at: c.createdAt,
87
+ login: c.author?.login,
88
+ bot: isBot(c.author)
89
+ });
90
+ }
91
+ const last = events.filter((e) => e.at).sort((a, b) => a.at.localeCompare(b.at)).pop();
92
+ const pushedAt = node.commits?.nodes?.[0]?.commit?.committedDate;
93
+ if (last?.bot && pushedAt && pushedAt > last.at)
94
+ return { count, lastActor: node.author?.login, lastEventAt: pushedAt };
95
+ return { count, lastActor: last?.login, lastEventAt: last?.at };
96
+ };
97
+ var detailOf = (node, lastEventAt) => {
98
+ const active = latestChecks(node.statusCheckRollup?.contexts?.nodes ?? []);
99
+ return {
100
+ reviewDecision: node.reviewDecision ?? void 0,
101
+ mergeable: node.mergeable ?? void 0,
102
+ checksPass: active.filter(isPassCheck).length,
103
+ checksFail: active.filter(isFailCheck).length,
104
+ checksPending: active.filter(isPendingCheck).length,
105
+ threadsTotal: (node.reviewThreads?.nodes ?? []).length,
106
+ lastCommitAt: node.commits?.nodes?.[0]?.commit?.committedDate,
107
+ lastEventAt
108
+ };
109
+ };
110
+ var toGHItem = (node, opts = {}) => {
111
+ const completedAt = node.mergedAt ?? node.closedAt ?? node.createdAt;
112
+ const convo = conversationOf(node);
113
+ const activityAt = [
114
+ node.createdAt,
115
+ convo.lastEventAt,
116
+ node.commits?.nodes?.[0]?.commit?.committedDate
117
+ ].filter(Boolean).sort().pop();
118
+ const isDone = Boolean(node.mergedAt ?? node.closedAt);
119
+ const sortAt = isDone ? completedAt : activityAt ?? completedAt;
120
+ return {
121
+ // Prefer GraphQL's own discriminator; fall back to field-presence only when
122
+ // a fragment omitted __typename. Field-presence alone mislabels PRs as issues
123
+ // whenever a fragment (reviewRequests, assigned, reviewed…) skips isDraft /
124
+ // headRefName — which sent PR drills to the issues endpoint.
125
+ kind: node.__typename === "PullRequest" ? "pr" : node.__typename === "Issue" ? "issue" : "isDraft" in node || "headRefName" in node ? "pr" : "issue",
126
+ number: node.number,
127
+ title: node.title ?? "",
128
+ repo: node.repository?.nameWithOwner ?? "",
129
+ url: node.url ?? "",
130
+ branch: node.headRefName,
131
+ health: computeHealth(node),
132
+ author: node.author?.login,
133
+ age: completedAt ? relativeTime(completedAt) : "",
134
+ activityAge: !isDone && activityAt ? relativeTime(activityAt) : void 0,
135
+ ts: sortAt ? new Date(sortAt).getTime() : 0,
136
+ unresolved: (node.reviewThreads?.nodes ?? []).filter(
137
+ (t) => t && !t.isResolved
138
+ ).length,
139
+ conversation: convo.count,
140
+ lastActor: convo.lastActor,
141
+ labels: (node.labels?.nodes ?? []).map((l) => l?.name).filter((n) => typeof n === "string"),
142
+ detail: detailOf(node, convo.lastEventAt),
143
+ indent: opts.indent ?? false
144
+ };
145
+ };
146
+ var signalPath = () => {
147
+ const dir = join(
148
+ process.env.XDG_CACHE_HOME || join(homedir(), ".cache"),
149
+ inboxConfig().cacheNamespace
150
+ );
151
+ mkdirSync(dir, { recursive: true });
152
+ return join(dir, "cockpit-dirty");
153
+ };
154
+ var DrillView = ({
155
+ title,
156
+ subtitle,
157
+ hints,
158
+ children
159
+ }) => {
160
+ const { rows } = useWindowSize();
161
+ return /* @__PURE__ */ jsx(Panel, { children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: rows - 2, paddingX: 1, children: [
162
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
163
+ /* @__PURE__ */ jsx(Text, { color: colors.accent, bold: true, children: title }),
164
+ subtitle ? /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
165
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Title: " }),
166
+ /* @__PURE__ */ jsx(Text, { children: subtitle })
167
+ ] }) : null
168
+ ] }),
169
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, children }),
170
+ /* @__PURE__ */ jsx(FooterHints, { hints })
171
+ ] }) });
172
+ };
173
+ var CANDIDATES = [
174
+ { id: "claude", label: "Claude Code", cmd: "claude", acceptsPrompt: true },
175
+ { id: "opencode", label: "opencode", cmd: "opencode" },
176
+ { id: "codex", label: "Codex", cmd: "codex", acceptsPrompt: true }
177
+ ];
178
+ var PLACEMENTS = [
179
+ { id: "here", label: "Right here" },
180
+ { id: "tab", label: "New tab" },
181
+ { id: "vpane", label: "New pane \u2192 (right)" },
182
+ { id: "hpane", label: "New pane \u2193 (below)" }
183
+ ];
184
+ var isInstalled = async (cmd) => (await $({ nothrow: true, quiet: true })`command -v ${cmd}`).exitCode === 0;
185
+ var shellQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
186
+ var seedPromptFor = (item) => item.kind === "pr" ? `/k-pr ${item.number}` : item.labels?.includes("plan") ? `/k-project plan ${item.number}` : `/k-project ${item.url}`;
187
+ var portablePromptFor = (item) => item.kind === "pr" ? `/k-pr ${item.url}` : item.labels?.includes("plan") ? `/k-project plan ${item.number} ${item.repo}` : `/k-project ${item.url}`;
188
+ var CopyPromptNotice = ({
189
+ item,
190
+ onBack
191
+ }) => {
192
+ const prompt = portablePromptFor(item);
193
+ const back = useRef(onBack);
194
+ back.current = onBack;
195
+ useEffect(() => {
196
+ (0, lib_exports.clipboard)(prompt);
197
+ const timer = setTimeout(() => back.current(), 1400);
198
+ return () => clearTimeout(timer);
199
+ }, [prompt]);
200
+ useInput(() => back.current());
201
+ return /* @__PURE__ */ jsx(
202
+ DrillView,
203
+ {
204
+ title: `Copy prompt \xB7 #${item.number} \xB7 ${item.repo}`,
205
+ subtitle: "paste it into a session that is already running",
206
+ hints: [["any key", "back"]],
207
+ children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
208
+ /* @__PURE__ */ jsx(Text, { color: colors.success, children: "\u2713 copied to clipboard" }),
209
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: `prompt ${prompt}` }) })
210
+ ] })
211
+ }
212
+ );
213
+ };
214
+ var AiLauncher = ({
215
+ item,
216
+ login,
217
+ prompt,
218
+ onBack
219
+ }) => {
220
+ const [agents, setAgents] = useState(null);
221
+ const [step, setStep] = useState("agent");
222
+ const [agent, setAgent] = useState(null);
223
+ const [note, setNote] = useState(null);
224
+ useEffect(() => {
225
+ let live = true;
226
+ Promise.all(
227
+ CANDIDATES.map(async (a) => await isInstalled(a.cmd) ? a : null)
228
+ ).then((found) => {
229
+ if (!live) return;
230
+ const shell = { id: "shell", label: "Shell (no AI)", cmd: "" };
231
+ setAgents([...found.filter((a) => a !== null), shell]);
232
+ });
233
+ return () => {
234
+ live = false;
235
+ };
236
+ }, []);
237
+ const list = step === "agent" ? (agents ?? []).map((a) => ({ label: a.label, hint: a.cmd })) : PLACEMENTS.map((p) => ({ label: p.label }));
238
+ const { cursor, setCursor } = useListCursor(list.length);
239
+ const safeCursor = Math.min(cursor, Math.max(0, list.length - 1));
240
+ const focused = step === "agent" ? (agents ?? [])[safeCursor] : agent;
241
+ const launch = async (a, placement) => {
242
+ setNote(`\u22EF opening ${a.label}\u2026`);
243
+ try {
244
+ const base = await (0, lib_exports.buildCheckoutCmd)(item.repo, item.branch ?? "", login);
245
+ const run = prompt && a.acceptsPrompt ? `${a.cmd} ${shellQuote(prompt)}` : a.cmd;
246
+ const full = a.cmd ? `${base} && ${run}` : base;
247
+ if (placement === "here") {
248
+ (0, lib_exports.runHere)(full);
249
+ process.exit(0);
250
+ }
251
+ if (placement === "tab") await (0, lib_exports.openInTab)(full);
252
+ if (placement === "vpane") await (0, lib_exports.runInPane)(full);
253
+ if (placement === "hpane") await (0, lib_exports.runInPaneHorizontal)(full);
254
+ setNote(`\u2197 ${a.label} launched${item.branch ? ` \xB7 ${item.branch}` : ""}`);
255
+ setTimeout(onBack, 900);
256
+ } catch (e) {
257
+ setNote(`\u2717 ${e.message}`);
258
+ }
259
+ };
260
+ useInput((input, key) => {
261
+ if (key.escape || input === "q") {
262
+ if (step === "place") {
263
+ setStep("agent");
264
+ setCursor(0);
265
+ return;
266
+ }
267
+ return onBack();
268
+ }
269
+ if (key.return) {
270
+ if (step === "agent") {
271
+ setAgent((agents ?? [])[safeCursor] ?? null);
272
+ setStep("place");
273
+ setCursor(0);
274
+ } else if (agent) {
275
+ void launch(agent, PLACEMENTS[safeCursor].id);
276
+ }
277
+ }
278
+ });
279
+ return /* @__PURE__ */ jsx(
280
+ DrillView,
281
+ {
282
+ title: `Run AI \xB7 #${item.number} \xB7 ${item.repo}`,
283
+ subtitle: step === "agent" ? "choose an agent" : `${agent?.label} \u2014 choose where`,
284
+ hints: [
285
+ ["\u2191\u2193", "nav"],
286
+ ["\u21B5", step === "agent" ? "choose" : "launch"],
287
+ ["q/esc", step === "place" ? "back" : "close"]
288
+ ],
289
+ children: !agents ? /* @__PURE__ */ jsx(Text, { color: colors.info, children: "Detecting agents\u2026" }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
290
+ list.map((row, i) => /* @__PURE__ */ jsxs(Box, { children: [
291
+ /* @__PURE__ */ jsx(Text, { color: colors.info, children: i === safeCursor ? " \u276F " : " " }),
292
+ /* @__PURE__ */ jsx(Text, { bold: i === safeCursor, children: row.label }),
293
+ row.hint ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " + row.hint }) : null
294
+ ] }, row.label)),
295
+ prompt ? /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
296
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `prompt ${prompt}` }),
297
+ focused && focused.cmd && !focused.acceptsPrompt ? /* @__PURE__ */ jsx(
298
+ Text,
299
+ {
300
+ dimColor: true,
301
+ children: ` ${focused.label} takes no prompt \u2014 starts cold`
302
+ }
303
+ ) : null
304
+ ] }) : null,
305
+ note ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: colors.success, children: note }) }) : null
306
+ ] })
307
+ }
308
+ );
309
+ };
310
+
311
+ export { AiLauncher, CopyPromptNotice, DrillView, __export, __reExport, computeHealth, lib_exports, seedPromptFor, signalPath, toGHItem, withRetry };
@@ -0,0 +1,7 @@
1
+ import { InboxExtension } from '@kud/gh-ink';
2
+
3
+ declare const delegateExtension: InboxExtension;
4
+
5
+ declare const copyPromptExtension: InboxExtension;
6
+
7
+ export { copyPromptExtension, delegateExtension };
@@ -0,0 +1,69 @@
1
+ import { AiLauncher, seedPromptFor, CopyPromptNotice } from '../chunk-A3NXVFEI.js';
2
+ import { useEffect } from 'react';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ var isDelegatable = (item) => item?.kind === "pr" || item?.kind === "issue";
6
+ var DelegateScreen = ({
7
+ item,
8
+ login,
9
+ onExit
10
+ }) => {
11
+ const delegatable = isDelegatable(item);
12
+ useEffect(() => {
13
+ if (!delegatable) onExit();
14
+ }, [delegatable, onExit]);
15
+ if (!delegatable) return null;
16
+ return /* @__PURE__ */ jsx(
17
+ AiLauncher,
18
+ {
19
+ item,
20
+ login,
21
+ prompt: seedPromptFor(item),
22
+ onBack: onExit
23
+ }
24
+ );
25
+ };
26
+ var delegateExtension = {
27
+ id: "delegate",
28
+ title: "Delegate to an agent",
29
+ key: "a",
30
+ // Short enough for the footer strip, where "delegate to an agent" would crowd out
31
+ // the bindings either side of it. Matches the drill views' own `a AI` hint.
32
+ hint: "AI",
33
+ // Item-scoped: this acts on the selected row, so it earns a place in that row's
34
+ // action menu under `m`. Without this it would still work as a keypress and stay
35
+ // absent from the menu, which is exactly the gap that prompted 0.5.0.
36
+ scope: "item",
37
+ body: (onExit, target) => /* @__PURE__ */ jsx(
38
+ DelegateScreen,
39
+ {
40
+ item: target?.item,
41
+ login: target?.login ?? "",
42
+ onExit
43
+ }
44
+ )
45
+ };
46
+ var isCopyable = (item) => item?.kind === "pr" || item?.kind === "issue";
47
+ var CopyPromptScreen = ({
48
+ item,
49
+ onExit
50
+ }) => {
51
+ const copyable = isCopyable(item);
52
+ useEffect(() => {
53
+ if (!copyable) onExit();
54
+ }, [copyable, onExit]);
55
+ if (!copyable) return null;
56
+ return /* @__PURE__ */ jsx(CopyPromptNotice, { item, onBack: onExit });
57
+ };
58
+ var copyPromptExtension = {
59
+ id: "copy-prompt",
60
+ title: "Copy prompt to clipboard",
61
+ // `y` for yank: `c` and `b` are gh-ink's own copy-URL and copy-branch, and `p`
62
+ // is open-repo-in-pane.
63
+ key: "y",
64
+ hint: "copy prompt",
65
+ scope: "item",
66
+ body: (onExit, target) => /* @__PURE__ */ jsx(CopyPromptScreen, { item: target?.item, onExit })
67
+ };
68
+
69
+ export { copyPromptExtension, delegateExtension };