@grackle-ai/web-components 0.107.2 → 0.108.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.
Files changed (27) hide show
  1. package/.rush/temp/{3ae72563f781afd72723475938136f113846603e.untar.log → 6a006f2c0da21b52033b551b9ab7fe5476f98104.untar.log} +2 -2
  2. package/.rush/temp/{bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log → 8d311efca0ab9b63564c69382ef5e7be719f9d07.untar.log} +2 -2
  3. package/.rush/temp/operation/_phase_build/all.log +5 -5
  4. package/.rush/temp/operation/_phase_build/log-chunks.jsonl +5 -5
  5. package/.rush/temp/operation/_phase_build/state.json +1 -1
  6. package/.rush/temp/operation/_phase_test/all.log +15 -15
  7. package/.rush/temp/operation/_phase_test/log-chunks.jsonl +15 -15
  8. package/.rush/temp/operation/_phase_test/state.json +1 -1
  9. package/dist/index.js +7038 -6764
  10. package/package.json +2 -2
  11. package/rush-logs/web-components._phase_build.cache.log +1 -1
  12. package/rush-logs/web-components._phase_test.cache.log +1 -1
  13. package/src/components/panels/EnvironmentEditPanel.stories.tsx +1 -0
  14. package/src/components/panels/EnvironmentEditPanel.tsx +65 -8
  15. package/src/components/panels/GitHubAccountsPanel.tsx +223 -0
  16. package/src/components/panels/index.ts +2 -0
  17. package/src/components/settings/SettingsNav.stories.tsx +11 -10
  18. package/src/components/settings/SettingsNav.tsx +2 -1
  19. package/src/context/GrackleContextTypes.ts +3 -0
  20. package/src/hooks/types.ts +36 -3
  21. package/src/index.ts +4 -3
  22. package/src/mocks/MockGrackleProvider.tsx +21 -0
  23. package/src/mocks/mockData.ts +5 -0
  24. package/src/test-utils/storybook-helpers.ts +1 -0
  25. package/src/utils/breadcrumbs.test.ts +1 -0
  26. package/src/utils/dashboard.test.ts +1 -0
  27. package/src/utils/navigation.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grackle-ai/web-components",
3
- "version": "0.107.2",
3
+ "version": "0.108.0",
4
4
  "description": "Presentational React component library for the Grackle web UI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,7 +40,7 @@
40
40
  "remark-gfm": "^4.0.0",
41
41
  "lucide-react": "~0.474.0",
42
42
  "react-router": "^7.0.0",
43
- "@grackle-ai/common": "0.107.2"
43
+ "@grackle-ai/common": "0.108.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@rushstack/heft": "1.2.7",
@@ -1,4 +1,4 @@
1
1
  Build cache hit.
2
- Cache key: bc1d5bf9201ce71abeaeaddd096deb9b0805d703
2
+ Cache key: 8d311efca0ab9b63564c69382ef5e7be719f9d07
3
3
  Clearing cached folders: dist, storybook-static, .rush/temp/operation/_phase_build
4
4
  Successfully restored output from the build cache.
@@ -1,4 +1,4 @@
1
1
  Build cache hit.
2
- Cache key: 3ae72563f781afd72723475938136f113846603e
2
+ Cache key: 6a006f2c0da21b52033b551b9ab7fe5476f98104
3
3
  Clearing cached folders: .rush/temp/operation/_phase_test
4
4
  Successfully restored output from the build cache.
