github-issue-tower-defence-management 1.101.6 → 1.101.8

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 (31) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/console/consoleServer.js +32 -10
  3. package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
  4. package/bin/adapter/entry-points/console/ui-dist/assets/index-ES6SLB1Y.css +1 -0
  5. package/bin/adapter/entry-points/console/ui-dist/assets/index-_g2CqG11.js +101 -0
  6. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapter/entry-points/console/consoleServer.test.ts +43 -0
  9. package/src/adapter/entry-points/console/consoleServer.ts +38 -11
  10. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +139 -145
  11. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleErrorToast.stories.tsx +23 -0
  12. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.test.tsx +44 -1
  13. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.tsx +25 -0
  14. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.test.ts +84 -1
  15. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.ts +31 -6
  16. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +2 -2
  17. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +2 -4
  18. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +37 -10
  19. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +19 -1
  20. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +8 -23
  21. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +0 -97
  22. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +10 -40
  23. package/src/adapter/entry-points/console/ui/src/index.css +6 -16
  24. package/src/adapter/entry-points/console/ui-dist/assets/index-ES6SLB1Y.css +1 -0
  25. package/src/adapter/entry-points/console/ui-dist/assets/index-_g2CqG11.js +101 -0
  26. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  27. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
  28. package/bin/adapter/entry-points/console/ui-dist/assets/index-D4zghbcI.css +0 -1
  29. package/bin/adapter/entry-points/console/ui-dist/assets/index-DX0mapkS.js +0 -101
  30. package/src/adapter/entry-points/console/ui-dist/assets/index-D4zghbcI.css +0 -1
  31. package/src/adapter/entry-points/console/ui-dist/assets/index-DX0mapkS.js +0 -101
@@ -7,7 +7,7 @@ import {
7
7
  export type ConsoleQueuedAction = {
8
8
  message: string;
9
9
  color: ConsoleToastColor;
10
- commit: () => void;
10
+ commit: () => Promise<void>;
11
11
  advance: () => void;
12
12
  };
13
13
 
@@ -18,6 +18,18 @@ export type ConsolePendingActionView = {
18
18
  progress: number;
19
19
  };
20
20
 
21
+ export type ConsoleActionError = {
22
+ message: string;
23
+ reason: string;
24
+ };
25
+
26
+ const errorReason = (error: unknown): string => {
27
+ if (error instanceof Error && error.message.length > 0) {
28
+ return error.message;
29
+ }
30
+ return String(error);
31
+ };
32
+
21
33
  const COUNTDOWN_TICK_MS = 100;
22
34
 
23
35
  const computeRemainingSeconds = (elapsedMs: number): number =>
@@ -28,12 +40,15 @@ const computeProgress = (elapsedMs: number): number =>
28
40
 
29
41
  export type ConsoleActionQueue = {
30
42
  pending: ConsolePendingActionView | null;
43
+ error: ConsoleActionError | null;
31
44
  enqueue: (action: ConsoleQueuedAction) => void;
32
45
  undo: () => void;
46
+ dismissError: () => void;
33
47
  };
34
48
 
35
49
  export const useConsoleActionQueue = (): ConsoleActionQueue => {
36
50
  const [pending, setPending] = useState<ConsolePendingActionView | null>(null);
51
+ const [error, setError] = useState<ConsoleActionError | null>(null);
37
52
  const actionRef = useRef<ConsoleQueuedAction | null>(null);
38
53
  const startRef = useRef<number>(0);
39
54
  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
@@ -46,6 +61,12 @@ export const useConsoleActionQueue = (): ConsoleActionQueue => {
46
61
  }
47
62
  }, []);
48
63
 
64
+ const runCommit = useCallback((action: ConsoleQueuedAction): void => {
65
+ action.commit().catch((cause: unknown) => {
66
+ setError({ message: action.message, reason: errorReason(cause) });
67
+ });
68
+ }, []);
69
+
49
70
  const commitPending = useCallback((): void => {
50
71
  const action = actionRef.current;
51
72
  clearTimer();
@@ -53,9 +74,9 @@ export const useConsoleActionQueue = (): ConsoleActionQueue => {
53
74
  setPending(null);
54
75
  if (action !== null && !committedRef.current) {
55
76
  committedRef.current = true;
56
- action.commit();
77
+ runCommit(action);
57
78
  }
58
- }, [clearTimer]);
79
+ }, [clearTimer, runCommit]);
59
80
 
