github-issue-tower-defence-management 1.135.2 → 1.137.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 (41) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +7 -0
  3. package/bin/adapter/entry-points/cli/index.js +24 -0
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/ui-dist/assets/{index-N9M1qhkw.css → index-BTtIpfv_.css} +1 -1
  6. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  7. package/bin/adapter/repositories/NodeTmuxSessionRepository.js +51 -6
  8. package/bin/adapter/repositories/NodeTmuxSessionRepository.js.map +1 -1
  9. package/bin/adapter/repositories/clSessionScopeUnitNameFromCgroupContent.js +10 -0
  10. package/bin/adapter/repositories/clSessionScopeUnitNameFromCgroupContent.js.map +1 -0
  11. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js +1 -1
  12. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js.map +1 -1
  13. package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +4 -2
  14. package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/adapter/entry-points/cli/index.test.ts +118 -1
  17. package/src/adapter/entry-points/cli/index.ts +38 -0
  18. package/src/adapter/entry-points/console/ui/src/index.css +22 -2
  19. package/src/adapter/entry-points/console/ui-dist/assets/{index-N9M1qhkw.css → index-BTtIpfv_.css} +1 -1
  20. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  21. package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +2 -2
  22. package/src/adapter/repositories/NodeTmuxSessionRepository.test.ts +78 -18
  23. package/src/adapter/repositories/NodeTmuxSessionRepository.ts +27 -6
  24. package/src/adapter/repositories/clSessionScopeUnitNameFromCgroupContent.test.ts +34 -0
  25. package/src/adapter/repositories/clSessionScopeUnitNameFromCgroupContent.ts +8 -0
  26. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.test.ts +25 -15
  27. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.ts +1 -1
  28. package/src/domain/usecases/adapter-interfaces/TmuxSessionRepository.ts +1 -0
  29. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +54 -0
  30. package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +8 -2
  31. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
  32. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  33. package/types/adapter/repositories/NodeTmuxSessionRepository.d.ts +4 -2
  34. package/types/adapter/repositories/NodeTmuxSessionRepository.d.ts.map +1 -1
  35. package/types/adapter/repositories/clSessionScopeUnitNameFromCgroupContent.d.ts +2 -0
  36. package/types/adapter/repositories/clSessionScopeUnitNameFromCgroupContent.d.ts.map +1 -0
  37. package/types/domain/usecases/adapter-interfaces/TmuxSessionRepository.d.ts +1 -0
  38. package/types/domain/usecases/adapter-interfaces/TmuxSessionRepository.d.ts.map +1 -1
  39. package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
  40. /package/bin/adapter/entry-points/console/ui-dist/assets/{index-Bz0KvgIG.js → index-CMK3yyfo.js} +0 -0
  41. /package/src/adapter/entry-points/console/ui-dist/assets/{index-Bz0KvgIG.js → index-CMK3yyfo.js} +0 -0
@@ -1,6 +1,14 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import YAML from 'yaml';
4
+
5
+ jest.mock('fs', () => {
6
+ const actualFs: typeof fs = jest.requireActual('fs');
7
+ return {
8
+ ...actualFs,
9
+ readFileSync: jest.fn(actualFs.readFileSync),
10
+ };
11
+ });
4
12
  import {
5
13
  program,
6
14
  loadConfigFile,
@@ -53,8 +61,14 @@ jest.mock('../../repositories/issue/ApiV3CheerioRestIssueRepository', () => ({
53
61
  getCachedProject: jest.fn().mockResolvedValue(null),
54
62
  })),
55
63
  }));
64
+ const mockRunCommand = jest.fn<
65
+ Promise<{ stdout: string; stderr: string; exitCode: number }>,
66
+ [string, string[]]
67
+ >();
56
68
  jest.mock('../../repositories/NodeLocalCommandRunner', () => ({
57
- NodeLocalCommandRunner: jest.fn().mockImplementation(() => ({})),
69
+ NodeLocalCommandRunner: jest.fn().mockImplementation(() => ({
70
+ runCommand: mockRunCommand,
71
+ })),
58
72
  }));