@@ -9,6 +9,7 @@ const meta: Meta<typeof EnvironmentEditPanel> = {
9
9
  args: {
10
10
  mode: "new",
11
11
  environments: [],
12
+ githubAccounts: [],
12
13
  onAddEnvironment: fn(),
13
14
  onUpdateEnvironment: fn(),
14
15
  onListCodespaces: fn(),
@@ -1,6 +1,6 @@
1
1
  import { useState, useCallback, type JSX } from "react";
2
2
  import type { ToastVariant } from "../../context/ToastContext.js";
3
- import type { Environment, Codespace } from "../../hooks/types.js";
3
+ import type { Environment, Codespace, GitHubAccountData } from "../../hooks/types.js";
4
4
  import { ENVIRONMENTS_URL, environmentUrl, useAppNavigate } from "../../utils/navigation.js";
5
5
  import { EditableTextField } from "../editable/EditableTextField.js";
6
6
  import styles from "./EnvironmentEditPanel.module.scss";
@@ -17,12 +17,14 @@ interface Props {
17
17
  environmentId?: string;
18
18
  /** All environments (for lookup in edit mode). */
19
19
  environments: Environment[];
20
+ /** All registered GitHub accounts for the account selector. */
21
+ githubAccounts: GitHubAccountData[];
20
22
  /** Callback to add a new environment. */
21
- onAddEnvironment: (displayName: string, adapterType: string, adapterConfig?: Record<string, unknown>) => void;
23
+ onAddEnvironment: (displayName: string, adapterType: string, adapterConfig?: Record<string, unknown>, githubAccountId?: string) => void;
22
24
  /** Callback to update an existing environment. */
23
- onUpdateEnvironment: (environmentId: string, fields: { displayName?: string; adapterConfig?: Record<string, unknown> }) => void;
24
- /** Callback to list available codespaces. */
25
- onListCodespaces: () => void;
25
+ onUpdateEnvironment: (environmentId: string, fields: { displayName?: string; adapterConfig?: Record<string, unknown>; githubAccountId?: string }) => void;
26
+ /** Callback to list available codespaces, optionally filtered by GitHub account. */
27
+ onListCodespaces: (githubAccountId?: string) => void;
26
28
  /** Available codespaces. */
27
29
  codespaces: Codespace[];
28
30
  /** Error from codespace operations. */
@@ -200,7 +202,7 @@ function CodespacePicker({ codespaceName, onCodespaceNameChange, envName, onEnvN
200
202
  * - edit: pre-populated form; uses click-to-edit fields that auto-save via
201
203
  * updateEnvironment.
202
204
  */
203
- export function EnvironmentEditPanel({ mode, environmentId, environments, onAddEnvironment, onUpdateEnvironment, onListCodespaces, codespaces, codespaceError, codespaceListError, codespaceCreating, onCreateCodespace, onShowToast }: Props): JSX.Element {
205
+ export function EnvironmentEditPanel({ mode, environmentId, environments, githubAccounts, onAddEnvironment, onUpdateEnvironment, onListCodespaces, codespaces, codespaceError, codespaceListError, codespaceCreating, onCreateCodespace, onShowToast }: Props): JSX.Element {
204
206
  const navigate = useAppNavigate();
205
207
 
206
208
  const isEdit = mode === "edit";
@@ -219,6 +221,7 @@ export function EnvironmentEditPanel({ mode, environmentId, environments, onAddE
219
221
  const [image, setImage] = useState("");
220
222
  const [repo, setRepo] = useState("");
221
223
  const [codespaceName, setCodespaceName] = useState("");
224
+ const [githubAccountId, setGithubAccountId] = useState("");
222
225
 
223
226
  // ─── Edit mode state ───────────────────────────────
224
227
 
@@ -286,7 +289,7 @@ export function EnvironmentEditPanel({ mode, environmentId, environments, onAddE
286
289
  if (!isCreateValid()) {
287
290
  return;
288
291
  }
289
- onAddEnvironment(envName.trim(), adapterType, buildCreateConfig());
292
+ onAddEnvironment(envName.trim(), adapterType, buildCreateConfig(), githubAccountId || undefined);
290
293
  onShowToast?.("Environment added successfully", "success");
291
294
  navigate(ENVIRONMENTS_URL, { replace: true });
292
295
  };
@@ -406,6 +409,32 @@ export function EnvironmentEditPanel({ mode, environmentId, environments, onAddE
406
409
  </span>
407
410
  </div>
408
411
 
412
+ {/* GitHub Account (codespace and docker only). Show when accounts are
413
+ registered OR when the env already has an account association so
414
+ the user can clear it even if the referenced account was removed. */}
415
+ {(existingEnv.adapterType === "codespace" || existingEnv.adapterType === "docker") && (githubAccounts.length > 0 || Boolean(existingEnv.githubAccountId)) && (
416
+ <div className={styles.section}>
417
+ <label className={styles.label}>GitHub Account</label>
418
+ <select
419
+ value={existingEnv.githubAccountId || ""}
420
+ onChange={(e) => {
421
+ if (environmentId) {
422
+ onUpdateEnvironment(environmentId, { githubAccountId: e.target.value });
423
+ }
424
+ }}
425
+ className={styles.adapterSelect}
426
+ data-testid="env-edit-github-account"
427
+ >
428
+ <option value="">(Default)</option>
429
+ {githubAccounts.map((a) => (
430
+ <option key={a.id} value={a.id}>
431
+ {a.label}{a.username ? ` (@${a.username})` : ""}{a.isDefault ? " — default" : ""}
432
+ </option>
433
+ ))}
434
+ </select>
435
+ </div>
436
+ )}
437
+
409
438
  {/* Adapter-specific editable fields */}
410
439
  {existingEnv.adapterType === "local" && (
411
440
  <>
@@ -620,7 +649,7 @@ export function EnvironmentEditPanel({ mode, environmentId, environments, onAddE
620
649
  onChange={(e) => {
621
650
  setAdapterType(e.target.value);
622
651
  if (e.target.value === "codespace") {
623
- onListCodespaces();
652
+ onListCodespaces(githubAccountId || undefined);
624
653
  }
625
654
  }}
626
655
  className={styles.adapterSelect}
@@ -633,6 +662,34 @@ export function EnvironmentEditPanel({ mode, environmentId, environments, onAddE
633
662
  </select>
634
663
  </div>
635
664
 
665
+ {/* GitHub Account (codespace and docker only) */}
666
+ {(adapterType === "codespace" || adapterType === "docker") && githubAccounts.length > 0 && (
667
+ <div className={styles.section}>
668
+ <label className={styles.label} htmlFor="env-create-github-account">
669
+ GitHub Account
670
+ </label>
671
+ <select
672
+ id="env-create-github-account"
673
+ value={githubAccountId}
674
+ onChange={(e) => {
675
+ setGithubAccountId(e.target.value);
676
+ if (adapterType === "codespace") {
677
+ onListCodespaces(e.target.value || undefined);
678
+ }
679
+ }}
680
+ className={styles.adapterSelect}
681
+ data-testid="env-create-github-account"
682
+ >
683
+ <option value="">(Default)</option>
684
+ {githubAccounts.map((a) => (
685
+ <option key={a.id} value={a.id}>
686
+ {a.label} (@{a.username}){a.isDefault ? " — default" : ""}
687
+ </option>
688
+ ))}
689
+ </select>
690
+ </div>
691
+ )}
692
+
636
693
  {/* Adapter-specific fields */}
637
694
  {adapterType === "local" && (
638
695
  <>
@@ -0,0 +1,223 @@
1
+ import { useState, type JSX, type FormEvent } from "react";
2
+ import { X, Star } from "lucide-react";
3
+ import type { ToastVariant } from "../../context/ToastContext.js";
4
+ import type { GitHubAccountData } from "../../hooks/types.js";
5
+ import { ICON_MD } from "../../utils/iconSize.js";
6
+ import { ConfirmDialog } from "../display/index.js";
7
+ import styles from "./SettingsPanel.module.scss";
8
+
9
+ /** Props for the GitHubAccountsPanel component. */
10
+ export interface GitHubAccountsPanelProps {
11
+ /** All registered GitHub accounts. */
12
+ githubAccounts: GitHubAccountData[];
13
+ /** Whether the account list is loading. */
14
+ githubAccountsLoading: boolean;
15
+ /** Register a new GitHub account. Username will be resolved server-side if empty. */
16
+ onAddGitHubAccount: (label: string, token: string, username: string, isDefault: boolean) => Promise<void>;
17
+ /** Update an existing GitHub account. */
18
+ onUpdateGitHubAccount: (id: string, fields: { label?: string; token?: string; isDefault?: boolean }) => Promise<void>;
19
+ /** Remove a GitHub account by ID. */
20
+ onRemoveGitHubAccount: (id: string) => Promise<void>;
21
+ /** Import accounts from the local gh CLI authentication state. */
22
+ onImportGitHubAccounts: () => Promise<{ imported: number; usernames: string[] }>;
23
+ /** Display a toast notification. */
24
+ onShowToast?: (message: string, variant?: ToastVariant) => void;
25
+ }
26
+
27
+ /** Settings panel for managing registered GitHub accounts. */
28
+ export function GitHubAccountsPanel({
29
+ githubAccounts,
30
+ githubAccountsLoading,
31
+ onAddGitHubAccount,
32
+ onUpdateGitHubAccount,
33
+ onRemoveGitHubAccount,
34
+ onImportGitHubAccounts,
35
+ onShowToast,
36
+ }: GitHubAccountsPanelProps): JSX.Element {
37
+
38
+ const [label, setLabel] = useState("");
39
+ const [token, setToken] = useState("");
40
+ const [isDefault, setIsDefault] = useState(false);
41
+ const [adding, setAdding] = useState(false);
42
+ const [importing, setImporting] = useState(false);
43
+ const [confirmRemoveId, setConfirmRemoveId] = useState<string | null>(null);
44
+
45
+ const confirmRemoveAccount = githubAccounts.find((a) => a.id === confirmRemoveId);
46
+
47
+ const handleSubmit = async (e: FormEvent): Promise<void> => {
48
+ e.preventDefault();
49
+ if (!label.trim() || !token.trim()) {
50
+ return;
51
+ }
52
+ setAdding(true);
53
+ try {
54
+ await onAddGitHubAccount(label.trim(), token.trim(), "", isDefault);
55
+ onShowToast?.("GitHub account added", "success");
56
+ setLabel("");
57
+ setToken("");
58
+ setIsDefault(false);
59
+ } catch (err) {
60
+ const msg = err instanceof Error ? err.message : "Failed to add account";
61
+ onShowToast?.(msg, "error");
62
+ } finally {
63
+ setAdding(false);
64
+ }
65
+ };
66
+
67
+ const handleSetDefault = async (id: string): Promise<void> => {
68
+ try {
69
+ await onUpdateGitHubAccount(id, { isDefault: true });
70
+ onShowToast?.("Default account updated", "success");
71
+ } catch (err) {
72
+ const msg = err instanceof Error ? err.message : "Failed to update account";
73
+ onShowToast?.(msg, "error");
74
+ }
75
+ };
76
+
77
+ const handleConfirmRemove = async (): Promise<void> => {
78
+ if (!confirmRemoveId) {
79
+ return;
80
+ }
81
+ try {
82
+ await onRemoveGitHubAccount(confirmRemoveId);
83
+ onShowToast?.("GitHub account removed", "info");
84
+ } catch (err) {
85
+ const msg = err instanceof Error ? err.message : "Failed to remove account";
86
+ onShowToast?.(msg, "error");
87
+ } finally {
88
+ setConfirmRemoveId(null);
89
+ }
90
+ };
91
+
92
+ const handleImport = async (): Promise<void> => {
93
+ setImporting(true);
94
+ try {
95
+ const result = await onImportGitHubAccounts();
96
+ if (result.imported > 0) {
97
+ onShowToast?.(`Imported ${result.imported} account(s): ${result.usernames.join(", ")}`, "success");
98
+ } else {
99
+ onShowToast?.("No new accounts to import", "info");
100
+ }
101
+ } catch (err) {
102
+ const msg = err instanceof Error ? err.message : "Import failed";
103
+ onShowToast?.(msg, "error");
104
+ } finally {
105
+ setImporting(false);
106
+ }
107
+ };
108
+
109
+ return (
110
+ <>
111
+ <ConfirmDialog
112
+ isOpen={confirmRemoveId !== null}
113
+ title="Remove GitHub Account?"
114
+ description={
115
+ confirmRemoveAccount
116
+ ? `"${confirmRemoveAccount.label}"${confirmRemoveAccount.username ? ` (@${confirmRemoveAccount.username})` : ""} will be permanently removed.`
117
+ : undefined
118
+ }
119
+ onConfirm={() => { handleConfirmRemove().catch(() => {}); }}
120
+ onCancel={() => setConfirmRemoveId(null)}
121
+ />
122
+
123
+ <section className={styles.section} data-testid="github-accounts-panel">
124
+ <h3 className={styles.sectionTitle}>GitHub Accounts</h3>
125
+ <p className={styles.sectionDescription}>
126
+ Register multiple GitHub accounts to use different identities per environment.
127
+ The default account is used when no specific account is assigned.
128
+ </p>
129
+
130
+ {githubAccountsLoading && githubAccounts.length === 0 ? (
131
+ <div className={styles.emptyState}>Loading...</div>
132
+ ) : githubAccounts.length === 0 ? (
133
+ <div className={styles.emptyStateInfo}>
134
+ No GitHub accounts registered. Add one below or import from the gh CLI.
135
+ </div>
136
+ ) : (
137
+ <div className={styles.tokenList}>
138
+ {githubAccounts.map((account) => (
139
+ <div key={account.id} className={styles.tokenRow} data-testid={`github-account-row-${account.id}`}>
140
+ {account.isDefault && (
141
+ <span className={styles.tokenBadge} title="Default account">default</span>
142
+ )}
143
+ <span className={styles.tokenName}>{account.label}</span>
144
+ {account.username && (
145
+ <span className={styles.tokenTarget}>@{account.username}</span>
146
+ )}
147
+ {!account.isDefault && (
148
+ <button
149
+ className={styles.deleteButton}
150
+ onClick={() => { handleSetDefault(account.id).catch(() => {}); }}
151
+ title="Set as default"
152
+ aria-label={`Set ${account.label} as default`}
153
+ data-testid={`github-account-set-default-${account.id}`}
154
+ >
155
+ <Star size={ICON_MD} aria-hidden="true" />
156
+ </button>
157
+ )}
158
+ <button
159
+ className={styles.deleteButton}
160
+ onClick={() => setConfirmRemoveId(account.id)}
161
+ title={`Remove ${account.label}`}
162
+ aria-label={`Remove ${account.label}`}
163
+ data-testid={`github-account-remove-${account.id}`}
164
+ >
165
+ <X size={ICON_MD} aria-hidden="true" />
166
+ </button>
167
+ </div>
168
+ ))}
169
+ </div>
170
+ )}
171
+
172
+ <form className={styles.addForm} onSubmit={(e) => { handleSubmit(e).catch(() => {}); }}>
173
+ <div className={styles.formRow}>
174
+ <input
175
+ className={styles.input}
176
+ type="text"
177
+ placeholder="Label (e.g. personal, work)"
178
+ value={label}
179
+ onChange={(e) => setLabel(e.target.value)}
180
+ data-testid="github-account-label-input"
181
+ />
182
+ <input
183
+ className={styles.input}
184
+ type="password"
185
+ placeholder="Personal access token"
186
+ value={token}
187
+ onChange={(e) => setToken(e.target.value)}
188
+ data-testid="github-account-token-input"
189
+ />
190
+ </div>
191
+ <div className={styles.formRow}>
192
+ <label className={styles.tokenTarget} style={{ display: "flex", alignItems: "center", gap: "6px", cursor: "pointer" }}>
193
+ <input
194
+ type="checkbox"
195
+ checked={isDefault}
196
+ onChange={(e) => setIsDefault(e.target.checked)}
197
+ data-testid="github-account-default-checkbox"
198
+ />
199
+ Set as default
200
+ </label>
201
+ <button
202
+ className={styles.addButton}
203
+ type="submit"
204
+ disabled={!label.trim() || !token.trim() || adding}
205
+ data-testid="github-account-add-button"
206
+ >
207
+ {adding ? "Adding..." : "Add Account"}
208
+ </button>
209
+ <button
210
+ className={styles.addButton}
211
+ type="button"
212
+ onClick={() => { handleImport().catch(() => {}); }}
213
+ disabled={importing}
214
+ data-testid="github-account-import-button"
215
+ >
216
+ {importing ? "Importing..." : "Import from gh CLI"}
217
+ </button>
218
+ </div>
219
+ </form>
220
+ </section>
221
+ </>
222
+ );
223
+ }
@@ -11,3 +11,5 @@ export { TaskActionButtons } from "./TaskActionButtons.js";
11
11
  export { TaskOverviewPanel } from "./TaskOverviewPanel.js";
12
12
  export { PluginsPanel } from "./PluginsPanel.js";
13
13
  export type { PluginsPanelProps } from "./PluginsPanel.js";
14
+ export { GitHubAccountsPanel } from "./GitHubAccountsPanel.js";
15
+ export type { GitHubAccountsPanelProps } from "./GitHubAccountsPanel.js";
@@ -9,10 +9,11 @@ const meta: Meta<typeof SettingsNav> = {
9
9
  export default meta;
10
10
  type Story = StoryObj<typeof meta>;
11
11
 
12
- /** All five tabs are rendered with correct labels. */
12
+ /** All tabs are rendered with correct labels. */
13
13
  export const AllTabsRendered: Story = {
14
14
  play: async ({ canvas }) => {
15
15
  await expect(canvas.getByRole("tab", { name: /Credentials/ })).toBeInTheDocument();
16
+ await expect(canvas.getByRole("tab", { name: /GitHub Accounts/ })).toBeInTheDocument();
16
17
  await expect(canvas.getByRole("tab", { name: /Personas/ })).toBeInTheDocument();
17
18
  await expect(canvas.getByRole("tab", { name: /Schedules/ })).toBeInTheDocument();
18
19
  await expect(canvas.getByRole("tab", { name: /Appearance/ })).toBeInTheDocument();
@@ -35,15 +36,15 @@ export const KeyboardNavigation: Story = {
35
36
  const credentialsTab = canvas.getByRole("tab", { name: /Credentials/ });
36
37
  credentialsTab.focus();
37
38
 
38
- // ArrowDown should move to Personas
39
+ // ArrowDown should move to GitHub (now second tab)
39
40
  await userEvent.keyboard("{ArrowDown}");
40
- const personasTab = canvas.getByRole("tab", { name: /Personas/ });
41
- await expect(personasTab).toHaveFocus();
41
+ const githubTab = canvas.getByRole("tab", { name: /GitHub Accounts/ });
42
+ await expect(githubTab).toHaveFocus();
42
43
 
43
- // ArrowDown again to Schedules
44
+ // ArrowDown again to Personas
44
45
  await userEvent.keyboard("{ArrowDown}");
45
- const schedulesTab = canvas.getByRole("tab", { name: /Schedules/ });
46
- await expect(schedulesTab).toHaveFocus();
46
+ const personasTab = canvas.getByRole("tab", { name: /Personas/ });
47
+ await expect(personasTab).toHaveFocus();
47
48
 
48
49
  // Home goes to first tab
49
50
  await userEvent.keyboard("{Home}");
@@ -62,10 +63,10 @@ export const JKNavigation: Story = {
62
63
  const credentialsTab = canvas.getByRole("tab", { name: /Credentials/ });
63
64
  credentialsTab.focus();
64
65
 
65
- // J moves down to Personas
66
+ // J moves down to GitHub (now second tab)
66
67
  await userEvent.keyboard("j");
67
- const personasTab = canvas.getByRole("tab", { name: /Personas/ });
68
- await expect(personasTab).toHaveFocus();
68
+ const githubTab = canvas.getByRole("tab", { name: /GitHub Accounts/ });
69
+ await expect(githubTab).toHaveFocus();
69
70
 
70
71
  // K moves back up to Credentials
71
72
  await userEvent.keyboard("k");
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useRef, type JSX, type KeyboardEvent, type ReactNode } from "react";
2
2
  import { useLocation } from "react-router";
3
- import { CalendarClock, Info, Key, Keyboard, Palette, Puzzle, User } from "lucide-react";
3
+ import { CalendarClock, Github, Info, Key, Keyboard, Palette, Puzzle, User } from "lucide-react";
4
4
  import { SETTINGS_URL, useAppNavigate } from "../../utils/navigation.js";
5
5
  import { ICON_LG } from "../../utils/iconSize.js";
6
6
  import styles from "./SettingsNav.module.scss";
@@ -18,6 +18,7 @@ interface SettingsTab {
18
18
  /** Ordered list of settings tabs. */
19
19
  const TABS: SettingsTab[] = [
20
20
  { path: "credentials", label: "Credentials", icon: <Key size={ICON_LG} /> },
21
+ { path: "github-accounts", label: "GitHub Accounts", icon: <Github size={ICON_LG} /> },
21
22
  { path: "personas", label: "Personas", icon: <User size={ICON_LG} /> },
22
23
  { path: "schedules", label: "Schedules", icon: <CalendarClock size={ICON_LG} /> },
23
24
  { path: "appearance", label: "Appearance", icon: <Palette size={ICON_LG} /> },
@@ -14,6 +14,7 @@ import type {
14
14
  UsePluginsResult,
15
15
  UseSchedulesResult,
16
16
  UseStreamsResult,
17
+ UseGitHubAccountsResult,
17
18
  ConnectionStatus,
18
19
  } from "../hooks/types.js";
19
20
 
@@ -47,6 +48,8 @@ export interface UseGrackleSocketResult {
47
48
  knowledge: Omit<UseKnowledgeResult, "handleEvent">;
48
49
  /** Plugin state and actions. */
49
50
  plugins: Omit<UsePluginsResult, "domainHook">;
51
+ /** GitHub account state and actions. */
52
+ githubAccounts: Omit<UseGitHubAccountsResult, "domainHook">;
50
53
  /** App-level default persona ID setting. */
51
54
  appDefaultPersonaId: string;
52
55
  /** Update the app-level default persona ID. */
@@ -32,6 +32,8 @@ export interface Environment {
32
32
  adapterConfig: string;
33
33
  status: string;
34
34
  bootstrapped: boolean;
35
+ /** ID of the GitHub account to use for gh CLI operations, or empty string for default. */
36
+ githubAccountId: string;
35
37
  }
36
38
 
37
39
 
@@ -223,11 +225,12 @@ export interface UseEnvironmentsResult {
223
225
  displayName: string,
224
226
  adapterType: string,
225
227
  adapterConfig?: Record<string, unknown>,
228
+ githubAccountId?: string,
226
229
  ) => Promise<void>;
227
230
  /** Update an existing environment's mutable fields. */
228
231
  updateEnvironment: (
229
232
  environmentId: string,
230
- fields: { displayName?: string; adapterConfig?: Record<string, unknown> },
233
+ fields: { displayName?: string; adapterConfig?: Record<string, unknown>; githubAccountId?: string },
231
234
  ) => Promise<void>;
232
235
  /** Provision an environment by ID. When force is true, kills active sessions and forces full provision. */
233
236
  provisionEnvironment: (environmentId: string, force?: boolean) => Promise<void>;
@@ -269,6 +272,7 @@ export interface UseSessionsResult {
269
272
  prompt: string,
270
273
  personaId?: string,
271
274
  workingDirectory?: string,
275
+ workspaceId?: string,
272
276
  ) => Promise<void>;
273
277
  /** Send text input to a running session. */
274
278
  sendInput: (sessionId: string, text: string) => Promise<void>;
@@ -484,8 +488,8 @@ export interface UseCodespacesResult {
484
488
  codespaceListError: string;
485
489
  /** Whether a codespace creation is currently in progress. */
486
490
  codespaceCreating: boolean;
487
- /** Request the current codespace list from the server. */
488
- listCodespaces: () => Promise<void>;
491
+ /** Request the current codespace list from the server, optionally filtered to a GitHub account. */
492
+ listCodespaces: (githubAccountId?: string) => Promise<void>;
489
493
  /** Create a new codespace for the given repo. */
490
494
  createCodespace: (repo: string, machine?: string) => Promise<void>;
491
495
  /** Lifecycle hook for connect/disconnect/event routing. */
@@ -854,6 +858,35 @@ export interface UsePluginsResult {
854
858
  domainHook: DomainHook;
855
859
  }
856
860
 
861
+ /** A registered GitHub account (token is never returned by the server). */
862
+ export interface GitHubAccountData {
863
+ id: string;
864
+ label: string;
865
+ username: string;
866
+ isDefault: boolean;
867
+ createdAt: string;
868
+ }
869
+
870
+ /** Values returned by the GitHub accounts domain hook. */
871
+ export interface UseGitHubAccountsResult {
872
+ /** All registered GitHub accounts. */
873
+ githubAccounts: GitHubAccountData[];
874
+ /** Whether the account list is currently loading. */
875
+ githubAccountsLoading: boolean;
876
+ /** Refresh the account list from the server. */
877
+ loadGitHubAccounts: () => Promise<void>;
878
+ /** Register a new GitHub account. */
879
+ addGitHubAccount: (label: string, token: string, username: string, isDefault: boolean) => Promise<void>;
880
+ /** Update an existing GitHub account. */
881
+ updateGitHubAccount: (id: string, fields: { label?: string; token?: string; isDefault?: boolean }) => Promise<void>;
882
+ /** Remove a GitHub account by ID. */
883
+ removeGitHubAccount: (id: string) => Promise<void>;
884
+ /** Import accounts from the local gh CLI authentication state. */
885
+ importGitHubAccounts: () => Promise<{ imported: number; usernames: string[] }>;
886
+ /** Lifecycle hook for connect/disconnect/event routing. */
887
+ domainHook: DomainHook;
888
+ }
889
+
857
890
  /** Delay in milliseconds before attempting a WebSocket reconnect. */
858
891
  export const WS_RECONNECT_DELAY_MS: number = 3_000;
859
892
 
package/src/index.ts CHANGED
@@ -63,8 +63,8 @@ export type { CalloutVariant } from "./components/notifications/index.js";
63
63
  export { UpdateBanner } from "./components/notifications/UpdateBanner.js";
64
64
 
65
65
  // Panels
66
- export { FindingsPanel, TokensPanel, AppearancePanel, AboutPanel, TaskEditPanel, TaskActionButtons, TaskOverviewPanel, PluginsPanel } from "./components/panels/index.js";
67
- export type { PluginsPanelProps } from "./components/panels/index.js";
66
+ export { FindingsPanel, TokensPanel, AppearancePanel, AboutPanel, TaskEditPanel, TaskActionButtons, TaskOverviewPanel, PluginsPanel, GitHubAccountsPanel } from "./components/panels/index.js";
67
+ export type { PluginsPanelProps, GitHubAccountsPanelProps } from "./components/panels/index.js";
68
68
  export type { TaskActionButtonsProps } from "./components/panels/TaskActionButtons.js";
69
69
  export type { TaskOverviewPanelProps } from "./components/panels/TaskOverviewPanel.js";
70
70
  export { EnvironmentEditPanel } from "./components/panels/EnvironmentEditPanel.js";
@@ -134,6 +134,7 @@ export type {
134
134
  UseCredentialsResult, UseCodespacesResult, UsePersonasResult,
135
135
  UsePluginsResult, PluginData,
136
136
  StreamData, StreamSubscriberData, UseStreamsResult,
137
+ UseGitHubAccountsResult, GitHubAccountData,
137
138
  DomainHook,
138
139
  ConnectionStatus,
139
140
  } from "./hooks/types.js";
@@ -156,7 +157,7 @@ export {
156
157
  useAppNavigate, sessionUrl, workspaceUrl, taskUrl, taskEditUrl,
157
158
  newTaskUrl, newChatUrl, ENVIRONMENTS_URL, NEW_ENVIRONMENT_URL,
158
159
  environmentUrl, environmentEditUrl, SETTINGS_URL,
159
- SETTINGS_ENVIRONMENTS_URL, SETTINGS_CREDENTIALS_URL,
160
+ SETTINGS_ENVIRONMENTS_URL, SETTINGS_CREDENTIALS_URL, SETTINGS_GITHUB_ACCOUNTS_URL,
160
161
  PERSONAS_URL, NEW_PERSONA_URL, personaUrl,
161
162
  SCHEDULES_URL, NEW_SCHEDULE_URL, scheduleUrl,
162
163
  SETTINGS_APPEARANCE_URL, SETTINGS_ABOUT_URL, SETTINGS_SHORTCUTS_URL,
@@ -1300,6 +1300,27 @@ export function MockGrackleProvider({ children }: MockGrackleProviderProps): JSX
1300
1300
  domainHook: NOOP_DOMAIN_HOOK,
1301
1301
  },
1302
1302
 
1303
+ // ── GitHub Accounts ──────────────────────────────
1304
+
1305
+ githubAccounts: {
1306
+ githubAccounts: [],
1307
+ githubAccountsLoading: false,
1308
+ loadGitHubAccounts: async () => { console.log("[MockGrackle] loadGitHubAccounts"); },
1309
+ addGitHubAccount: async (label: string, _token: string, _username: string, _isDefault: boolean) => {
1310
+ console.log("[MockGrackle] addGitHubAccount", label);
1311
+ },
1312
+ updateGitHubAccount: async (id: string, fields: { label?: string; token?: string; isDefault?: boolean }) => {
1313
+ console.log("[MockGrackle] updateGitHubAccount", id, fields);
1314
+ },
1315
+ removeGitHubAccount: async (id: string) => {
1316
+ console.log("[MockGrackle] removeGitHubAccount", id);
1317
+ },
1318
+ importGitHubAccounts: async () => {
1319
+ console.log("[MockGrackle] importGitHubAccounts");
1320
+ return { imported: 0, usernames: [] };
1321
+ },
1322
+ },
1323
+
1303
1324
  // ── Plugins ─────────────────────────────────────
1304
1325
 
1305
1326
  plugins: {
@@ -29,6 +29,7 @@ export const MOCK_ENVIRONMENTS: Environment[] = [
29
29
  adapterConfig: "{}",
30
30
  status: "connected",
31
31
  bootstrapped: true,
32
+ githubAccountId: "",
32
33
  },
33
34
  {
34
35
  id: "env-docker-01",
@@ -37,6 +38,7 @@ export const MOCK_ENVIRONMENTS: Environment[] = [
37
38
  adapterConfig: '{"image":"node:20"}',
38
39
  status: "connected",
39
40
  bootstrapped: true,
41
+ githubAccountId: "",
40
42
  },
41
43
  {
42
44
  id: "env-cs-01",
@@ -45,6 +47,7 @@ export const MOCK_ENVIRONMENTS: Environment[] = [
45
47
  adapterConfig: '{"codespaceName":"my-codespace"}',
46
48
  status: "connected",
47
49
  bootstrapped: true,
50
+ githubAccountId: "",
48
51
  },
49
52
  {
50
53
  id: "env-remote-01",
@@ -53,6 +56,7 @@ export const MOCK_ENVIRONMENTS: Environment[] = [
53
56
  adapterConfig: '{"host":"192.168.1.10","user":"deploy","sshPort":22}',
54
57
  status: "disconnected",
55
58
  bootstrapped: false,
59
+ githubAccountId: "",
56
60
  },
57
61
  {
58
62
  id: "error-env",
@@ -61,6 +65,7 @@ export const MOCK_ENVIRONMENTS: Environment[] = [
61
65
  adapterConfig: "{}",
62
66
  status: "connected",
63
67
  bootstrapped: true,
68
+ githubAccountId: "",
64
69
  },
65
70
  ];
66
71
 
@@ -48,6 +48,7 @@ export function makeEnvironment(overrides: Partial<Environment> = {}): Environme
48
48
  adapterConfig: "{}",
49
49
  status: "connected",
50
50
  bootstrapped: true,
51
+ githubAccountId: "",
51
52
  ...overrides,
52
53
  };
53
54
  }