60
81
  const undo = useCallback((): void => {
61
82
  clearTimer();
@@ -64,13 +85,17 @@ export const useConsoleActionQueue = (): ConsoleActionQueue => {
64
85
  setPending(null);
65
86
  }, [clearTimer]);
66
87
 
88
+ const dismissError = useCallback((): void => {
89
+ setError(null);
90
+ }, []);
91
+
67
92
  const enqueue = useCallback(
68
93
  (action: ConsoleQueuedAction): void => {
69
94
  if (actionRef.current !== null && !committedRef.current) {
70
95
  const previous = actionRef.current;
71
96
  clearTimer();
72
97
  committedRef.current = true;
73
- previous.commit();
98
+ runCommit(previous);
74
99
  }
75
100
  committedRef.current = false;
76
101
  actionRef.current = action;
@@ -96,10 +121,10 @@ export const useConsoleActionQueue = (): ConsoleActionQueue => {
96
121
  });
97
122
  }, COUNTDOWN_TICK_MS);
98
123
  },
99
- [clearTimer, commitPending],
124
+ [clearTimer, commitPending, runCommit],
100
125
  );
101
126
 
102
127
  useEffect(() => clearTimer, [clearTimer]);
103
128
 
104
- return { pending, enqueue, undo };
129
+ return { pending, error, enqueue, undo, dismissError };
105
130
  };
@@ -133,7 +133,7 @@ describe('useConsoleOperations', () => {
133
133
  });
134
134
  });
135
135
 