59
73
  jest.mock('../../repositories/OauthAPIClaudeRepository', () => ({
60
74
  OauthAPIClaudeRepository: jest.fn().mockImplementation(() => ({
@@ -151,6 +165,7 @@ describe('CLI', () => {
151
165
  beforeEach(() => {
152
166
  jest.clearAllMocks();
153
167
  mockFetchReturningReadme(null);
168
+ mockRunCommand.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 });
154
169
  process.env = { ...originalEnv, GH_TOKEN: 'test-token' };
155
170
  writeConfig(defaultConfig);
156
171
  });
@@ -2142,4 +2157,106 @@ mysteryKey: 'value'
2142
2157
  logSpy.mockRestore();
2143
2158
  });
2144
2159
  });
2160
+
2161
+ describe('killTmuxSession', () => {
2162
+ it('reaches the real repository command sequence to kill a named session via --session', async () => {
2163
+ await program.parseAsync([
2164
+ 'node',
2165
+ 'test',
2166
+ 'killTmuxSession',
2167
+ '--session',
2168
+ 'https_//github_com/owner/repo/issues/9',
2169
+ ]);
2170
+
2171
+ const expectedScopeUnitName =
2172
+ 'cl-https---github-com-owner-repo-issues-9.scope';
2173
+ expect(mockRunCommand.mock.calls).toEqual([
2174
+ ['systemctl', ['--user', 'reset-failed', expectedScopeUnitName]],
2175
+ ['systemctl', ['--user', 'stop', expectedScopeUnitName]],
2176
+ ['systemctl', ['--user', 'reset-failed', expectedScopeUnitName]],
2177
+ [
2178
+ 'tmux',
2179
+ ['kill-session', '-t', '=https_//github_com/owner/repo/issues/9'],
2180
+ ],
2181
+ ]);
2182
+ });
2183
+
2184
+ it('reaches the real repository command sequence to stop its own scope via --self, without calling tmux', async () => {
2185
+ const actualFs = jest.requireActual<typeof fs>('fs');
2186
+ const readFileSyncMock = jest.mocked(fs.readFileSync);
2187
+ readFileSyncMock.mockImplementation(
2188
+ (
2189
+ filePath: Parameters<typeof fs.readFileSync>[0],
2190
+ options: Parameters<typeof fs.readFileSync>[1],
2191
+ ): ReturnType<typeof fs.readFileSync> => {
2192
+ if (filePath === '/proc/self/cgroup') {
2193
+ return '0::/user.slice/user-1000.slice/user@1000.service/app.slice/cl-current-session.scope\n';
2194
+ }
2195
+ return actualFs.readFileSync(filePath, options);
2196
+ },
2197
+ );
2198
+
2199
+ await program.parseAsync(['node', 'test', 'killTmuxSession', '--self']);
2200
+
2201
+ expect(mockRunCommand.mock.calls).toEqual([
2202
+ ['systemctl', ['--user', 'reset-failed', 'cl-current-session.scope']],
2203
+ ['systemctl', ['--user', 'stop', 'cl-current-session.scope']],
2204
+ ['systemctl', ['--user', 'reset-failed', 'cl-current-session.scope']],
2205
+ ]);
2206
+ expect(mockRunCommand.mock.calls.some((call) => call[0] === 'tmux')).toBe(
2207
+ false,
2208
+ );
2209
+
2210
+ readFileSyncMock.mockImplementation(actualFs.readFileSync);
2211
+ });
2212
+
2213
+ it('exits with an error when neither --session nor --self is provided', async () => {
2214
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
2215
+ const processExitSpy = jest
2216
+ .spyOn(process, 'exit')
2217
+ .mockImplementation(() => {
2218
+ throw new Error('process.exit called');
2219
+ });
2220
+
2221
+ await expect(
2222
+ program.parseAsync(['node', 'test', 'killTmuxSession']),
2223
+ ).rejects.toThrow('process.exit called');
2224
+
2225
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
2226
+ 'Either --session <name> or --self is required',
2227
+ );
2228
+ expect(processExitSpy).toHaveBeenCalledWith(1);
2229
+
2230
+ consoleErrorSpy.mockRestore();
2231
+ processExitSpy.mockRestore();
2232
+ });
2233
+
2234
+ it('exits with an error when both --session and --self are provided', async () => {
2235
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
2236
+ const processExitSpy = jest
2237
+ .spyOn(process, 'exit')
2238
+ .mockImplementation(() => {
2239
+ throw new Error('process.exit called');
2240
+ });
2241
+
2242
+ await expect(
2243
+ program.parseAsync([
2244
+ 'node',
2245
+ 'test',
2246
+ 'killTmuxSession',
2247
+ '--session',
2248
+ 'some_session',
2249
+ '--self',
2250
+ ]),
2251
+ ).rejects.toThrow('process.exit called');
2252
+
2253
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
2254
+ '--session and --self cannot be used together',
2255
+ );
2256
+ expect(processExitSpy).toHaveBeenCalledWith(1);
2257
+
2258
+ consoleErrorSpy.mockRestore();
2259
+ processExitSpy.mockRestore();
2260
+ });
2261
+ });
2145
2262
  });
@@ -29,6 +29,7 @@ import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCach
29
29
  import { SystemDateRepository } from '../../repositories/SystemDateRepository';
30
30
  import { BaseGitHubRepository } from '../../repositories/BaseGitHubRepository';
31
31
  import { NodeLocalCommandRunner } from '../../repositories/NodeLocalCommandRunner';
32
+ import { NodeTmuxSessionRepository } from '../../repositories/NodeTmuxSessionRepository';
32
33
  import { GitHubIssueCommentRepository } from '../../repositories/GitHubIssueCommentRepository';
33
34
  import { FetchWebhookRepository } from '../../repositories/FetchWebhookRepository';
34
35
  import { RevertOrphanedPreparationUseCase } from '../../../domain/usecases/RevertOrphanedPreparationUseCase';
@@ -124,6 +125,11 @@ type CountInTmuxByHumanSessionsPerTokenOptions = {
124
125
  tokenListJsonPath?: string;
125
126
  };
126
127
 
128
+ type KillTmuxSessionOptions = {
129
+ session?: string;
130
+ self?: boolean;
131
+ };
132
+
127
133
  const buildGithubRepositoryParams = (
128
134
  localStorageRepository: LocalStorageRepository,
129
135
  token: string,
@@ -1028,6 +1034,38 @@ program
1028
1034
  }
1029
1035
  });
1030
1036
 
