@slycode/slycode 0.2.20 → 0.2.22

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 (197) hide show
  1. package/dist/bridge/provider-utils.d.ts +10 -0
  2. package/dist/bridge/provider-utils.js +5 -1
  3. package/dist/bridge/provider-utils.js.map +1 -1
  4. package/dist/bridge/pty-handler.js +58 -12
  5. package/dist/bridge/pty-handler.js.map +1 -1
  6. package/dist/bridge/session-manager.js +6 -0
  7. package/dist/bridge/session-manager.js.map +1 -1
  8. package/dist/bridge/types.d.ts +4 -0
  9. package/dist/messaging/bridge-client.d.ts +3 -3
  10. package/dist/messaging/bridge-client.js +6 -4
  11. package/dist/messaging/bridge-client.js.map +1 -1
  12. package/dist/messaging/index.js +117 -26
  13. package/dist/messaging/index.js.map +1 -1
  14. package/dist/messaging/state.d.ts +3 -0
  15. package/dist/messaging/state.js +13 -0
  16. package/dist/messaging/state.js.map +1 -1
  17. package/dist/messaging/stt.d.ts +1 -1
  18. package/dist/messaging/stt.js +41 -9
  19. package/dist/messaging/stt.js.map +1 -1
  20. package/dist/messaging/types.d.ts +3 -0
  21. package/dist/web/.next/BUILD_ID +1 -1
  22. package/dist/web/.next/build-manifest.json +2 -2
  23. package/dist/web/.next/server/app/_global-error.html +2 -2
  24. package/dist/web/.next/server/app/_global-error.rsc +1 -1
  25. package/dist/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  27. package/dist/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  28. package/dist/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  29. package/dist/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  30. package/dist/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  31. package/dist/web/.next/server/app/_not-found.html +1 -1
  32. package/dist/web/.next/server/app/_not-found.rsc +2 -2
  33. package/dist/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  34. package/dist/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  35. package/dist/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  36. package/dist/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  37. package/dist/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  38. package/dist/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  39. package/dist/web/.next/server/app/api/areas/route.js +1 -1
  40. package/dist/web/.next/server/app/api/areas/route.js.nft.json +1 -1
  41. package/dist/web/.next/server/app/api/bridge/[...path]/route.js +1 -1
  42. package/dist/web/.next/server/app/api/bridge/[...path]/route.js.nft.json +1 -1
  43. package/dist/web/.next/server/app/api/cli-assets/assistant/route.js +1 -1
  44. package/dist/web/.next/server/app/api/cli-assets/assistant/route.js.nft.json +1 -1
  45. package/dist/web/.next/server/app/api/cli-assets/fix/route.js +1 -1
  46. package/dist/web/.next/server/app/api/cli-assets/fix/route.js.nft.json +1 -1
  47. package/dist/web/.next/server/app/api/cli-assets/import/route.js +1 -1
  48. package/dist/web/.next/server/app/api/cli-assets/import/route.js.nft.json +1 -1
  49. package/dist/web/.next/server/app/api/cli-assets/route.js +2 -2
  50. package/dist/web/.next/server/app/api/cli-assets/route.js.nft.json +1 -1
  51. package/dist/web/.next/server/app/api/cli-assets/store/preview/route.js +1 -1
  52. package/dist/web/.next/server/app/api/cli-assets/store/preview/route.js.nft.json +1 -1
  53. package/dist/web/.next/server/app/api/cli-assets/store/route.js +2 -2
  54. package/dist/web/.next/server/app/api/cli-assets/store/route.js.nft.json +1 -1
  55. package/dist/web/.next/server/app/api/cli-assets/sync/route.js +1 -1
  56. package/dist/web/.next/server/app/api/cli-assets/sync/route.js.nft.json +1 -1
  57. package/dist/web/.next/server/app/api/cli-assets/updates/route.js +2 -2
  58. package/dist/web/.next/server/app/api/cli-assets/updates/route.js.nft.json +1 -1
  59. package/dist/web/.next/server/app/api/dashboard/route.js +2 -2
  60. package/dist/web/.next/server/app/api/dashboard/route.js.nft.json +1 -1
  61. package/dist/web/.next/server/app/api/events/route.js +1 -1
  62. package/dist/web/.next/server/app/api/events/route.js.nft.json +1 -1
  63. package/dist/web/.next/server/app/api/file/route.js +1 -1
  64. package/dist/web/.next/server/app/api/file/route.js.nft.json +1 -1
  65. package/dist/web/.next/server/app/api/git-status/route.js +1 -1
  66. package/dist/web/.next/server/app/api/git-status/route.js.nft.json +1 -1
  67. package/dist/web/.next/server/app/api/kanban/route.js +1 -1
  68. package/dist/web/.next/server/app/api/kanban/route.js.nft.json +1 -1
  69. package/dist/web/.next/server/app/api/kanban/stream/route.js +1 -1
  70. package/dist/web/.next/server/app/api/kanban/stream/route.js.nft.json +1 -1
  71. package/dist/web/.next/server/app/api/projects/[id]/route.js +2 -2
  72. package/dist/web/.next/server/app/api/projects/[id]/route.js.nft.json +1 -1
  73. package/dist/web/.next/server/app/api/projects/analyze/route.js +1 -1
  74. package/dist/web/.next/server/app/api/projects/analyze/route.js.nft.json +1 -1
  75. package/dist/web/.next/server/app/api/projects/reorder/route.js +2 -2
  76. package/dist/web/.next/server/app/api/projects/reorder/route.js.nft.json +1 -1
  77. package/dist/web/.next/server/app/api/projects/route.js +2 -2
  78. package/dist/web/.next/server/app/api/projects/route.js.nft.json +1 -1
  79. package/dist/web/.next/server/app/api/providers/route.js +1 -1
  80. package/dist/web/.next/server/app/api/providers/route.js.nft.json +1 -1
  81. package/dist/web/.next/server/app/api/scheduler/route.js +1 -1
  82. package/dist/web/.next/server/app/api/scheduler/route.js.nft.json +1 -1
  83. package/dist/web/.next/server/app/api/search/route.js +1 -1
  84. package/dist/web/.next/server/app/api/search/route.js.nft.json +1 -1
  85. package/dist/web/.next/server/app/api/settings/route.js +1 -1
  86. package/dist/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  87. package/dist/web/.next/server/app/api/sly-actions/invalidate/route.js +1 -1
  88. package/dist/web/.next/server/app/api/sly-actions/invalidate/route.js.nft.json +1 -1
  89. package/dist/web/.next/server/app/api/sly-actions/route.js +1 -1
  90. package/dist/web/.next/server/app/api/sly-actions/route.js.nft.json +1 -1
  91. package/dist/web/.next/server/app/api/sly-actions/stream/route.js +1 -1
  92. package/dist/web/.next/server/app/api/sly-actions/stream/route.js.nft.json +1 -1
  93. package/dist/web/.next/server/app/api/system-stats/route.js +1 -1
  94. package/dist/web/.next/server/app/api/system-stats/route.js.nft.json +1 -1
  95. package/dist/web/.next/server/app/api/terminal-classes/route.js +1 -1
  96. package/dist/web/.next/server/app/api/terminal-classes/route.js.nft.json +1 -1
  97. package/dist/web/.next/server/app/api/transcribe/route.js +5 -5
  98. package/dist/web/.next/server/app/api/transcribe/route.js.nft.json +1 -1
  99. package/dist/web/.next/server/app/api/version-check/route.js +1 -1
  100. package/dist/web/.next/server/app/api/version-check/route.js.nft.json +1 -1
  101. package/dist/web/.next/server/app/page.js +1 -1
  102. package/dist/web/.next/server/app/page.js.nft.json +1 -1
  103. package/dist/web/.next/server/app/page_client-reference-manifest.js +1 -1
  104. package/dist/web/.next/server/app/project/[id]/page.js +2 -2
  105. package/dist/web/.next/server/app/project/[id]/page.js.nft.json +1 -1
  106. package/dist/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
  107. package/dist/web/.next/server/chunks/{[externals]__c6831f39._.js → [externals]__78e522ea._.js} +2 -2
  108. package/dist/web/.next/server/chunks/{[root-of-the-server]__1ec21ccc._.js → [root-of-the-server]__029203cd._.js} +3 -3
  109. package/dist/web/.next/server/chunks/{[root-of-the-server]__4297cb97._.js → [root-of-the-server]__0d6d4443._.js} +1 -1
  110. package/dist/web/.next/server/chunks/[root-of-the-server]__172ad0b1._.js +18 -0
  111. package/dist/web/.next/server/chunks/[root-of-the-server]__1c5f4ef9._.js +3 -0
  112. package/dist/web/.next/server/chunks/[root-of-the-server]__1cab11f0._.js +3 -0
  113. package/dist/web/.next/server/chunks/{[root-of-the-server]__0f69c28a._.js → [root-of-the-server]__1eb3f172._.js} +2 -2
  114. package/dist/web/.next/server/chunks/[root-of-the-server]__22cba275._.js +3 -0
  115. package/dist/web/.next/server/chunks/[root-of-the-server]__2543e413._.js +3 -0
  116. package/dist/web/.next/server/chunks/[root-of-the-server]__2c42a835._.js +3 -0
  117. package/dist/web/.next/server/chunks/[root-of-the-server]__2ed0ff47._.js +3 -0
  118. package/dist/web/.next/server/chunks/[root-of-the-server]__35454eea._.js +27 -0
  119. package/dist/web/.next/server/chunks/[root-of-the-server]__35768b56._.js +3 -0
  120. package/dist/web/.next/server/chunks/[root-of-the-server]__3880228a._.js +3 -0
  121. package/dist/web/.next/server/chunks/[root-of-the-server]__42322d88._.js +3 -0
  122. package/dist/web/.next/server/chunks/{[root-of-the-server]__d0f4efec._.js → [root-of-the-server]__5152eeff._.js} +3 -3
  123. package/dist/web/.next/server/chunks/[root-of-the-server]__527c7f57._.js +3 -0
  124. package/dist/web/.next/server/chunks/[root-of-the-server]__5c5dac4b._.js +3 -0
  125. package/dist/web/.next/server/chunks/[root-of-the-server]__5cb130f2._.js +3 -0
  126. package/dist/web/.next/server/chunks/[root-of-the-server]__68927e75._.js +3 -0
  127. package/dist/web/.next/server/chunks/[root-of-the-server]__719517c7._.js +3 -0
  128. package/dist/web/.next/server/chunks/[root-of-the-server]__73cf49c2._.js +3 -0
  129. package/dist/web/.next/server/chunks/{[root-of-the-server]__f5dae2ad._.js → [root-of-the-server]__7af4ab09._.js} +1 -1
  130. package/dist/web/.next/server/chunks/{[root-of-the-server]__4244617a._.js → [root-of-the-server]__7e6860e0._.js} +3 -3
  131. package/dist/web/.next/server/chunks/[root-of-the-server]__88bf5e22._.js +3 -0
  132. package/dist/web/.next/server/chunks/{[root-of-the-server]__f97e93fa._.js → [root-of-the-server]__8b4259cb._.js} +3 -3
  133. package/dist/web/.next/server/chunks/[root-of-the-server]__92f81907._.js +3 -0
  134. package/dist/web/.next/server/chunks/[root-of-the-server]__967603e9._.js +3 -0
  135. package/dist/web/.next/server/chunks/[root-of-the-server]__9e4bd28f._.js +3 -0
  136. package/dist/web/.next/server/chunks/[root-of-the-server]__ba1d2e56._.js +3 -0
  137. package/dist/web/.next/server/chunks/[root-of-the-server]__c942d872._.js +3 -0
  138. package/dist/web/.next/server/chunks/[root-of-the-server]__d7893622._.js +3 -0
  139. package/dist/web/.next/server/chunks/{[root-of-the-server]__3b9d3e43._.js → [root-of-the-server]__d843611b._.js} +6 -6
  140. package/dist/web/.next/server/chunks/[root-of-the-server]__f4d2627f._.js +3 -0
  141. package/dist/web/.next/server/chunks/[root-of-the-server]__f597835d._.js +3 -0
  142. package/dist/web/.next/server/chunks/{[root-of-the-server]__cf14e306._.js → [root-of-the-server]__fe8b9abd._.js} +1 -1
  143. package/dist/web/.next/server/chunks/src_677020aa._.js +1 -1
  144. package/dist/web/.next/server/chunks/ssr/[root-of-the-server]__1f5fc489._.js +1 -1
  145. package/dist/web/.next/server/chunks/ssr/[root-of-the-server]__43d93717._.js +3 -0
  146. package/dist/web/.next/server/chunks/ssr/[root-of-the-server]__90f82e6d._.js +3 -0
  147. package/dist/web/.next/server/chunks/ssr/[root-of-the-server]__bcbe4bf2._.js +1 -1
  148. package/dist/web/.next/server/chunks/ssr/src_components_c4135402._.js +1 -1
  149. package/dist/web/.next/server/chunks/ssr/src_lib_registry_ts_2fc87c9c._.js +1 -1
  150. package/dist/web/.next/server/pages/404.html +1 -1
  151. package/dist/web/.next/server/pages/500.html +2 -2
  152. package/dist/web/.next/static/chunks/18cfbdd7e977bb01.css +1 -0
  153. package/dist/web/.next/static/chunks/{8cb404d087e9f3c7.js → 4049cceee6a49323.js} +1 -1
  154. package/dist/web/.next/static/chunks/8415039c5941cf5c.js +4 -0
  155. package/dist/web/.next/static/chunks/{3d5195b57fc05540.js → a0f5f9cdee8a22c1.js} +2 -2
  156. package/dist/web/src/app/api/projects/analyze/route.ts +12 -2
  157. package/dist/web/src/app/api/projects/route.ts +10 -11
  158. package/dist/web/src/app/api/providers/route.ts +4 -0
  159. package/dist/web/src/components/CardModal.tsx +31 -24
  160. package/dist/web/src/components/ClaudeTerminalPanel.tsx +124 -70
  161. package/dist/web/src/lib/paths.ts +14 -0
  162. package/dist/web/tsconfig.tsbuildinfo +1 -1
  163. package/package.json +2 -2
  164. package/templates/kanban-seed.json +1 -1
  165. package/dist/web/.next/server/chunks/[root-of-the-server]__09aec55a._.js +0 -3
  166. package/dist/web/.next/server/chunks/[root-of-the-server]__12f6cd6f._.js +0 -3
  167. package/dist/web/.next/server/chunks/[root-of-the-server]__15fc9266._.js +0 -18
  168. package/dist/web/.next/server/chunks/[root-of-the-server]__198f01e0._.js +0 -3
  169. package/dist/web/.next/server/chunks/[root-of-the-server]__279e9bf3._.js +0 -3
  170. package/dist/web/.next/server/chunks/[root-of-the-server]__2b639eab._.js +0 -3
  171. package/dist/web/.next/server/chunks/[root-of-the-server]__2d1f0ed9._.js +0 -3
  172. package/dist/web/.next/server/chunks/[root-of-the-server]__3f239285._.js +0 -3
  173. package/dist/web/.next/server/chunks/[root-of-the-server]__47dd878e._.js +0 -3
  174. package/dist/web/.next/server/chunks/[root-of-the-server]__5b8c9374._.js +0 -3
  175. package/dist/web/.next/server/chunks/[root-of-the-server]__5e08b942._.js +0 -3
  176. package/dist/web/.next/server/chunks/[root-of-the-server]__6ffce934._.js +0 -3
  177. package/dist/web/.next/server/chunks/[root-of-the-server]__71bb3374._.js +0 -3
  178. package/dist/web/.next/server/chunks/[root-of-the-server]__7603305e._.js +0 -3
  179. package/dist/web/.next/server/chunks/[root-of-the-server]__7c476ad6._.js +0 -3
  180. package/dist/web/.next/server/chunks/[root-of-the-server]__846ca56f._.js +0 -3
  181. package/dist/web/.next/server/chunks/[root-of-the-server]__98d88050._.js +0 -3
  182. package/dist/web/.next/server/chunks/[root-of-the-server]__b273cc05._.js +0 -3
  183. package/dist/web/.next/server/chunks/[root-of-the-server]__b90bbd70._.js +0 -3
  184. package/dist/web/.next/server/chunks/[root-of-the-server]__d5272169._.js +0 -3
  185. package/dist/web/.next/server/chunks/[root-of-the-server]__d56e68cb._.js +0 -3
  186. package/dist/web/.next/server/chunks/[root-of-the-server]__d6362272._.js +0 -3
  187. package/dist/web/.next/server/chunks/[root-of-the-server]__de1277ee._.js +0 -27
  188. package/dist/web/.next/server/chunks/[root-of-the-server]__e88a19d2._.js +0 -3
  189. package/dist/web/.next/server/chunks/[root-of-the-server]__f3e501b6._.js +0 -3
  190. package/dist/web/.next/server/chunks/[root-of-the-server]__f59af2bc._.js +0 -3
  191. package/dist/web/.next/server/chunks/ssr/[root-of-the-server]__9ac6ea25._.js +0 -3
  192. package/dist/web/.next/server/chunks/ssr/[root-of-the-server]__dfe2728c._.js +0 -3
  193. package/dist/web/.next/static/chunks/59fb302a5bfd2dc0.js +0 -4
  194. package/dist/web/.next/static/chunks/747f5e5f9dcf2621.css +0 -1
  195. /package/dist/web/.next/static/{tQdF18XbrwPnmXEMVlcfU → b2V8jC3HBMi4vgm7Kie3H}/_buildManifest.js +0 -0
  196. /package/dist/web/.next/static/{tQdF18XbrwPnmXEMVlcfU → b2V8jC3HBMi4vgm7Kie3H}/_clientMiddlewareManifest.json +0 -0
  197. /package/dist/web/.next/static/{tQdF18XbrwPnmXEMVlcfU → b2V8jC3HBMi4vgm7Kie3H}/_ssgManifest.js +0 -0