136
- it('does not mark done on snooze outside the todo-by-human tab', async () => {
136
+ it('marks done on snooze outside the todo-by-human tab so the item disappears immediately', async () => {
137
137
  captureFetch();
138
138
  const { result } = setup();
139
139
  await act(async () => {
@@ -145,7 +145,7 @@ describe('useConsoleOperations', () => {
145
145
  const stored = JSON.parse(
146
146
  localStorage.getItem(overlayStorageKey('umino')) ?? '{}',
147
147
  );
148
- expect(stored[issueItem.projectItemId]?.done).toBeUndefined();
148
+ expect(stored[issueItem.projectItemId].done).toBe(true);
149
149
  });
150
150
 
151
151
  it('marks done on snooze in the todo-by-human tab so the item is skipped', async () => {
@@ -146,11 +146,9 @@ export const useConsoleOperations = (
146
146
  projectItemId: item.projectItemId,
147
147
  };
148
148
  await postConsoleOperation(appendToken, TRIAGE_OPERATION_PATH, request);
149
- if (mode === 'todo-by-human') {
150
- markDone(item);
151
- }
149
+ markDone(item);
152
150
  },
153
- [pjcode, appendToken, markDone, mode],
151
+ [pjcode, appendToken, markDone],
154
152
  );
155
153
 
156
154
  const setStory = useCallback(
@@ -8,6 +8,17 @@ const mockFetchOnce = (body: unknown, ok = true): jest.Mock => {
8
8
  ok,
9
9
  status: ok ? 200 : 500,
10
10
  json: async () => body,
11
+ text: async () => (typeof body === 'string' ? body : JSON.stringify(body)),
12
+ });
13
+ global.fetch = fetchMock as unknown as typeof fetch;
14
+ return fetchMock;
15
+ };
16
+
17
+ const mockFetchFailureOnce = (status: number, rawBody: string): jest.Mock => {
18
+ const fetchMock = jest.fn().mockResolvedValue({
19
+ ok: false,
20
+ status,
21
+ text: async () => rawBody,
11
22
  });
12
23
  global.fetch = fetchMock as unknown as typeof fetch;
13
24
  return fetchMock;
@@ -169,15 +180,31 @@ describe('postConsoleOperation', () => {
169
180
  expect(init).toMatchObject({ method: 'POST' });
170
181
  });
171
182
 
172
- it('throws on a failed operation', async () => {
173
- mockFetchOnce({}, false);
174
- await expect(
175
- postConsoleOperation(appendToken, '/api/review', {
176
- pjcode: 'umino',
177
- action: 'approve',
178
- prUrl: 'https://github.com/o/r/pull/1',
179
- projectItemId: 'PVTI_1',
180
- }),
181
- ).rejects.toThrow('HTTP 500');
183
+ const failingReview = (): Promise<void> =>
184
+ postConsoleOperation(appendToken, '/api/review', {
185
+ pjcode: 'umino',
186
+ action: 'approve',
187
+ prUrl: 'https://github.com/o/r/pull/1',
188
+ projectItemId: 'PVTI_1',
189
+ });
190
+
191
+ it('throws the error reason from a JSON error body', async () => {
192
+ mockFetchFailureOnce(
193
+ 502,
194
+ JSON.stringify({ error: 'Failed to approve PR: HTTP 422' }),
195
+ );
196
+ await expect(failingReview()).rejects.toThrow(
197
+ 'Failed to approve PR: HTTP 422',
198
+ );
199
+ });
200
+
201
+ it('throws the raw body when the error body is not JSON', async () => {
202
+ mockFetchFailureOnce(500, 'Internal Server Error');
203
+ await expect(failingReview()).rejects.toThrow('Internal Server Error');
204
+ });
205
+
206
+ it('falls back to the status code when the error body is empty', async () => {
207
+ mockFetchFailureOnce(500, '');
208
+ await expect(failingReview()).rejects.toThrow('HTTP 500');
182
209
  });
183
210
  });
@@ -171,6 +171,24 @@ export const createConsoleApiClient = (
171
171
  parseState(await requestJson(appendToken, '/api/issuetitle', url)),
172
172
  });
173
173
 
174
+ const readOperationErrorReason = async (
175
+ response: Response,
176
+ ): Promise<string> => {
177
+ const raw = await response.text().catch(() => '');
178
+ if (raw.length === 0) {
179
+ return `HTTP ${response.status}`;
180
+ }
181
+ try {
182
+ const parsed: unknown = JSON.parse(raw);
183
+ if (isRecord(parsed) && typeof parsed.error === 'string') {
184
+ return parsed.error;
185
+ }
186
+ } catch {
187
+ return raw;
188
+ }
189
+ return raw;
190
+ };
191
+
174
192
  export const postConsoleOperation = async (
175
193
  appendToken: AppendToken,
176
194
  apiPath: string,
@@ -182,7 +200,7 @@ export const postConsoleOperation = async (
182
200
  body: JSON.stringify(body),
183
201
  });
184
202
  if (!response.ok) {
185
- throw new Error(`HTTP ${response.status}`);
203
+ throw new Error(await readOperationErrorReason(response));
186
204
  }
187
205
  };
188
206
 
@@ -1,4 +1,4 @@
1
- import { type Ref, useCallback } from 'react';
1
+ import { useCallback } from 'react';
2
2
  import { ConsoleCommentComposer } from '../components/detail/ConsoleCommentComposer';
3
3
  import { ConsoleItemDetail } from '../components/detail/ConsoleItemDetail';
4
4
  import { ConsoleOperationMenu } from '../components/operations/ConsoleOperationMenu';
@@ -22,7 +22,7 @@ import type {
22
22
  export type ConsoleQueueActionInput = {
23
23
  kind: ConsoleActionKind;
24
24
  item: ConsoleListItem;
25
- commit: () => void;
25
+ commit: () => Promise<void>;
26
26
  };
27
27
 
28
28
  export type ConsoleItemDetailContainerProps = {
@@ -37,7 +37,6 @@ export type ConsoleItemDetailContainerProps = {
37
37
  overlayStatus: ConsoleOverlayStatus | null;
38
38
  now: number;
39
39
  onQueueAction: (input: ConsoleQueueActionInput) => void;
40
- scrollRef?: Ref<HTMLElement>;
41
40
  };
42
41
 
43
42
  export const ConsoleItemDetailContainer = ({
@@ -52,7 +51,6 @@ export const ConsoleItemDetailContainer = ({
52
51
  overlayStatus,
53
52
  now,
54
53
  onQueueAction,
55
- scrollRef,
56
54
  }: ConsoleItemDetailContainerProps) => {
57
55
  const detail = useConsoleItemDetailData(caches, item);
58
56
  const { token } = useConsoleToken();
@@ -70,54 +68,42 @@ export const ConsoleItemDetailContainer = ({
70
68
  onQueueAction({
71
69
  kind: { type: 'review', action },
72
70
  item,
73
- commit: () => {
74
- void operations.reviewPullRequest(item, prUrl, action);
75
- },
71
+ commit: () => operations.reviewPullRequest(item, prUrl, action),
76
72
  });
77
73
  },
78
74
  onSetNextActionDate: (action) => {
79
75
  onQueueAction({
80
76
  kind: { type: 'next_action_date', action },
81
77
  item,
82
- commit: () => {
83
- void operations.setNextActionDate(item, action);
84
- },
78
+ commit: () => operations.setNextActionDate(item, action),
85
79
  });
86
80
  },
87
81
  onSetStory: (option: ConsoleFieldOption) => {
88
82
  onQueueAction({
89
83
  kind: { type: 'set_story', optionName: option.name },
90
84
  item,
91
- commit: () => {
92
- void operations.setStory(item, option);
93
- },
85
+ commit: () => operations.setStory(item, option),
94
86
  });
95
87
  },
96
88
  onSetStatus: (option: ConsoleFieldOption) => {
97
89
  onQueueAction({
98
90
  kind: { type: 'set_status', optionName: option.name },
99
91
  item,
100
- commit: () => {
101
- void operations.setStatus(item, option);
102
- },
92
+ commit: () => operations.setStatus(item, option),
103
93
  });
104
94
  },
105
95
  onSetInTmuxByHuman: (option: ConsoleFieldOption) => {
106
96
  onQueueAction({
107
97
  kind: { type: 'set_in_tmux_by_human', optionName: option.name },
108
98
  item,
109
- commit: () => {
110
- void operations.setInTmuxByHuman(item, option);
111
- },
99
+ commit: () => operations.setInTmuxByHuman(item, option),
112
100
  });
113
101
  },
114
102
  onClose: (action) => {
115
103
  onQueueAction({
116
104
  kind: { type: 'close', action },
117
105
  item,
118
- commit: () => {
119
- void operations.closeIssue(item, action);
120
- },
106
+ commit: () => operations.closeIssue(item, action),
121
107
  });
122
108
  },
123
109
  };
@@ -150,7 +136,6 @@ export const ConsoleItemDetailContainer = ({
150
136
  commitsError={detail.commitsError}
151
137
  relatedPullRequests={detail.relatedPullRequests}
152
138
  now={now}
153
- scrollRef={scrollRef}
154
139
  buildImageProxyUrl={resolveImageProxyUrl}
155
140
  commentComposer={
156
141
  <ConsoleCommentComposer
@@ -391,103 +391,6 @@ describe('ConsolePage swipe navigation', () => {
391
391
  });
392
392
  });
393
393
 
394
- describe('ConsolePage detail scroll reset', () => {
395
- beforeEach(() => {
396
- localStorage.clear();
397
- window.history.replaceState({}, '', '/projects/umino/prs?k=token');
398
- const fetchMock = jest.fn(async (url: string) => {
399
- const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
400
- if (listMatch !== null) {
401
- return {
402
- ok: true,
403
- status: 200,
404
- json: async () =>
405
- listMatch[1] === 'prs'
406
- ? twoItemPrPayload()
407
- : { ...twoItemPrPayload(), items: [] },
408
- };
409
- }
410
- return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
411
- });
412
- global.fetch = fetchMock as unknown as typeof fetch;
413
- });
414
-
415
- it('resets the detail scroll container to the top when navigating to the next item', async () => {
416
- const { container, getByText, findByText } = render(<ConsolePage />);
417
- await waitFor(() => {
418
- expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
419
- });
420
- fireEvent.click(getByText('Add serveConsole subcommand'));
421
- expect(await findByText('Approve')).toBeInTheDocument();
422
-
423
- const scrollContainer = container.querySelector('.console-detail');
424
- expect(scrollContainer).not.toBeNull();
425
- (scrollContainer as HTMLElement).scrollTop = 240;
426
- expect((scrollContainer as HTMLElement).scrollTop).toBe(240);
427
-
428
- const detailScreen = container.querySelector('.console-detail-screen');
429
- swipeDetailScreen(
430
- detailScreen as HTMLElement,
431
- { clientX: 240, clientY: 100 },
432
- { clientX: 40, clientY: 110 },
433
- );
434
-
435
- await waitFor(() => {
436
- expect(window.location.hash).toBe('#item/PVTI_2');
437
- });
438
- await waitFor(() => {
439
- expect((scrollContainer as HTMLElement).scrollTop).toBe(0);
440
- });
441
- });
442
- });
443
-
444
- describe('ConsolePage next item prefetch', () => {
445
- beforeEach(() => {
446
- localStorage.clear();
447
- window.history.replaceState({}, '', '/projects/umino/prs?k=token');
448
- });
449
-
450
- it('warms the next pending item resources while the current item is open', async () => {
451
- const fetchMock = jest.fn(async (url: string) => {
452
- const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
453
- if (listMatch !== null) {
454
- return {
455
- ok: true,
456
- status: 200,
457
- json: async () =>
458
- listMatch[1] === 'prs'
459
- ? twoItemPrPayload()
460
- : { ...twoItemPrPayload(), items: [] },
461
- };
462
- }
463
- return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
464
- });
465
- global.fetch = fetchMock as unknown as typeof fetch;
466
-
467
- const { getByText, findByText, queryByText } = render(<ConsolePage />);
468
- await waitFor(() => {
469
- expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
470
- });
471
- fireEvent.click(getByText('Add serveConsole subcommand'));
472
- expect(await findByText('Approve')).toBeInTheDocument();
473
- expect(window.location.hash).toBe('#item/PVTI_1');
474
- expect(queryByText('Add server-side console API handlers')).toBeNull();
475
-
476
- const nextItemUrlFragment = encodeURIComponent(
477
- 'https://github.com/o/r/pull/852',
478
- );
479
- await waitFor(() => {
480
- const calledNextItemBody = fetchMock.mock.calls.some(
481
- ([url]) =>
482
- typeof url === 'string' &&
483
- url.includes('/api/itembody') &&
484
- url.includes(nextItemUrlFragment),
485
- );
486
- expect(calledNextItemBody).toBe(true);
487
- });
488
- });
489
- });
490
-
491
394
  describe('ConsolePage auto-advance', () => {
492
395
  beforeEach(() => {
493
396
  localStorage.clear();
@@ -1,7 +1,10 @@
1
1
  import { useCallback, useEffect, useMemo, useRef } from 'react';
2
2
  import { ConsoleTabList } from '../components/layout/ConsoleTabList';
3
3
  import { ConsoleItemList } from '../components/list/ConsoleItemList';
4
- import { ConsoleUndoToast } from '../components/operations/ConsoleUndoToast';
4
+ import {
5
+ ConsoleErrorToast,
6
+ ConsoleUndoToast,
7
+ } from '../components/operations/ConsoleUndoToast';
5
8
  import { useConsoleActionQueue } from '../hooks/useConsoleActionQueue';
6
9
  import { useConsoleCaches } from '../hooks/useConsoleCaches';
7
10
  import { useConsoleNavigation } from '../hooks/useConsoleNavigation';
@@ -191,44 +194,6 @@ export const ConsolePage = () => {
191
194
  );
192
195
 
193
196
  const detailScreenRef = useConsoleSwipeNavigation(handleSwipe);
194
- const detailScrollElementRef = useRef<HTMLElement | null>(null);
195
-
196
- useEffect(() => {
197
- if (selectedItemKey === null) {
198
- return;
199
- }
200
- const element = detailScrollElementRef.current;
201
- if (element !== null) {
202
- element.scrollTop = 0;
203
- }
204
- }, [selectedItemKey]);
205
-
206
- const nextPendingItem = useMemo<ConsoleListItem | null>(() => {
207
- if (selectedItemKey === null) {
208
- return null;
209
- }
210
- const nextKey = nextPendingKeyBrowse(orderedPendingKeys, selectedItemKey);
211
- if (nextKey === null) {
212
- return null;
213
- }
214
- return (
215
- pendingItems.find((item) => overlayKeyForItem(item) === nextKey) ?? null
216
- );
217
- }, [selectedItemKey, orderedPendingKeys, pendingItems]);
218
-
219
- useEffect(() => {
220
- if (nextPendingItem === null) {
221
- return;
222
- }
223
- const key = `${nextPendingItem.repo}#${nextPendingItem.number}`;
224
- const url = nextPendingItem.url;
225
- void caches.body.load(key, url);
226
- void caches.state.load(key, url);
227
- if (nextPendingItem.isPr) {
228
- void caches.files.load(key, url);
229
- void caches.commits.load(key, url);
230
- }
231
- }, [nextPendingItem, caches]);
232
197
 
233
198
  return (
234
199
  <main className="console-app">
@@ -241,6 +206,12 @@ export const ConsolePage = () => {
241
206
  onUndo={actionQueue.undo}
242
207
  />
243
208
  )}
209
+ {actionQueue.error !== null && (
210
+ <ConsoleErrorToast
211
+ message={`操作に失敗しました: ${actionQueue.error.reason}`}
212
+ onDismiss={actionQueue.dismissError}
213
+ />
214
+ )}
244
215
  <ConsoleTabList
245
216
  activeTab={activeTab}
246
217
  counts={counts}
@@ -273,7 +244,6 @@ export const ConsolePage = () => {
273
244
  overlayStatus={overlayStatusForSelected}
274
245
  now={now}
275
246
  onQueueAction={handleQueueAction}
276
- scrollRef={detailScrollElementRef}
277
247
  />
278
248
  </div>
279
249
  )}
@@ -33,10 +33,8 @@ body {
33
33
  .console-app {
34
34
  max-width: 920px;
35
35
  margin: 0 auto;
36
- height: 100dvh;
37
36
  display: flex;
38
37
  flex-direction: column;
39
- min-height: 0;
40
38
  }
41
39
 
42
40
  .console-tabbar {
@@ -116,10 +114,6 @@ body {
116
114
  list-style: none;
117
115
  margin: 0;
118
116
  padding: 12px 18px 18px;
119
- flex: 1;
120
- min-height: 0;
121
- overflow-y: auto;
122
- -webkit-overflow-scrolling: touch;
123
117
  }
124
118
 
125
119
  .console-list-group {
@@ -264,10 +258,6 @@ body {
264
258
  }
265
259
 
266
260
  .console-detail {
267
- flex: 1;
268
- min-height: 0;
269
- overflow-y: auto;
270
- -webkit-overflow-scrolling: touch;
271
261
  display: flex;
272
262
  flex-direction: column;
273
263
  gap: 12px;
@@ -473,11 +463,14 @@ body {
473
463
  }
474
464
 
475
465
  .console-actionbar {
476
- flex-shrink: 0;
466
+ position: fixed;
467
+ bottom: 0;
468
+ left: 0;
469
+ right: 0;
470
+ z-index: 100;
477
471
  background: #161b22;
478
472
  border-top: 2px solid #30363d;
479
473
  padding: 10px 16px;
480
- padding-bottom: calc(10px + env(safe-area-inset-bottom));
481
474
  }
482
475
 
483
476
  .console-operation-bar {
@@ -568,10 +561,7 @@ body {
568
561
  }
569
562
 
570
563
  .console-detail-screen {
571
- flex: 1;
572
- min-height: 0;
573
- display: flex;
574
- flex-direction: column;
564
+ padding-bottom: 140px;
575
565
  }
576
566
 
577
567
  .console-panel-open-link {
@@ -0,0 +1 @@
1
+ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-outline-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-input:#e5e5e5;--color-ring:#0a0a0a;--color-background:#fff;--color-foreground:#0a0a0a;--color-primary:#171717;--color-primary-foreground:#fafafa;--color-secondary:#f5f5f5;--color-secondary-foreground:#171717;--color-accent:#f5f5f5;--color-accent-foreground:#171717}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.relative{position:relative}.static{position:static}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.hidden{display:none}.inline-flex{display:inline-flex}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-input{border-color:var(--color-input)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--color-background)}.bg-primary{background-color:var(--color-primary)}.bg-secondary{background-color:var(--color-secondary)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-2{padding-block:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-foreground{color:var(--color-foreground)}.text-primary-foreground{color:var(--color-primary-foreground)}.text-secondary-foreground{color:var(--color-secondary-foreground)}.lowercase{text-transform:lowercase}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.hover\:bg-accent:hover{background-color:var(--color-accent)}.hover\:bg-primary\/90:hover{background-color:#171717e6}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--color-primary) 90%,transparent)}}.hover\:bg-secondary\/80:hover{background-color:#f5f5f5cc}@supports (color:color-mix(in lab,red,red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--color-secondary) 80%,transparent)}}.hover\:text-accent-foreground:hover{color:var(--color-accent-foreground)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--color-ring)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}}body{color:#e6edf3;background-color:#0d1117;margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif}.console-app{flex-direction:column;max-width:920px;margin:0 auto;display:flex}.console-tabbar{-webkit-overflow-scrolling:touch;background:#161b22;border-bottom:2px solid #30363d;flex-wrap:nowrap;align-items:stretch;gap:0;min-height:42px;padding:0 8px;display:flex;overflow-x:auto}.console-tab{color:#8b949e;white-space:nowrap;cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;align-items:center;gap:6px;margin-bottom:-2px;padding:10px 14px;font-size:13px;font-weight:500;line-height:1.2;text-decoration:none;display:inline-flex}.console-tab:hover{color:#e6edf3}.console-tab[data-active=true]{color:#e6edf3;border-bottom-color:#2f81f7;font-weight:700}.console-tab-badge{text-align:center;color:#e6edf3;background:#484f58;border-radius:20px;min-width:20px;padding:1px 7px;font-size:11px;font-weight:700;line-height:1.5}.console-tab-badge[data-zero=true]{color:#8b949e;background:#30363d}.console-tab-pjname{color:#8b949e;align-self:center;margin-left:auto;padding:0 8px;font-size:11.5px}.console-tab-geninfo{color:#8b949e;align-self:center;padding:0 4px;font-size:11px}.console-list{margin:0;padding:12px 18px 18px;list-style:none}.console-list-group{list-style:none}.console-item-row .console-item-icon{flex:none;margin-top:3px}.console-group-header{background:#0b0f14;border-bottom:1px solid #21262d;justify-content:space-between;align-items:center;padding:6px 12px;display:flex}.console-storytag{align-items:center;gap:8px;font-size:13px;font-weight:700;display:inline-flex}.console-story-dot{border-radius:999px;width:10px;height:10px;display:inline-block}.console-group-count{color:#8b949e;font-size:12px}.console-item-row{color:#e6edf3;text-align:left;cursor:pointer;background:#161b22;border:1px solid #30363d;border-radius:8px;align-items:flex-start;gap:14px;width:100%;margin-bottom:10px;padding:12px 16px;display:flex}.console-item-row:hover{background:#1a2029;border-color:#484f58}.console-item-row[data-active=true]{background:#1a2029;border-color:#4493f8}.console-item-meta{flex:1;min-width:0}.console-item-title{font-size:14.5px;font-weight:600;display:block}.console-item-sub{color:#8b949e;margin-top:3px;font-size:12.5px;display:block}.console-item-pill{color:#8b949e;border:1px solid #30363d;border-radius:20px;margin-right:6px;padding:1px 8px;font-size:11px;display:inline-block}.console-item-createdat{color:#8b949e;cursor:help}.console-item-fields{color:#adbac7;flex-wrap:wrap;gap:4px 10px;margin-top:4px;font-size:11.5px;display:flex}.console-item-field{word-break:break-all;align-items:baseline;gap:4px;min-width:0;display:inline-flex}.console-item-field-label{color:#6e7681;text-transform:uppercase;letter-spacing:.03em;font-size:10px}.console-list-message{color:#8b949e;padding:16px;font-size:14px}.console-list-empty{text-align:center;color:#8b949e;padding:40px;font-size:14px}.console-list-error,.console-comment-error,.console-files-error,.console-commits-error,.console-detail-body-error{color:#f85149}.console-detail{flex-direction:column;gap:12px;padding:16px;display:flex}.console-detail-title{align-items:center;gap:8px;margin:0;font-size:20px;display:flex}.console-detail-title-text{flex:1}.console-detail-number{color:#8b949e;font-weight:400}.console-detail-closed-label{color:#a371f7;font-size:13px}.console-detail-subbar{align-items:center;gap:12px;font-size:13px;display:flex}.console-detail-link{color:#4493f8}.console-detail-repo{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-detail-pill,.console-label-chip,.console-detail-status-chip{border:1px solid #30363d;border-radius:999px;padding:2px 8px;font-size:12px;display:inline-block}.console-detail-labels{flex-wrap:wrap;gap:6px;display:flex}.console-detail-createdat{color:#8b949e;font-size:12px}.console-panel{border:1px solid #30363d;border-radius:8px;overflow:hidden}.console-panel-header{background:#161b22;justify-content:space-between;align-items:center;padding:6px 12px;display:flex}.console-panel-toggle{color:#e6edf3;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;font-size:14px;font-weight:600;display:inline-flex}.console-panel-body{padding:12px}.console-markdown{word-break:break-word;font-size:14px;line-height:1.5}.console-markdown ul{margin:.5em 0;padding-left:1.5em;list-style:outside}.console-markdown ol{margin:.5em 0;padding-left:1.5em;list-style:decimal}.console-markdown li{margin:.25em 0}.console-markdown table{border-collapse:collapse;margin:.5em 0}.console-markdown th,.console-markdown td{border:1px solid #30363d;padding:4px 8px}.console-markdown th{background:#161b22}.console-mermaid-error{color:#f85149;font-size:13px}.console-comment{border-top:1px solid #21262d;padding-top:8px}.console-comment-header{color:#8b949e;gap:8px;font-size:12px;display:flex}.console-comment-author{color:#e6edf3;font-weight:600}.console-files,.console-commits{margin:0;padding:0;list-style:none}.console-commit{align-items:center;gap:8px;padding:4px 0;font-size:13px;display:flex}.console-file{flex-direction:column;font-size:13px;display:flex}.console-file-badge{text-align:center;border:1px solid;border-radius:4px;width:18px;font-size:11px}.console-file-path,.console-commit-message{flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-file-add,.console-pr-add{color:#3fb950}.console-file-del,.console-pr-del{color:#f85149}.console-commit-sha{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-actionbar{z-index:100;background:#161b22;border-top:2px solid #30363d;padding:10px 16px;position:fixed;bottom:0;left:0;right:0}.console-operation-bar{flex-direction:column;gap:8px;max-width:920px;margin:0 auto;display:flex}.console-op-group{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.console-op-group-review{gap:10px}.console-op-group-stories{max-height:50vh;overflow-y:auto}.console-op-button{color:#e6edf3;white-space:nowrap;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:7px 16px;font-size:13px;font-weight:600}.console-op-button:hover{border-color:#484f58}.console-op-button-approve{color:#fff;background:#238636;border-color:#2ea043}.console-op-button-reject{color:#ffd166;background:#7d5000;border-color:#a06800}.console-op-button-wrong{color:#f85149;background:#3a1518;border-color:#f85149}.console-op-button-unneeded{color:#aab0b8;background:#2a2d31;border-color:#6e7681}.console-op-button-snooze{color:#79c0ff;background:#1c2b4a;border-color:#4493f8}.console-pr-section{border:1px solid #30363d;border-radius:8px;flex-direction:column;gap:10px;padding:12px;display:flex}.console-pr-statbar{color:#8b949e;gap:12px;font-size:12px;display:flex}.console-detail-screen{padding-bottom:140px}.console-panel-open-link{color:#4493f8;font-size:13px;font-weight:400}.console-composer{margin-top:4px}.console-composer-toggle{color:#8b949e;cursor:pointer;background:0 0;border:none;padding:2px 0;font-size:12.5px}.console-composer-toggle:hover{color:#e6edf3}.console-composer-posted{flex-direction:column;gap:8px;margin-top:8px;display:flex}.console-composer-form{margin-top:8px}.console-composer-input{box-sizing:border-box;color:#e6edf3;width:100%;font:inherit;resize:vertical;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:8px;font-size:14px}.console-composer-row{align-items:center;gap:8px;margin-top:6px;display:flex}.console-composer-submit{color:#fff;cursor:pointer;background:#238636;border:1px solid #2ea043;border-radius:6px;padding:7px 16px;font-size:13px;font-weight:600}.console-composer-submit:disabled{opacity:.6;cursor:default}.console-composer-status{color:#8b949e;font-size:12px}.console-composer-error{color:#f85149}.console-undo-toast{z-index:9998;white-space:nowrap;border-radius:10px;align-items:center;gap:12px;max-width:92vw;padding:12px 18px;font-size:13.5px;display:flex;position:fixed;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 20px #0009}.console-undo-toast-green{color:#3fb950;background:#0d3320;border:1px solid #2ea043}.console-undo-toast-amber{color:#ffd166;background:#3a2800;border:1px solid #a06800}.console-undo-toast-red{color:#f85149;background:#3a1518;border:1px solid #da3633}.console-undo-toast-gray{color:#aab0b8;background:#2a2d31;border:1px solid #6e7681}.console-undo-toast-blue{color:#79c0ff;background:#0d1f40;border:1px solid #4493f8}.console-undo-toast-error{color:#f85149;background:#3a1518;border:1px solid #f85149}.console-undo-toast-message{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.console-undo-toast-undo{cursor:pointer;color:inherit;background:#ffffff1f;border:none;border-radius:6px;flex:none;padding:4px 12px;font-size:13px;font-weight:700}.console-undo-toast-undo:hover{background:#ffffff38}.console-undo-toast-countdown{opacity:.7;flex:none;font-size:11px}.console-undo-toast-bar{opacity:.5;background:currentColor;border-radius:0 0 10px 10px;height:3px;transition:width .1s linear;position:absolute;bottom:0;left:0}.console-file-row{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;padding:4px 0;font-size:13px;display:flex}.console-file-row:hover{background:#1a2029}.console-file-caret{color:#8b949e;flex:none;width:1em}.console-file-diff{border-collapse:collapse;width:100%;margin:4px 0 8px;font:13px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace}.console-file-diff td{white-space:pre-wrap;word-break:break-word;vertical-align:top;padding:0 10px}.console-diff-ln{text-align:right;color:#6e7681;-webkit-user-select:none;user-select:none;white-space:nowrap;border-right:1px solid #30363d;width:1%;padding:0 8px}.console-file-diff td.console-diff-ln{white-space:nowrap;word-break:normal;overflow-wrap:normal}.console-diff-add td{background:#2ea04326}.console-diff-add .console-diff-code{color:#aff5b4}.console-diff-del td{background:#f8514926}.console-diff-del .console-diff-code{color:#ffdcd7}.console-diff-hunk td{color:#79c0ff;background:#161b22}.console-diff-ctx .console-diff-code{color:#c9d1d9}.console-file-diff-empty{color:#8b949e;padding:8px 14px;font-size:12px}.console-pr-header{flex-wrap:wrap;align-items:center;gap:10px;padding-top:4px;display:flex}.console-pr-section-title{color:#e6edf3;font-size:15px;font-weight:600}.console-pr-section-state{color:#8b949e;border:1px solid #6e7681;border-radius:999px;padding:1px 8px;font-size:11px}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}