1037
+ program
1038
+ .command('killTmuxSession')
1039
+ .description(
1040
+ 'Cleanly kill a tmux session by running tmux kill-session and stopping its cl-*.scope systemd --user unit. Use --session <name> to kill another named session, or --self to terminate the current session from inside it.',
1041
+ )
1042
+ .option('--session <name>', 'Name of the tmux session to kill')
1043
+ .option(
1044
+ '--self',
1045
+ 'Terminate the current session by stopping its own cl-*.scope systemd user unit, derived from /proc/self/cgroup',
1046
+ )
1047
+ .action(async (options: KillTmuxSessionOptions) => {
1048
+ if (!options.session && !options.self) {
1049
+ console.error('Either --session <name> or --self is required');
1050
+ process.exit(1);
1051
+ }
1052
+ if (options.session && options.self) {
1053
+ console.error('--session and --self cannot be used together');
1054
+ process.exit(1);
1055
+ }
1056
+
1057
+ const localCommandRunner = new NodeLocalCommandRunner();
1058
+ const tmuxSessionRepository = new NodeTmuxSessionRepository(
1059
+ localCommandRunner,
1060
+ );
1061
+
1062
+ if (options.self) {
1063
+ await tmuxSessionRepository.killOwnSession();
1064
+ } else if (options.session) {
1065
+ await tmuxSessionRepository.killSession(options.session);
1066
+ }
1067
+ });
1068
+
1031
1069
  /* istanbul ignore next */