@@ -2,7 +2,7 @@ import { NextResponse } from 'next/server';
2
2
  import { execFile } from 'child_process';
3
3
  import { promisify } from 'util';
4
4
  import path from 'path';
5
- import { getPackageDir } from '@/lib/paths';
5
+ import { getPackageDir, expandTilde } from '@/lib/paths';
6
6
 
7
7
  const execFileAsync = promisify(execFile);
8
8
 
@@ -23,11 +23,21 @@ export async function POST(request: Request) {
23
23
  );
24
24
  }
25
25
 
26
+ // Expand tilde and validate absolute path
27
+ const expanded = expandTilde(targetPath);
28
+ if (!path.isAbsolute(expanded)) {
29
+ return NextResponse.json(
30
+ { error: 'Please enter an absolute path (e.g. ~/Dev/myproject or /home/user/Dev/myproject)' },
31
+ { status: 400 }
32
+ );
33
+ }
34
+ const resolvedPath = path.resolve(expanded);
35
+
26
36
  const scaffoldScript = path.join(getPackageDir(), 'scripts', 'scaffold.js');
27
37
  const args = [
28
38
  scaffoldScript,
29
39
  'analyze',
30
- '--path', targetPath,
40
+ '--path', resolvedPath,
31
41
  '--json',
32
42
  ];
33
43
  if (providers && Array.isArray(providers) && providers.length > 0) {
@@ -3,20 +3,12 @@ import { loadRegistry, saveRegistry } from '@/lib/registry';
3
3
  import { execFile } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  import path from 'path';
6
- import os from 'os';
7
- import { getSlycodeRoot, getPackageDir } from '@/lib/paths';
6
+ import { getSlycodeRoot, getPackageDir, expandTilde } from '@/lib/paths';
8
7
 
9
8
  const execFileAsync = promisify(execFile);
10
9
 
11
10
  export const dynamic = 'force-dynamic';
12
11
 
13
- function expandTilde(p: string): string {
14
- if (p.startsWith('~/') || p === '~') {
15
- return p.replace(/^~/, os.homedir());
16
- }
17
- return p;
18
- }
19
-
20
12
  function toKebabCase(str: string): string {
21
13
  return str
22
14
  .toLowerCase()
@@ -55,8 +47,15 @@ export async function POST(request: Request) {
55
47
  );
56
48
  }
57
49
 
58
- // Resolve tilde and make absolute
59
- const resolvedPath = path.resolve(expandTilde(projectPath));
50
+ // Resolve tilde and validate absolute path
51
+ const expanded = expandTilde(projectPath);
52
+ if (!path.isAbsolute(expanded)) {
53
+ return NextResponse.json(
54
+ { error: 'Please enter an absolute path (e.g. ~/Dev/myproject or /home/user/Dev/myproject)' },
55
+ { status: 400 }
56
+ );
57
+ }
58
+ const resolvedPath = path.resolve(expanded);
60
59
 
61
60
  const registry = await loadRegistry();
62
61
  const projectId = toKebabCase(name);
@@ -31,6 +31,10 @@ function validateProviderDefault(
31
31
  if (typeof d.skipPermissions !== 'boolean') {
32
32
  return `${label}.skipPermissions must be a boolean`;
33
33
  }
34
+ // Optional model field — pass through to CLI (no validation against available list)
35
+ if ('model' in d && d.model !== undefined && typeof d.model !== 'string') {
36
+ return `${label}.model must be a string if provided`;
37
+ }
34
38
  return null;
35
39
  }
36
40
 
@@ -649,33 +649,40 @@ export function CardModal({ card, stage, projectId, projectPath, onClose, onUpda
649
649
  };
650
650
  const currentDocPath = getDocPath(activeTab);
651
651
 
652
- // Track loaded docs by path to avoid reloading
652
+ // Track loaded docs by path
653
653
  const [loadedDocs, setLoadedDocs] = useState<Record<string, string>>({});
654
654
  const [docErrors, setDocErrors] = useState<Record<string, string>>({});
655
655
 
656
- // Load document when a doc tab is selected
656
+ // Re-fetch document every time a doc tab is selected (always show latest from disk)
657
657
  useEffect(() => {
658
658
  const isDocTab = activeTab === 'design' || activeTab === 'feature' || activeTab === 'test';
659
- if (isDocTab && currentDocPath && !loadedDocs[currentDocPath] && !docLoading) {
660
- // eslint-disable-next-line react-hooks/set-state-in-effect -- gating fetch with loading flag
661
- setDocLoading(true);
662
- fetch(`/api/file?path=${encodeURIComponent(currentDocPath)}&projectId=${encodeURIComponent(projectId)}`)
663
- .then((res) => res.json())
664
- .then((data) => {
665
- if (data.error) {
666
- setDocErrors((prev) => ({ ...prev, [currentDocPath!]: data.error }));
667
- } else {
668
- setLoadedDocs((prev) => ({ ...prev, [currentDocPath!]: data.content }));
669
- }
670
- })
671
- .catch((err) => {
672
- setDocErrors((prev) => ({ ...prev, [currentDocPath!]: err.message }));
673
- })
674
- .finally(() => {
675
- setDocLoading(false);
676
- });
677
- }
678
- }, [activeTab, currentDocPath, loadedDocs, docLoading, projectId]);
659
+ if (!isDocTab || !currentDocPath) return;
660
+
661
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- gating fetch with loading flag
662
+ setDocLoading(true);
663
+ const fetchPath = currentDocPath;
664
+ fetch(`/api/file?path=${encodeURIComponent(fetchPath)}&projectId=${encodeURIComponent(projectId)}`)
665
+ .then((res) => res.json())
666
+ .then((data) => {
667
+ if (data.error) {
668
+ setDocErrors((prev) => ({ ...prev, [fetchPath]: data.error }));
669
+ } else {
670
+ setDocErrors((prev) => {
671
+ const next = { ...prev };
672
+ delete next[fetchPath];
673
+ return next;
674
+ });
675
+ setLoadedDocs((prev) => ({ ...prev, [fetchPath]: data.content }));
676
+ }
677
+ })
678
+ .catch((err) => {
679
+ setDocErrors((prev) => ({ ...prev, [fetchPath]: err.message }));
680
+ })
681
+ .finally(() => {
682
+ setDocLoading(false);
683
+ });
684
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally re-fetch on every tab switch
685
+ }, [activeTab, currentDocPath, projectId]);
679
686
 