1032
1070
  if (process.argv && require.main === module) {
1033
1071
  program.parse(process.argv);
@@ -562,8 +562,11 @@ body {
562
562
  }
563
563
 
564
564
  .console-comment {
565
- border-top: 1px solid #21262d;
566
- padding-top: 8px;
565
+ padding: 12px 16px;
566
+ margin-bottom: 10px;
567
+ background: #161b22;
568
+ border: 1px solid #30363d;
569
+ border-radius: 8px;
567
570
  }
568
571
 
569
572
  .console-comment-header {
@@ -573,6 +576,23 @@ body {
573
576
  color: #8b949e;
574
577
  }
575
578
 
579
+ .console-comment-show-all {
580
+ padding: 6px 12px;
581
+ margin-bottom: 10px;
582
+ border: 1px solid #30363d;
583
+ border-radius: 6px;
584
+ background: #21262d;
585
+ color: #e6edf3;
586
+ font-size: 0.75rem;
587
+ font-weight: 600;
588
+ cursor: pointer;
589
+ }
590
+
591
+ .console-comment-show-all:hover {
592
+ border-color: #484f58;
593
+ background-color: #262c34;
594
+ }
595
+
576
596
  .console-comment-author {
577
597
  font-weight: 600;
578
598
  color: #e6edf3;
@@ -1 +1 @@
1
- /*! tailwindcss v4.3.1 | 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}}.block{display:block}.hidden{display:none}.inline{display:inline}.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}}html{font-size:clamp(15px,2.1vw,26px)}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;width:100%;min-height:100dvh;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:.8125rem;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:.6875rem;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:.7188rem}.console-tab-geninfo{color:#8b949e;align-self:center;padding:0 4px;font-size:.6875rem}.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:.8125rem;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:.75rem}.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:.9063rem;font-weight:600;display:block}.console-item-sub{color:#8b949e;margin-top:3px;font-size:.7813rem;display:block}.console-item-pill{color:#8b949e;border:1px solid #30363d;border-radius:20px;margin-right:6px;padding:1px 8px;font-size:.6875rem;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:.7188rem;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:.625rem}.console-list-message{color:#8b949e;padding:16px;font-size:.875rem}.console-list-empty{text-align:center;color:#8b949e;padding:40px;font-size:.875rem}.console-list-error,.console-comment-error,.console-files-error,.console-commits-error,.console-detail-body-error{color:#f85149}.console-detail{flex-direction:column;flex:1 0 auto;gap:12px;padding:16px 16px 0;display:flex}.console-detail-title{align-items:center;gap:8px;margin:0;font-size:1.25rem;display:flex}.console-detail-title-text{flex:1}.console-detail-number{color:#8b949e;font-weight:400}.console-detail-closed-label{color:#a371f7;font-size:.8125rem}.console-detail-subbar{align-items:center;gap:12px;font-size:.8125rem;display:flex}.console-detail-link{color:#4493f8}.console-detail-repo{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-copy-url-button{color:#c9d1d9;border:1px solid #30363d;border-radius:6px;height:24px;padding:0 8px;font-size:.75rem}.console-copy-url-button:hover{color:#e6edf3;background-color:#21262d}.console-detail-pill,.console-label-chip,.console-detail-status-chip{border:1px solid #30363d;border-radius:999px;padding:2px 8px;font-size:.75rem;display:inline-block}.console-detail-pr-status-row{flex-wrap:wrap;align-items:center;gap:6px;margin-top:-4px;display:flex}.console-detail-pr-status{flex-wrap:wrap;align-items:center;gap:6px;display:inline-flex}.console-detail-ci-missing{opacity:.85;font-weight:400}.console-detail-mergeable-chip{font-weight:600}.console-detail-mergeable-chip-unknown{opacity:.7;font-weight:400}.console-detail-labels{flex-wrap:wrap;gap:6px;display:flex}.console-detail-createdat{color:#8b949e;font-size:.75rem}.console-panel{border:1px solid #30363d;border-radius:8px;overflow:hidden}.console-panel-header{background:#161b22;justify-content:space-between;align-items:center;display:flex}.console-panel-toggle{color:#e6edf3;text-align:left;cursor:pointer;background:0 0;border:none;flex:1;align-items:center;gap:8px;padding:6px 12px;font-size:.875rem;font-weight:600;display:flex}.console-panel-toggle:hover{background:#1a2029}.console-panel-action{padding:6px 12px}.console-panel-body{padding:12px}.console-markdown{word-break:break-word;font-size:.875rem;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-markdown h1,.console-markdown h2,.console-markdown h3,.console-markdown h4,.console-markdown h5,.console-markdown h6{margin:1em 0 .5em;font-weight:700;line-height:1.25}.console-markdown h1{font-size:1.6em}.console-markdown h2{font-size:1.4em}.console-markdown h3{font-size:1.2em}.console-markdown h4{font-size:1.05em}.console-markdown h5{font-size:.95em}.console-markdown h6{color:#8b949e;font-size:.9em}.console-markdown pre{background:#161b22;border:1px solid #30363d;border-radius:6px;margin:.5em 0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.8125rem;line-height:1.45;overflow-x:auto}.console-markdown code{background:#6e768166;border-radius:4px;padding:.15em .4em;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}.console-markdown pre code{font-size:inherit;background:0 0;border-radius:0;padding:0}.console-markdown blockquote{color:#8b949e;border-left:3px solid #30363d;margin:.5em 0;padding:0 1em}.console-markdown a{color:#4493f8;text-decoration:none}.console-markdown a:hover{text-decoration:underline}.console-markdown-reference-host{display:inline}.console-markdown-reference{vertical-align:baseline;align-items:center;gap:.25em;display:inline-flex}.console-markdown-reference .console-item-icon{flex-shrink:0}.console-markdown-reference-title{text-decoration:none}.console-markdown-reference:hover .console-markdown-reference-title{text-decoration:underline}.console-markdown hr{border:none;border-top:1px solid #30363d;margin:1em 0}.console-mermaid-error{color:#f85149;font-size:.8125rem}.console-comment{border-top:1px solid #21262d;padding-top:8px}.console-comment-header{color:#8b949e;gap:8px;font-size:.75rem;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:.8125rem;display:flex}.console-file{flex-direction:column;font-size:.8125rem;display:flex}.console-file-tree,.console-file-tree-children{margin:0;padding:0;list-style:none}.console-file-tree-dir{flex-direction:column;display:flex}.console-file-tree-dir-name{color:#8b949e;align-items:center;gap:6px;padding:4px 0;font-weight:600;display:flex}.console-file-tree-dir-icon{flex:none}.console-file-badge{text-align:center;border:1px solid;border-radius:4px;width:18px;font-size:.6875rem}.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;padding:10px 16px;padding-bottom:calc(10px + env(safe-area-inset-bottom));background:#161b22;border-top:2px solid #30363d;margin-top:auto;position:sticky;bottom:0}.console-operation-bar{flex-direction:column;gap:8px;width:100%;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:33vh;overflow-y:auto}.console-op-button{color:#e6edf3;white-space:nowrap;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:9px 16px;font-size:.8125rem;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:.75rem;display:flex}.console-detail-screen{flex-direction:column;flex:1 0 auto;display:flex}.console-panel-open-link{color:#4493f8;font-size:.8125rem;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:.7813rem}.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:.875rem}.console-composer-row{justify-content:flex-end;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:.8125rem;font-weight:600}.console-composer-submit:disabled{opacity:.6;cursor:default}.console-composer-status{color:#8b949e;font-size:.75rem}.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:.8438rem;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:.8125rem;font-weight:700}.console-undo-toast-undo:hover{background:#ffffff38}.console-undo-toast-countdown{opacity:.7;flex:none;font-size:.6875rem}.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:.8125rem;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:.8125rem/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:.75rem}.console-diff-comment-cell{text-align:center;-webkit-user-select:none;user-select:none;width:1%;padding:0 2px}.console-diff-comment-button{color:#6e7681;cursor:pointer;opacity:.6;background:0 0;border:1px solid #30363d;border-radius:4px;width:18px;height:18px;padding:0;font-size:.875rem;line-height:16px}.console-diff-row:hover .console-diff-comment-button,.console-diff-comment-button:focus-visible,.console-diff-comment-button[aria-expanded=true]{opacity:1;color:#fff;background:#1f6feb;border-color:#2f81f7}.console-diff-composer-row td{background:#0d1117;padding:6px 10px}.console-diff-composer{background:#161b22;border:1px solid #30363d;border-radius:6px;flex-direction:column;gap:6px;padding:8px;display:flex}.console-diff-composer-anchor{color:#8b949e;font-size:.6875rem}.console-diff-composer-input{resize:vertical;color:#c9d1d9;width:100%;font:inherit;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:6px 8px}.console-diff-composer-controls{align-items:center;gap:8px;display:flex}.console-diff-composer-submit{color:#fff;cursor:pointer;background:#238636;border:1px solid #238636;border-radius:6px;padding:4px 12px}.console-diff-composer-submit:disabled{opacity:.6;cursor:default}.console-diff-composer-cancel{color:#c9d1d9;cursor:pointer;background:0 0;border:1px solid #30363d;border-radius:6px;padding:4px 12px}.console-diff-composer-status{color:#8b949e;font-size:.75rem}.console-diff-composer-error{color:#ff7b72}.console-diff-composer-posted{color:#3fb950;margin:0;font-size:.75rem}.console-pr-header{flex-wrap:wrap;align-items:center;gap:10px;padding-top:4px;display:flex}.console-pr-section-title{color:#e6edf3;font-size:.9375rem;font-weight:600}.console-pr-section-state{color:#8b949e;border:1px solid #6e7681;border-radius:999px;padding:1px 8px;font-size:.6875rem}@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}
1
+ /*! tailwindcss v4.3.1 | 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}}.block{display:block}.hidden{display:none}.inline{display:inline}.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}}html{font-size:clamp(15px,2.1vw,26px)}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;width:100%;min-height:100dvh;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:.8125rem;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:.6875rem;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:.7188rem}.console-tab-geninfo{color:#8b949e;align-self:center;padding:0 4px;font-size:.6875rem}.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:.8125rem;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:.75rem}.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:.9063rem;font-weight:600;display:block}.console-item-sub{color:#8b949e;margin-top:3px;font-size:.7813rem;display:block}.console-item-pill{color:#8b949e;border:1px solid #30363d;border-radius:20px;margin-right:6px;padding:1px 8px;font-size:.6875rem;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:.7188rem;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:.625rem}.console-list-message{color:#8b949e;padding:16px;font-size:.875rem}.console-list-empty{text-align:center;color:#8b949e;padding:40px;font-size:.875rem}.console-list-error,.console-comment-error,.console-files-error,.console-commits-error,.console-detail-body-error{color:#f85149}.console-detail{flex-direction:column;flex:1 0 auto;gap:12px;padding:16px 16px 0;display:flex}.console-detail-title{align-items:center;gap:8px;margin:0;font-size:1.25rem;display:flex}.console-detail-title-text{flex:1}.console-detail-number{color:#8b949e;font-weight:400}.console-detail-closed-label{color:#a371f7;font-size:.8125rem}.console-detail-subbar{align-items:center;gap:12px;font-size:.8125rem;display:flex}.console-detail-link{color:#4493f8}.console-detail-repo{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-copy-url-button{color:#c9d1d9;border:1px solid #30363d;border-radius:6px;height:24px;padding:0 8px;font-size:.75rem}.console-copy-url-button:hover{color:#e6edf3;background-color:#21262d}.console-detail-pill,.console-label-chip,.console-detail-status-chip{border:1px solid #30363d;border-radius:999px;padding:2px 8px;font-size:.75rem;display:inline-block}.console-detail-pr-status-row{flex-wrap:wrap;align-items:center;gap:6px;margin-top:-4px;display:flex}.console-detail-pr-status{flex-wrap:wrap;align-items:center;gap:6px;display:inline-flex}.console-detail-ci-missing{opacity:.85;font-weight:400}.console-detail-mergeable-chip{font-weight:600}.console-detail-mergeable-chip-unknown{opacity:.7;font-weight:400}.console-detail-labels{flex-wrap:wrap;gap:6px;display:flex}.console-detail-createdat{color:#8b949e;font-size:.75rem}.console-panel{border:1px solid #30363d;border-radius:8px;overflow:hidden}.console-panel-header{background:#161b22;justify-content:space-between;align-items:center;display:flex}.console-panel-toggle{color:#e6edf3;text-align:left;cursor:pointer;background:0 0;border:none;flex:1;align-items:center;gap:8px;padding:6px 12px;font-size:.875rem;font-weight:600;display:flex}.console-panel-toggle:hover{background:#1a2029}.console-panel-action{padding:6px 12px}.console-panel-body{padding:12px}.console-markdown{word-break:break-word;font-size:.875rem;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-markdown h1,.console-markdown h2,.console-markdown h3,.console-markdown h4,.console-markdown h5,.console-markdown h6{margin:1em 0 .5em;font-weight:700;line-height:1.25}.console-markdown h1{font-size:1.6em}.console-markdown h2{font-size:1.4em}.console-markdown h3{font-size:1.2em}.console-markdown h4{font-size:1.05em}.console-markdown h5{font-size:.95em}.console-markdown h6{color:#8b949e;font-size:.9em}.console-markdown pre{background:#161b22;border:1px solid #30363d;border-radius:6px;margin:.5em 0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.8125rem;line-height:1.45;overflow-x:auto}.console-markdown code{background:#6e768166;border-radius:4px;padding:.15em .4em;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}.console-markdown pre code{font-size:inherit;background:0 0;border-radius:0;padding:0}.console-markdown blockquote{color:#8b949e;border-left:3px solid #30363d;margin:.5em 0;padding:0 1em}.console-markdown a{color:#4493f8;text-decoration:none}.console-markdown a:hover{text-decoration:underline}.console-markdown-reference-host{display:inline}.console-markdown-reference{vertical-align:baseline;align-items:center;gap:.25em;display:inline-flex}.console-markdown-reference .console-item-icon{flex-shrink:0}.console-markdown-reference-title{text-decoration:none}.console-markdown-reference:hover .console-markdown-reference-title{text-decoration:underline}.console-markdown hr{border:none;border-top:1px solid #30363d;margin:1em 0}.console-mermaid-error{color:#f85149;font-size:.8125rem}.console-comment{background:#161b22;border:1px solid #30363d;border-radius:8px;margin-bottom:10px;padding:12px 16px}.console-comment-header{color:#8b949e;gap:8px;font-size:.75rem;display:flex}.console-comment-show-all{color:#e6edf3;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:6px;margin-bottom:10px;padding:6px 12px;font-size:.75rem;font-weight:600}.console-comment-show-all:hover{background-color:#262c34;border-color:#484f58}.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:.8125rem;display:flex}.console-file{flex-direction:column;font-size:.8125rem;display:flex}.console-file-tree,.console-file-tree-children{margin:0;padding:0;list-style:none}.console-file-tree-dir{flex-direction:column;display:flex}.console-file-tree-dir-name{color:#8b949e;align-items:center;gap:6px;padding:4px 0;font-weight:600;display:flex}.console-file-tree-dir-icon{flex:none}.console-file-badge{text-align:center;border:1px solid;border-radius:4px;width:18px;font-size:.6875rem}.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;padding:10px 16px;padding-bottom:calc(10px + env(safe-area-inset-bottom));background:#161b22;border-top:2px solid #30363d;margin-top:auto;position:sticky;bottom:0}.console-operation-bar{flex-direction:column;gap:8px;width:100%;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:33vh;overflow-y:auto}.console-op-button{color:#e6edf3;white-space:nowrap;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:9px 16px;font-size:.8125rem;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:.75rem;display:flex}.console-detail-screen{flex-direction:column;flex:1 0 auto;display:flex}.console-panel-open-link{color:#4493f8;font-size:.8125rem;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:.7813rem}.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:.875rem}.console-composer-row{justify-content:flex-end;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:.8125rem;font-weight:600}.console-composer-submit:disabled{opacity:.6;cursor:default}.console-composer-status{color:#8b949e;font-size:.75rem}.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:.8438rem;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:.8125rem;font-weight:700}.console-undo-toast-undo:hover{background:#ffffff38}.console-undo-toast-countdown{opacity:.7;flex:none;font-size:.6875rem}.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:.8125rem;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:.8125rem/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:.75rem}.console-diff-comment-cell{text-align:center;-webkit-user-select:none;user-select:none;width:1%;padding:0 2px}.console-diff-comment-button{color:#6e7681;cursor:pointer;opacity:.6;background:0 0;border:1px solid #30363d;border-radius:4px;width:18px;height:18px;padding:0;font-size:.875rem;line-height:16px}.console-diff-row:hover .console-diff-comment-button,.console-diff-comment-button:focus-visible,.console-diff-comment-button[aria-expanded=true]{opacity:1;color:#fff;background:#1f6feb;border-color:#2f81f7}.console-diff-composer-row td{background:#0d1117;padding:6px 10px}.console-diff-composer{background:#161b22;border:1px solid #30363d;border-radius:6px;flex-direction:column;gap:6px;padding:8px;display:flex}.console-diff-composer-anchor{color:#8b949e;font-size:.6875rem}.console-diff-composer-input{resize:vertical;color:#c9d1d9;width:100%;font:inherit;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:6px 8px}.console-diff-composer-controls{align-items:center;gap:8px;display:flex}.console-diff-composer-submit{color:#fff;cursor:pointer;background:#238636;border:1px solid #238636;border-radius:6px;padding:4px 12px}.console-diff-composer-submit:disabled{opacity:.6;cursor:default}.console-diff-composer-cancel{color:#c9d1d9;cursor:pointer;background:0 0;border:1px solid #30363d;border-radius:6px;padding:4px 12px}.console-diff-composer-status{color:#8b949e;font-size:.75rem}.console-diff-composer-error{color:#ff7b72}.console-diff-composer-posted{color:#3fb950;margin:0;font-size:.75rem}.console-pr-header{flex-wrap:wrap;align-items:center;gap:10px;padding-top:4px;display:flex}.console-pr-section-title{color:#e6edf3;font-size:.9375rem;font-weight:600}.console-pr-section-state{color:#8b949e;border:1px solid #6e7681;border-radius:999px;padding:1px 8px;font-size:.6875rem}@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}
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>TDPM Console</title>
7
- <script type="module" crossorigin src="/assets/index-Bz0KvgIG.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-N9M1qhkw.css">
7
+ <script type="module" crossorigin src="/assets/index-CMK3yyfo.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BTtIpfv_.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -102,7 +102,7 @@ describe('cleanStaleTmuxSessions', () => {
102
102
  expect(killCall?.[1]).toEqual([
103
103
  'kill-session',
104
104
  '-t',
105
- 'https_//github_com/demo/repo/issues/1',
105
+ '=https_//github_com/demo/repo/issues/1',
106
106
  ]);
107
107
  });
108
108
 
@@ -160,6 +160,6 @@ describe('cleanStaleTmuxSessions', () => {
160
160
  (call) => call[0] === 'tmux' && call[1][0] === 'kill-session',
161
161
  );
162
162
  expect(killCalls).toHaveLength(1);
163
- expect(killCalls[0][1]).toEqual(['kill-session', '-t', 'idle_no_task']);
163
+ expect(killCalls[0][1]).toEqual(['kill-session', '-t', '=idle_no_task']);
164
164
  });
165
165
  });
@@ -1,3 +1,6 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
1
4
  import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
2
5
  import { NodeTmuxSessionRepository } from './NodeTmuxSessionRepository';
3
6
 
@@ -91,7 +94,7 @@ describe('NodeTmuxSessionRepository', () => {
91
94
  });
92
95
 
93
96
  describe('killSession', () => {
94
- it('kills the tmux session by name', async () => {
97
+ it('kills the tmux session by exact name', async () => {
95
98
  const runner = createMockRunner();
96
99
  runner.runCommand.mockResolvedValue({
97
100
  stdout: '',
@@ -102,20 +105,22 @@ describe('NodeTmuxSessionRepository', () => {
102
105
 
103
106
  await repository.killSession('no_task_session');
104
107
 
105
- expect(runner.runCommand.mock.calls[0][0]).toBe('tmux');
106
- expect(runner.runCommand.mock.calls[0][1]).toEqual([
107
- 'kill-session',
108
- '-t',
109
- 'no_task_session',
108
+ const tmuxCall = runner.runCommand.mock.calls.find(
109
+ (call) => call[0] === 'tmux',
110
+ );
111
+ expect(tmuxCall).toEqual([
112
+ 'tmux',
113
+ ['kill-session', '-t', '=no_task_session'],
110
114
  ]);
111
115
  });
112
116
 
113
117
  it('throws when tmux exits non-zero', async () => {
114
118
  const runner = createMockRunner();
115
- runner.runCommand.mockResolvedValue({
116
- stdout: '',
117
- stderr: "can't find session",
118
- exitCode: 1,
119
+ runner.runCommand.mockImplementation(async (program: string) => {
120
+ if (program === 'tmux') {
121
+ return { stdout: '', stderr: "can't find session", exitCode: 1 };
122
+ }
123
+ return { stdout: '', stderr: '', exitCode: 0 };
119
124
  });
120
125
  const repository = new NodeTmuxSessionRepository(runner);
121
126
 
@@ -124,7 +129,7 @@ describe('NodeTmuxSessionRepository', () => {
124
129
  );
125
130
  });
126
131
 
127
- it('stops the systemd user scope for the session after killing it, wrapping the stop with reset-failed', async () => {
132
+ it('stops the systemd user scope for the session before killing it, wrapping the stop with reset-failed', async () => {
128
133
  const runner = createMockRunner();
129
134
  runner.runCommand.mockResolvedValue({
130
135
  stdout: '',
@@ -137,16 +142,14 @@ describe('NodeTmuxSessionRepository', () => {
137
142
 
138
143
  const expectedScopeUnitName =
139
144
  'cl-https---github-com-owner-repo-issues-9.scope';
140
- const calls = runner.runCommand.mock.calls;
141
- expect(calls[0]).toEqual([
142
- 'tmux',
143
- ['kill-session', '-t', 'https_//github_com/owner/repo/issues/9'],
144
- ]);
145
- const systemctlCalls = calls.filter((call) => call[0] === 'systemctl');
146
- expect(systemctlCalls).toEqual([
145
+ expect(runner.runCommand.mock.calls).toEqual([
147
146
  ['systemctl', ['--user', 'reset-failed', expectedScopeUnitName]],
148
147
  ['systemctl', ['--user', 'stop', expectedScopeUnitName]],
149
148
  ['systemctl', ['--user', 'reset-failed', expectedScopeUnitName]],
149
+ [
150
+ 'tmux',
151
+ ['kill-session', '-t', '=https_//github_com/owner/repo/issues/9'],
152
+ ],
150
153
  ]);
151
154
  });
152
155
 
@@ -180,6 +183,63 @@ describe('NodeTmuxSessionRepository', () => {
180
183
  });
181
184
  });
182
185
 
186
+ describe('killOwnSession', () => {
187
+ let procDirectory: string;
188
+
189
+ beforeEach(() => {
190
+ procDirectory = fs.mkdtempSync(
191
+ path.join(os.tmpdir(), 'node-tmux-session-repository-proc-'),
192
+ );
193
+ });
194
+
195
+ afterEach(() => {
196
+ fs.rmSync(procDirectory, { recursive: true, force: true });
197
+ });
198
+
199
+ const writeCgroupContent = (content: string): void => {
200
+ const selfDirectory = path.join(procDirectory, 'self');
201
+ fs.mkdirSync(selfDirectory, { recursive: true });
202
+ fs.writeFileSync(path.join(selfDirectory, 'cgroup'), content);
203
+ };
204
+
205
+ it('stops only the current session scope derived from /proc/self/cgroup, without calling tmux', async () => {
206
+ writeCgroupContent(
207
+ '0::/user.slice/user-1000.slice/user@1000.service/app.slice/cl-leader-session.scope\n',
208
+ );
209
+ const runner = createMockRunner();
210
+ runner.runCommand.mockResolvedValue({
211
+ stdout: '',
212
+ stderr: '',
213
+ exitCode: 0,
214
+ });
215
+ const repository = new NodeTmuxSessionRepository(runner, procDirectory);
216
+
217
+ await repository.killOwnSession();
218
+
219
+ expect(runner.runCommand.mock.calls).toEqual([
220
+ ['systemctl', ['--user', 'reset-failed', 'cl-leader-session.scope']],
221
+ ['systemctl', ['--user', 'stop', 'cl-leader-session.scope']],
222
+ ['systemctl', ['--user', 'reset-failed', 'cl-leader-session.scope']],
223
+ ]);
224
+ expect(
225
+ runner.runCommand.mock.calls.some((call) => call[0] === 'tmux'),
226
+ ).toBe(false);
227
+ });
228
+
229
+ it('throws when no cl-*.scope unit can be found in /proc/self/cgroup', async () => {
230
+ writeCgroupContent(
231
+ '0::/user.slice/user-1000.slice/user@1000.service/app.slice/vte-spawn-abc.scope\n',
232
+ );
233
+ const runner = createMockRunner();
234
+ const repository = new NodeTmuxSessionRepository(runner, procDirectory);
235
+
236
+ await expect(repository.killOwnSession()).rejects.toThrow(
237
+ 'Failed to determine the current cl-*.scope systemd user unit from /proc/self/cgroup',
238
+ );
239
+ expect(runner.runCommand.mock.calls).toHaveLength(0);
240
+ });
241
+ });
242
+
183
243
  describe('listInteractiveProcessCommandLines', () => {
184
244
  it('parses process command lines from ps output', async () => {
185
245
  const runner = createMockRunner();
@@ -1,10 +1,16 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
1
3
  import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
2
4
  import { TmuxSessionRepository } from '../../domain/usecases/adapter-interfaces/TmuxSessionRepository';
3
5
  import { LiveTmuxSession } from '../../domain/entities/LiveTmuxSession';
4
6
  import { clSessionScopeUnitName } from './clSessionScopeUnitName';
7
+ import { clSessionScopeUnitNameFromCgroupContent } from './clSessionScopeUnitNameFromCgroupContent';
5
8
 
6
9
  export class NodeTmuxSessionRepository implements TmuxSessionRepository {
7
- constructor(private readonly localCommandRunner: LocalCommandRunner) {}
10
+ constructor(
11
+ private readonly localCommandRunner: LocalCommandRunner,
12
+ private readonly procDirectory: string = '/proc',
13
+ ) {}
8
14
 
9
15
  listLiveSessionNames = async (): Promise<string[]> => {
10
16
  const { stdout, exitCode } = await this.localCommandRunner.runCommand(
@@ -75,9 +81,11 @@ export class NodeTmuxSessionRepository implements TmuxSessionRepository {
75
81
  };
76
82
 
77
83
  killSession = async (sessionName: string): Promise<void> => {
84
+ const scopeUnitName = clSessionScopeUnitName(sessionName);
85
+ await this.stopScopeUnit(scopeUnitName);
78
86
  const { stderr, exitCode } = await this.localCommandRunner.runCommand(
79
87
  'tmux',
80
- ['kill-session', '-t', sessionName],
88
+ ['kill-session', '-t', `=${sessionName}`],
81
89
  );
82
90
  if (exitCode !== 0) {
83
91
  throw new Error(
@@ -86,11 +94,24 @@ export class NodeTmuxSessionRepository implements TmuxSessionRepository {
86
94
  }`,
87
95
  );
88
96
  }
89
- await this.stopClSessionScope(sessionName);
90
97
  };
91
98
 
92
- private stopClSessionScope = async (sessionName: string): Promise<void> => {
93
- const scopeUnitName = clSessionScopeUnitName(sessionName);
99
+ killOwnSession = async (): Promise<void> => {
100
+ const cgroupContent = fs.readFileSync(
101
+ path.join(this.procDirectory, 'self', 'cgroup'),
102
+ 'utf8',
103
+ );
104
+ const scopeUnitName =
105
+ clSessionScopeUnitNameFromCgroupContent(cgroupContent);
106
+ if (scopeUnitName === null) {
107
+ throw new Error(
108
+ 'Failed to determine the current cl-*.scope systemd user unit from /proc/self/cgroup',
109
+ );
110
+ }
111
+ await this.stopScopeUnit(scopeUnitName);
112
+ };
113
+
114
+ private stopScopeUnit = async (scopeUnitName: string): Promise<void> => {
94
115
  await this.localCommandRunner.runCommand('systemctl', [
95
116
  '--user',
96
117
  'reset-failed',
@@ -107,7 +128,7 @@ export class NodeTmuxSessionRepository implements TmuxSessionRepository {
107
128
  ]);
108
129
  if (exitCode !== 0) {
109
130
  console.error(
110
- `Failed to stop systemd user scope "${scopeUnitName}" for tmux session "${sessionName}": exit code ${exitCode}${
131
+ `Failed to stop systemd user scope "${scopeUnitName}": exit code ${exitCode}${
111
132
  stderr ? `: ${stderr}` : ''
112
133
  }`,
113
134
  );
@@ -0,0 +1,34 @@
1
+ import { clSessionScopeUnitNameFromCgroupContent } from './clSessionScopeUnitNameFromCgroupContent';
2
+
3
+ describe('clSessionScopeUnitNameFromCgroupContent', () => {
4
+ it('extracts the cl-*.scope unit from a cgroup v2 single-hierarchy line', () => {
5
+ const cgroupContent =
6
+ '0::/user.slice/user-1000.slice/user@1000.service/app.slice/cl-https---github-com-owner-repo-issues-9.scope\n';
7
+
8
+ const result = clSessionScopeUnitNameFromCgroupContent(cgroupContent);
9
+
10
+ expect(result).toBe('cl-https---github-com-owner-repo-issues-9.scope');
11
+ });
12
+
13
+ it('extracts the cl-*.scope unit from a multi-hierarchy cgroup v1 file', () => {
14
+ const cgroupContent = [
15
+ '12:pids:/user.slice/user-1000.slice/user@1000.service/app.slice/cl-leader-session.scope',
16
+ '11:cpu,cpuacct:/user.slice/user-1000.slice/user@1000.service/app.slice/cl-leader-session.scope',
17
+ '0::/user.slice/user-1000.slice/user@1000.service/app.slice/cl-leader-session.scope',
18
+ '',
19
+ ].join('\n');
20
+
21
+ const result = clSessionScopeUnitNameFromCgroupContent(cgroupContent);
22
+
23
+ expect(result).toBe('cl-leader-session.scope');
24
+ });
25
+
26
+ it('returns null when no cl-*.scope unit is present', () => {
27
+ const cgroupContent =
28
+ '0::/user.slice/user-1000.slice/user@1000.service/app.slice/vte-spawn-abc.scope\n';
29
+
30
+ const result = clSessionScopeUnitNameFromCgroupContent(cgroupContent);
31
+
32
+ expect(result).toBeNull();
33
+ });
34
+ });
@@ -0,0 +1,8 @@
1
+ const CL_SESSION_SCOPE_UNIT_PATTERN = /cl-[A-Za-z0-9._-]+\.scope/;
2
+
3
+ export const clSessionScopeUnitNameFromCgroupContent = (
4
+ cgroupContent: string,
5
+ ): string | null => {
6
+ const match = cgroupContent.match(CL_SESSION_SCOPE_UNIT_PATTERN);
7
+ return match ? match[0] : null;
8
+ };