680
687
  const handleTitleSave = () => {
681
688
  if (editedTitle.trim() && editedTitle !== card.title) {
@@ -1623,12 +1630,12 @@ export function CardModal({ card, stage, projectId, projectPath, onClose, onUpda
1623
1630
  )}
1624
1631
  </button>
1625
1632
  )}
1626
- {docLoading && !loadedDocs[currentDocPath!] && (
1633
+ {docLoading && !loadedDocs[currentDocPath!] && !docErrors[currentDocPath!] && (
1627
1634
  <div className="flex items-center justify-center py-8">
1628
1635
  <div className="text-void-500">Loading document...</div>
1629
1636
  </div>
1630
1637
  )}
1631
- {currentDocPath && docErrors[currentDocPath] && (
1638
+ {currentDocPath && docErrors[currentDocPath] && !loadedDocs[currentDocPath] && (
1632
1639
  <div className="rounded-lg bg-red-50 p-4 text-red-700 dark:bg-red-900/20 dark:text-red-300">
1633
1640
  Error loading document: {docErrors[currentDocPath]}
1634
1641
  </div>
@@ -13,11 +13,16 @@ interface ProviderConfig {
13
13
  command: string;
14
14
  permissions: { flag: string; label: string; default: boolean };
15
15
  resume: { supported: boolean };
16
+ model?: {
17
+ flag: string;
18
+ available: Array<{ id: string; label: string; description?: string }>;
19
+ };
16
20
  }
17
21
 
18
22
  interface ProviderDefault {
19
23
  provider: string;
20
24
  skipPermissions: boolean;
25
+ model?: string;
21
26
  }
22
27
 
23
28
  interface ProvidersData {
@@ -45,6 +50,7 @@ interface SessionInfo {
45
50
  claudeSessionId?: string | null;
46
51
  provider?: string;
47
52
  skipPermissions?: boolean;
53
+ model?: string;
48
54
  }
49
55
 
50
56
  export interface TerminalContext {
@@ -145,6 +151,13 @@ export function ClaudeTerminalPanel({
145
151
  // Spawn error toast — shown when session creation fails (e.g. posix_spawnp failed)
146
152
  const [spawnError, setSpawnError] = useState<string | null>(null);
147
153
 
154
+ // Model selection state
155
+ const [selectedModel, setSelectedModel] = useState(''); // '' = Default (no flag)
156
+ const prevProviderRef = useRef(selectedProvider);
157
+ const userSelectedModelRef = useRef(false);
158
+ const sessionInfoRef = useRef(sessionInfo);
159
+ sessionInfoRef.current = sessionInfo;
160
+
148
161
  // Instruction file check state
149
162
  const [instructionFileCheck, setInstructionFileCheck] = useState<{ needed: boolean; targetFile?: string; copySource?: string } | null>(null);
150
163
  const [createInstructionFile, setCreateInstructionFile] = useState(true);
@@ -177,15 +190,53 @@ export function ClaudeTerminalPanel({
177
190
  setSelectedProvider(def.provider);
178
191
  }
179
192
  setSkipPermissions(def.skipPermissions);
193
+ if (def.model) {
194
+ setSelectedModel(def.model);
195
+ }
180
196
  }
181
197
  }
182
198
  })
183
199
  .catch(() => { /* providers.json not available, use defaults */ });
184
200
  }, [stage]);
185
201
 
202
+ // Pre-fill model when provider changes
203
+ useEffect(() => {
204
+ if (!providersData) return;
205
+ const providerChanged = prevProviderRef.current !== selectedProvider;
206
+ prevProviderRef.current = selectedProvider;
207
+ if (!providerChanged && userSelectedModelRef.current) return;
208
+ userSelectedModelRef.current = false;
209
+ // Priority: last-used from session > stage default > Default
210
+ const si = sessionInfoRef.current;
211
+ if (si?.model && si.provider === selectedProvider) {
212
+ const available = providersData.providers[selectedProvider]?.model?.available;
213
+ if (available?.some(m => m.id === si.model)) {
214
+ setSelectedModel(si.model);
215
+ return;
216
+ }
217
+ }
218
+ const stageDefault = stage ? providersData.defaults.stages[stage] : null;
219
+ const def = stageDefault || providersData.defaults.global;
220
+ if (def?.model && def.provider === selectedProvider) {
221
+ const available = providersData.providers[selectedProvider]?.model?.available;
222
+ if (available?.some(m => m.id === def.model)) {
223
+ setSelectedModel(def.model);
224
+ return;
225
+ }
226
+ }
227
+ setSelectedModel('');
228
+ }, [selectedProvider, providersData, stage]);
229
+
186
230
  // Persist provider default to /api/providers (fire-and-forget)
187
231
  const saveProviderDefault = useCallback((provider: string, skip: boolean) => {
188
- const defaultVal = { provider, skipPermissions: skip };
232
+ // Preserve any existing model field in the stage default (stage model defaults are intentional config)
233
+ const existingDefault = stage
234
+ ? providersData?.defaults.stages[stage]
235
+ : providersData?.defaults.global;
236
+ const defaultVal: Record<string, unknown> = { provider, skipPermissions: skip };
237
+ if (existingDefault?.model && existingDefault.provider === provider) {
238
+ defaultVal.model = existingDefault.model;
239
+ }
189
240
  const defaults = stage
190
241
  ? { stages: { [stage]: defaultVal } }
191
242
  : { global: defaultVal };
@@ -194,7 +245,7 @@ export function ClaudeTerminalPanel({
194
245
  headers: { 'Content-Type': 'application/json' },
195
246
  body: JSON.stringify({ defaults }),
196
247
  }).catch(() => { /* preference save — ignore errors */ });
197
- }, [stage]);
248
+ }, [stage, providersData]);
198
249
 
199
250
  const isRunning = sessionInfo?.status === 'running' || sessionInfo?.status === 'detached';
200
251
  const hasHistory = sessionInfo?.hasHistory;
@@ -288,6 +339,74 @@ export function ClaudeTerminalPanel({
288
339
  </div>
289
340
  ) : null;
290
341
 
342
+ // Helper to render model label from provider config
343
+ const getModelLabel = (providerId: string, modelId?: string) => {
344
+ if (!modelId || !providersData) return null;
345
+ return providersData.providers[providerId]?.model?.available?.find(m => m.id === modelId)?.label || modelId;
346
+ };
347
+
348
+ // Shared provider selector (eliminates duplication between resume and fresh-start screens)
349
+ const renderProviderSelector = (options?: { showModel?: boolean }) => {
350
+ if (!providersData || Object.keys(providersData.providers).length <= 1) return null;
351
+ const currentProvider = providersData.providers[selectedProvider];
352
+ const models = currentProvider?.model?.available;
353
+ const showModel = options?.showModel !== false;
354
+ return (
355
+ <div className="flex flex-col items-center gap-2 mt-3 pt-3 border-t border-void-700/50">
356
+ <div className="flex gap-1">
357
+ {Object.values(providersData.providers).map(p => (
358
+ <button
359
+ key={p.id}
360
+ onClick={() => {
361
+ setSelectedProvider(p.id);
362
+ saveProviderDefault(p.id, skipPermissions);
363
+ onProviderChange?.(p.id);
364
+ }}
365
+ className={`rounded-md px-3 py-1 text-xs font-medium transition-all ${
366
+ selectedProvider === p.id
367
+ ? 'border border-neon-blue-400/60 bg-neon-blue-400/15 text-neon-blue-400 shadow-[0_0_8px_rgba(0,191,255,0.2)]'
368
+ : 'border border-void-600 bg-void-800 text-void-400 hover:border-void-500 hover:text-void-300'
369
+ }`}
370
+ >
371
+ {p.displayName}
372
+ </button>
373
+ ))}
374
+ </div>
375
+ <div className="flex items-center gap-3">
376
+ {showModel && models && models.length > 0 && (
377
+ <div className="flex items-center gap-1.5">
378
+ <span className="text-xs text-void-500">Model</span>
379
+ <select
380
+ value={selectedModel}
381
+ onChange={(e) => { setSelectedModel(e.target.value); userSelectedModelRef.current = true; }}
382
+ className={`max-w-[140px] truncate rounded border px-2 py-1 text-xs font-medium transition-all ${
383
+ selectedModel
384
+ ? 'border-neon-blue-400/40 bg-neon-blue-400/10 text-neon-blue-400'
385
+ : 'border-void-600 bg-void-800 text-void-400'
386
+ }`}
387
+ >
388
+ <option value="">Default</option>
389
+ {models.map(m => (
390
+ <option key={m.id} value={m.id}>{m.label}</option>
391
+ ))}
392
+ </select>
393
+ </div>
394
+ )}
395
+ <label className="flex items-center gap-1.5 text-xs text-void-500 cursor-pointer">
396
+ <input
397
+ type="checkbox"
398
+ checked={skipPermissions}
399
+ onChange={(e) => { setSkipPermissions(e.target.checked); saveProviderDefault(selectedProvider, e.target.checked); }}
400
+ className="rounded border-void-600"
401
+ />
402
+ {providersData.providers[selectedProvider]?.permissions.label || 'Skip permissions'}
403
+ </label>
404
+ </div>
405
+ {instructionFileWarning}
406
+ </div>
407
+ );
408
+ };
409
+
291
410
  // Derive startup and toolbar lists from placement
292
411
  const startupActions = actions.filter(a => a.placement === 'startup' || a.placement === 'both');
293
412
  const toolbarActions = actions.filter(a => a.placement === 'toolbar' || a.placement === 'both');
@@ -317,6 +436,7 @@ export function ClaudeTerminalPanel({
317
436
  cwd,
318
437
  fresh: !hasHistory,
319
438
  prompt,
439
+ model: selectedModel || undefined,
320
440
  createInstructionFile: instructionFileCheck?.needed ? createInstructionFile : undefined,
321
441
  }),
322
442
  });
@@ -619,40 +739,7 @@ export function ClaudeTerminalPanel({
619
739
  Custom...
620
740
  </button>
621
741
  </div>
622
- {/* Provider selector */}
623
- {providersData && Object.keys(providersData.providers).length > 1 && (
624
- <div className="flex flex-col items-center gap-2 mt-3 pt-3 border-t border-void-700/50">
625
- <div className="flex gap-1">
626
- {Object.values(providersData.providers).map(p => (
627
- <button
628
- key={p.id}
629
- onClick={() => {
630
- setSelectedProvider(p.id);
631
- saveProviderDefault(p.id, skipPermissions);
632
- onProviderChange?.(p.id);
633
- }}
634
- className={`rounded-md px-3 py-1 text-xs font-medium transition-all ${
635
- selectedProvider === p.id
636
- ? 'border border-neon-blue-400/60 bg-neon-blue-400/15 text-neon-blue-400 shadow-[0_0_8px_rgba(0,191,255,0.2)]'
637
- : 'border border-void-600 bg-void-800 text-void-400 hover:border-void-500 hover:text-void-300'
638
- }`}
639
- >
640
- {p.displayName}
641
- </button>
642
- ))}
643
- </div>
644
- <label className="flex items-center gap-1.5 text-xs text-void-500 cursor-pointer">
645
- <input
646
- type="checkbox"
647
- checked={skipPermissions}
648
- onChange={(e) => { setSkipPermissions(e.target.checked); saveProviderDefault(selectedProvider, e.target.checked); }}
649
- className="rounded border-void-600"
650
- />
651
- {providersData.providers[selectedProvider]?.permissions.label || 'Skip permissions'}
652
- </label>
653
- {instructionFileWarning}
654
- </div>
655
- )}
742
+ {renderProviderSelector({ showModel: false })}
656
743
  </>
657
744
  ) : (
658
745
  <>
@@ -684,40 +771,7 @@ export function ClaudeTerminalPanel({
684
771
  >
685
772
  Start without prompt
686
773
  </button>
687
- {/* Provider selector */}
688
- {providersData && Object.keys(providersData.providers).length > 1 && (
689
- <div className="flex flex-col items-center gap-2 mt-3 pt-3 border-t border-void-700/50">
690
- <div className="flex gap-1">
691
- {Object.values(providersData.providers).map(p => (
692
- <button
693
- key={p.id}
694
- onClick={() => {
695
- setSelectedProvider(p.id);
696
- saveProviderDefault(p.id, skipPermissions);
697
- onProviderChange?.(p.id);
698
- }}
699
- className={`rounded-md px-3 py-1 text-xs font-medium transition-all ${
700
- selectedProvider === p.id
701
- ? 'border border-neon-blue-400/60 bg-neon-blue-400/15 text-neon-blue-400 shadow-[0_0_8px_rgba(0,191,255,0.2)]'
702
- : 'border border-void-600 bg-void-800 text-void-400 hover:border-void-500 hover:text-void-300'
703
- }`}
704
- >
705
- {p.displayName}
706
- </button>
707
- ))}
708
- </div>
709
- <label className="flex items-center gap-1.5 text-xs text-void-500 cursor-pointer">
710
- <input
711
- type="checkbox"
712
- checked={skipPermissions}
713
- onChange={(e) => { setSkipPermissions(e.target.checked); saveProviderDefault(selectedProvider, e.target.checked); }}
714
- className="rounded border-void-600"
715
- />
716
- {providersData.providers[selectedProvider]?.permissions.label || 'Skip permissions'}
717
- </label>
718
- {instructionFileWarning}
719
- </div>
720
- )}
774
+ {renderProviderSelector()}
721
775
  </>
722
776
  )}
723
777
  </div>
@@ -7,6 +7,20 @@
7
7
 
8
8
  import path from 'path';
9
9
  import fs from 'fs';
10
+ import os from 'os';
11
+
12
+ /**
13
+ * Expand tilde (~) in a path to the user's home directory.
14
+ * Also normalizes Unicode tildes (U+02DC, U+FF5E) that Mac browsers can produce.
15
+ */
16
+ export function expandTilde(p: string): string {
17
+ // Normalize Unicode tildes (U+02DC small tilde, U+FF5E fullwidth tilde) to ASCII
18
+ p = p.replace(/^[\u02dc\uff5e]/, '~');
19
+ if (p.startsWith('~/') || p === '~') {
20
+ return p.replace(/^~/, os.homedir());
21
+ }
22
+ return p;
23
+ }
10
24
 
11
25
  /**
12
26
  * Resolve the SlyCode root directory (workspace).