@slycode/slycode 0.2.13 → 0.2.15

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 (75) hide show
  1. package/dist/bridge/claude-utils.d.ts +2 -3
  2. package/dist/bridge/claude-utils.js +28 -17
  3. package/dist/bridge/claude-utils.js.map +1 -1
  4. package/dist/bridge/pty-handler.js +40 -0
  5. package/dist/bridge/pty-handler.js.map +1 -1
  6. package/dist/bridge/session-manager.js +28 -3
  7. package/dist/bridge/session-manager.js.map +1 -1
  8. package/dist/bridge/types.d.ts +3 -0
  9. package/dist/scripts/kanban.js +26 -1
  10. package/dist/scripts/slycode-env-wrapper.sh +10 -1
  11. package/dist/web/.next/BUILD_ID +1 -1
  12. package/dist/web/.next/build-manifest.json +2 -2
  13. package/dist/web/.next/server/app/_global-error.html +2 -2
  14. package/dist/web/.next/server/app/_global-error.rsc +1 -1
  15. package/dist/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  16. package/dist/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  17. package/dist/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/dist/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/dist/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/dist/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  21. package/dist/web/.next/server/app/_not-found.html +1 -1
  22. package/dist/web/.next/server/app/_not-found.rsc +2 -2
  23. package/dist/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  24. package/dist/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  25. package/dist/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  26. package/dist/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  28. package/dist/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  29. package/dist/web/.next/server/app/page/react-loadable-manifest.json +1 -1
  30. package/dist/web/.next/server/app/page_client-reference-manifest.js +1 -1
  31. package/dist/web/.next/server/app/project/[id]/page/react-loadable-manifest.json +1 -1
  32. package/dist/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
  33. package/dist/web/.next/server/chunks/ssr/[root-of-the-server]__1f5fc489._.js +1 -1
  34. package/dist/web/.next/server/chunks/ssr/[root-of-the-server]__bcbe4bf2._.js +1 -1
  35. package/dist/web/.next/server/pages/404.html +1 -1
  36. package/dist/web/.next/server/pages/500.html +2 -2
  37. package/dist/web/.next/static/chunks/{4d5e1b83ce67acfa.js → 0a4b215957655f38.js} +1 -1
  38. package/dist/web/.next/static/chunks/2744d72103f49934.css +1 -0
  39. package/dist/web/.next/static/chunks/{29b5d391d655a999.js → 68a737843232927b.js} +2 -2
  40. package/dist/web/.next/static/chunks/7b456abf0afeeb60.js +4 -0
  41. package/dist/web/.next/static/chunks/{24c966c8b9e12be5.js → c7a853519f3ebcb8.js} +1 -1
  42. package/dist/web/.next/static/chunks/{02a9b2dcd9d8b87c.js → e52d73ad4544e983.js} +1 -1
  43. package/dist/web/src/components/ClaudeTerminalPanel.tsx +60 -1
  44. package/dist/web/src/components/Terminal.tsx +2 -2
  45. package/dist/web/tsconfig.tsbuildinfo +1 -1
  46. package/lib/cli/restart.d.ts.map +1 -1
  47. package/lib/cli/restart.js +5 -9
  48. package/lib/cli/restart.js.map +1 -1
  49. package/lib/cli/start.d.ts.map +1 -1
  50. package/lib/cli/start.js +38 -10
  51. package/lib/cli/start.js.map +1 -1
  52. package/lib/cli/update.js +1 -1
  53. package/lib/cli/update.js.map +1 -1
  54. package/lib/platform/service-common.d.ts +24 -0
  55. package/lib/platform/service-common.d.ts.map +1 -0
  56. package/lib/platform/service-common.js +119 -0
  57. package/lib/platform/service-common.js.map +1 -0
  58. package/lib/platform/service-detect.d.ts.map +1 -1
  59. package/lib/platform/service-detect.js +32 -20
  60. package/lib/platform/service-detect.js.map +1 -1
  61. package/lib/platform/service-linux.d.ts +1 -1
  62. package/lib/platform/service-linux.d.ts.map +1 -1
  63. package/lib/platform/service-linux.js +10 -77
  64. package/lib/platform/service-linux.js.map +1 -1
  65. package/lib/platform/service-macos.d.ts +1 -1
  66. package/lib/platform/service-macos.d.ts.map +1 -1
  67. package/lib/platform/service-macos.js +176 -49
  68. package/lib/platform/service-macos.js.map +1 -1
  69. package/package.json +1 -1
  70. package/templates/kanban-seed.json +1 -1
  71. package/dist/web/.next/static/chunks/17bf7ad67057dc74.js +0 -4
  72. package/dist/web/.next/static/chunks/3df3846316317676.css +0 -1
  73. /package/dist/web/.next/static/{5LMWBFPioBVfTpJ4F5OT6 → du1bb9CKBc_rQF_UArbIP}/_buildManifest.js +0 -0
  74. /package/dist/web/.next/static/{5LMWBFPioBVfTpJ4F5OT6 → du1bb9CKBc_rQF_UArbIP}/_clientMiddlewareManifest.json +0 -0
  75. /package/dist/web/.next/static/{5LMWBFPioBVfTpJ4F5OT6 → du1bb9CKBc_rQF_UArbIP}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,33525,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"warnOnce",{enumerable:!0,get:function(){return s}});let s=e=>{}},87365,e=>{"use strict";let t="slycode:terminal-prompt";function n(e,s=!0){let i=new CustomEvent(t,{detail:{prompt:e,autoSubmit:s}});window.dispatchEvent(i)}function s(e){let n=t=>{e(t.detail)};return window.addEventListener(t,n),()=>window.removeEventListener(t,n)}e.s(["onTerminalPrompt",()=>s,"pushToTerminal",()=>n])},21146,e=>{"use strict";function t(...e){"1"===localStorage.getItem("cm-debug")&&console.log(`[CM ${new Date().toISOString().slice(11,23)}]`,...e)}class n{connections=new Map;statusListeners=new Set;_status="connected";connectionIdCounter=0;isPageVisible=!0;visibilityChangeTime=null;healthCheckInterval=null;lastHealthCheckSuccess=null;consecutiveHealthFailures=0;statusDowngradeTimeout=null;constructor(){this.setupVisibilityListener(),this.setupNetworkListener(),this.setupHealthCheck()}setupVisibilityListener(){document.addEventListener("visibilitychange",()=>{let e=this.isPageVisible;this.isPageVisible="visible"===document.visibilityState,!e&&this.isPageVisible?(this.visibilityChangeTime=Date.now(),this.handleTabWake()):e&&!this.isPageVisible&&(this.visibilityChangeTime=Date.now())})}setupNetworkListener(){window.addEventListener("online",()=>{this.doHealthCheck()}),window.addEventListener("offline",()=>{this.updateStatus("disconnected")})}setupHealthCheck(){this.healthCheckInterval=setInterval(()=>{if(this.isPageVisible&&0!==this.connections.size){if(this.hasRecentlyActiveConnection()){this.consecutiveHealthFailures=0,"connected"!==this._status&&this.reconnectBroken();return}this.doHealthCheck()}},2e4)}hasRecentlyActiveConnection(){let e=Date.now();for(let t of this.connections.values())if(t.eventSource?.readyState===EventSource.OPEN&&t.lastConnected&&e-t.lastConnected<6e4)return!0;return!1}async doHealthCheck(){t("HEALTH_CHECK — fetching /api/bridge/health");try{let e=new AbortController,n=setTimeout(()=>e.abort(),3e3),s=await fetch("/api/bridge/health",{method:"GET",signal:e.signal});clearTimeout(n),s.ok?(t("HEALTH_CHECK — OK"),this.consecutiveHealthFailures=0,this.lastHealthCheckSuccess=Date.now(),"connected"!==this._status&&this.reconnectBroken()):(t(`HEALTH_CHECK — FAIL status=${s.status}`),this.handleHealthCheckFailure())}catch(e){t(`HEALTH_CHECK — ERROR`,e),this.handleHealthCheckFailure()}}handleHealthCheckFailure(){this.consecutiveHealthFailures++,t(`HEALTH_FAIL — consecutive=${this.consecutiveHealthFailures}`),this.consecutiveHealthFailures>=2&&"connected"===this._status&&this.updateStatus("disconnected")}handleTabWake(){t("TAB_WAKE — force-reconnecting all connections"),this.reconnectAll(!0)}calculateBackoff(e){let t=Math.min(1e3*Math.pow(2,e),3e4),n=.2*t*(2*Math.random()-1);return Math.round(t+n)}createManagedEventSource(e,t){let n=`conn-${++this.connectionIdCounter}`;return this.connections.set(n,{url:e,handlers:t,eventSource:null,retryCount:0,retryTimeout:null,lastConnected:null,id:n}),this.connect(n),n}connect(e){let n=this.connections.get(e);if(n){n.eventSource&&(n.eventSource.close(),n.eventSource=null),n.retryTimeout&&(clearTimeout(n.retryTimeout),n.retryTimeout=null);try{let s=new EventSource(n.url);n.eventSource=s,s.onopen=()=>{n.retryCount=0,n.lastConnected=Date.now(),this.consecutiveHealthFailures=0,t(`OPEN ${e} → ${n.url}`),this.updateOverallStatus(),n.handlers.onOpen?.()},s.onerror=i=>{t(`ERROR ${e} → ${n.url} readyState=${s.readyState} (0=CONNECTING, 1=OPEN, 2=CLOSED)`,i),n.handlers.onError?.(i),s.readyState===EventSource.CLOSED&&(t(`CLOSED ${e} — scheduling reconnect`),this.scheduleReconnect(e))},s.onmessage=e=>{n.lastConnected=Date.now(),n.handlers.onMessage?.(e)},s.addEventListener("heartbeat",()=>{n.lastConnected=Date.now(),t(`HEARTBEAT ${e} → ${n.url}`)}),Object.entries(n.handlers).forEach(([e,t])=>{"onOpen"!==e&&"onError"!==e&&"onMessage"!==e&&t&&s.addEventListener(e,e=>{n.lastConnected=Date.now(),t(e)})})}catch(t){console.error(`ConnectionManager: Failed to create EventSource for ${n.url}`,t),this.scheduleReconnect(e)}}}scheduleReconnect(e){let t=this.connections.get(e);if(!t)return;t.retryTimeout&&clearTimeout(t.retryTimeout);let n=this.calculateBackoff(t.retryCount);t.retryCount++,this.updateOverallStatus(),t.retryTimeout=setTimeout(()=>{t.retryTimeout=null,this.connect(e)},n)}reconnect(e,t=!1){let n=this.connections.get(e);n&&(t?(n.retryCount=0,this.updateStatus("reconnecting"),this.connect(e)):this.scheduleReconnect(e))}reconnectAll(e=!1){this.connections.size>0&&this.updateStatus("reconnecting"),this.connections.forEach((t,n)=>{this.reconnect(n,e)})}reconnectBroken(){let e=!1;this.connections.forEach((t,n)=>{t.eventSource?.readyState!==EventSource.OPEN&&(e=!0,this.reconnect(n,!0))}),e||this.updateOverallStatus()}closeConnection(e){let t=this.connections.get(e);t&&(t.retryTimeout&&clearTimeout(t.retryTimeout),t.eventSource&&t.eventSource.close(),this.connections.delete(e),this.updateOverallStatus())}updateOverallStatus(){if(0===this.connections.size){this.updateStatus("connected"),this.clearDowngradeTimeout();return}let e=0,t=0;this.connections.forEach(n=>{n.eventSource?.readyState===EventSource.OPEN?e++:(n.retryTimeout||n.eventSource?.readyState===EventSource.CONNECTING)&&t++}),e>0?(this.updateStatus("connected"),this.clearDowngradeTimeout()):t>0?this.scheduleDowngrade("reconnecting"):this.scheduleDowngrade("disconnected")}scheduleDowngrade(e){"connected"!==this._status?this.updateStatus(e):this.statusDowngradeTimeout||(this.statusDowngradeTimeout=setTimeout(()=>{this.statusDowngradeTimeout=null,this.updateOverallStatus()},3e3))}clearDowngradeTimeout(){this.statusDowngradeTimeout&&(clearTimeout(this.statusDowngradeTimeout),this.statusDowngradeTimeout=null)}updateStatus(e){if(this._status!==e){let n=Array.from(this.connections.values()).map(e=>({id:e.id,url:e.url.replace(/.*\/api\//,"/api/"),readyState:e.eventSource?.readyState,lastConnectedAge:e.lastConnected?Math.round((Date.now()-e.lastConnected)/1e3)+"s":"never",hasRetryPending:!!e.retryTimeout}));t(`STATUS ${this._status} → ${e}`,JSON.stringify(n,null,2)),this._status=e,this.statusListeners.forEach(t=>t(e))}}subscribe(e){return this.statusListeners.add(e),e(this._status),()=>{this.statusListeners.delete(e)}}get status(){return this._status}get pageVisible(){return this.isPageVisible}get connectionCount(){return this.connections.size}}let s=new n;e.s(["connectionManager",0,s])},54423,e=>{e.v(t=>Promise.all(["static/chunks/54d5670f5fa2abbe.css","static/chunks/4d5e1b83ce67acfa.js"].map(t=>e.l(t))).then(()=>t(81485)))},90178,e=>{e.v(e=>Promise.resolve().then(()=>e(87365)))}]);
1
+ (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,33525,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"warnOnce",{enumerable:!0,get:function(){return s}});let s=e=>{}},87365,e=>{"use strict";let t="slycode:terminal-prompt";function n(e,s=!0){let i=new CustomEvent(t,{detail:{prompt:e,autoSubmit:s}});window.dispatchEvent(i)}function s(e){let n=t=>{e(t.detail)};return window.addEventListener(t,n),()=>window.removeEventListener(t,n)}e.s(["onTerminalPrompt",()=>s,"pushToTerminal",()=>n])},21146,e=>{"use strict";function t(...e){"1"===localStorage.getItem("cm-debug")&&console.log(`[CM ${new Date().toISOString().slice(11,23)}]`,...e)}class n{connections=new Map;statusListeners=new Set;_status="connected";connectionIdCounter=0;isPageVisible=!0;visibilityChangeTime=null;healthCheckInterval=null;lastHealthCheckSuccess=null;consecutiveHealthFailures=0;statusDowngradeTimeout=null;constructor(){this.setupVisibilityListener(),this.setupNetworkListener(),this.setupHealthCheck()}setupVisibilityListener(){document.addEventListener("visibilitychange",()=>{let e=this.isPageVisible;this.isPageVisible="visible"===document.visibilityState,!e&&this.isPageVisible?(this.visibilityChangeTime=Date.now(),this.handleTabWake()):e&&!this.isPageVisible&&(this.visibilityChangeTime=Date.now())})}setupNetworkListener(){window.addEventListener("online",()=>{this.doHealthCheck()}),window.addEventListener("offline",()=>{this.updateStatus("disconnected")})}setupHealthCheck(){this.healthCheckInterval=setInterval(()=>{if(this.isPageVisible&&0!==this.connections.size){if(this.hasRecentlyActiveConnection()){this.consecutiveHealthFailures=0,"connected"!==this._status&&this.reconnectBroken();return}this.doHealthCheck()}},2e4)}hasRecentlyActiveConnection(){let e=Date.now();for(let t of this.connections.values())if(t.eventSource?.readyState===EventSource.OPEN&&t.lastConnected&&e-t.lastConnected<6e4)return!0;return!1}async doHealthCheck(){t("HEALTH_CHECK — fetching /api/bridge/health");try{let e=new AbortController,n=setTimeout(()=>e.abort(),3e3),s=await fetch("/api/bridge/health",{method:"GET",signal:e.signal});clearTimeout(n),s.ok?(t("HEALTH_CHECK — OK"),this.consecutiveHealthFailures=0,this.lastHealthCheckSuccess=Date.now(),"connected"!==this._status&&this.reconnectBroken()):(t(`HEALTH_CHECK — FAIL status=${s.status}`),this.handleHealthCheckFailure())}catch(e){t(`HEALTH_CHECK — ERROR`,e),this.handleHealthCheckFailure()}}handleHealthCheckFailure(){this.consecutiveHealthFailures++,t(`HEALTH_FAIL — consecutive=${this.consecutiveHealthFailures}`),this.consecutiveHealthFailures>=2&&"connected"===this._status&&this.updateStatus("disconnected")}handleTabWake(){t("TAB_WAKE — force-reconnecting all connections"),this.reconnectAll(!0)}calculateBackoff(e){let t=Math.min(1e3*Math.pow(2,e),3e4),n=.2*t*(2*Math.random()-1);return Math.round(t+n)}createManagedEventSource(e,t){let n=`conn-${++this.connectionIdCounter}`;return this.connections.set(n,{url:e,handlers:t,eventSource:null,retryCount:0,retryTimeout:null,lastConnected:null,id:n}),this.connect(n),n}connect(e){let n=this.connections.get(e);if(n){n.eventSource&&(n.eventSource.close(),n.eventSource=null),n.retryTimeout&&(clearTimeout(n.retryTimeout),n.retryTimeout=null);try{let s=new EventSource(n.url);n.eventSource=s,s.onopen=()=>{n.retryCount=0,n.lastConnected=Date.now(),this.consecutiveHealthFailures=0,t(`OPEN ${e} → ${n.url}`),this.updateOverallStatus(),n.handlers.onOpen?.()},s.onerror=i=>{t(`ERROR ${e} → ${n.url} readyState=${s.readyState} (0=CONNECTING, 1=OPEN, 2=CLOSED)`,i),n.handlers.onError?.(i),s.readyState===EventSource.CLOSED&&(t(`CLOSED ${e} — scheduling reconnect`),this.scheduleReconnect(e))},s.onmessage=e=>{n.lastConnected=Date.now(),n.handlers.onMessage?.(e)},s.addEventListener("heartbeat",()=>{n.lastConnected=Date.now(),t(`HEARTBEAT ${e} → ${n.url}`)}),Object.entries(n.handlers).forEach(([e,t])=>{"onOpen"!==e&&"onError"!==e&&"onMessage"!==e&&t&&s.addEventListener(e,e=>{n.lastConnected=Date.now(),t(e)})})}catch(t){console.error(`ConnectionManager: Failed to create EventSource for ${n.url}`,t),this.scheduleReconnect(e)}}}scheduleReconnect(e){let t=this.connections.get(e);if(!t)return;t.retryTimeout&&clearTimeout(t.retryTimeout);let n=this.calculateBackoff(t.retryCount);t.retryCount++,this.updateOverallStatus(),t.retryTimeout=setTimeout(()=>{t.retryTimeout=null,this.connect(e)},n)}reconnect(e,t=!1){let n=this.connections.get(e);n&&(t?(n.retryCount=0,this.updateStatus("reconnecting"),this.connect(e)):this.scheduleReconnect(e))}reconnectAll(e=!1){this.connections.size>0&&this.updateStatus("reconnecting"),this.connections.forEach((t,n)=>{this.reconnect(n,e)})}reconnectBroken(){let e=!1;this.connections.forEach((t,n)=>{t.eventSource?.readyState!==EventSource.OPEN&&(e=!0,this.reconnect(n,!0))}),e||this.updateOverallStatus()}closeConnection(e){let t=this.connections.get(e);t&&(t.retryTimeout&&clearTimeout(t.retryTimeout),t.eventSource&&t.eventSource.close(),this.connections.delete(e),this.updateOverallStatus())}updateOverallStatus(){if(0===this.connections.size){this.updateStatus("connected"),this.clearDowngradeTimeout();return}let e=0,t=0;this.connections.forEach(n=>{n.eventSource?.readyState===EventSource.OPEN?e++:(n.retryTimeout||n.eventSource?.readyState===EventSource.CONNECTING)&&t++}),e>0?(this.updateStatus("connected"),this.clearDowngradeTimeout()):t>0?this.scheduleDowngrade("reconnecting"):this.scheduleDowngrade("disconnected")}scheduleDowngrade(e){"connected"!==this._status?this.updateStatus(e):this.statusDowngradeTimeout||(this.statusDowngradeTimeout=setTimeout(()=>{this.statusDowngradeTimeout=null,this.updateOverallStatus()},3e3))}clearDowngradeTimeout(){this.statusDowngradeTimeout&&(clearTimeout(this.statusDowngradeTimeout),this.statusDowngradeTimeout=null)}updateStatus(e){if(this._status!==e){let n=Array.from(this.connections.values()).map(e=>({id:e.id,url:e.url.replace(/.*\/api\//,"/api/"),readyState:e.eventSource?.readyState,lastConnectedAge:e.lastConnected?Math.round((Date.now()-e.lastConnected)/1e3)+"s":"never",hasRetryPending:!!e.retryTimeout}));t(`STATUS ${this._status} → ${e}`,JSON.stringify(n,null,2)),this._status=e,this.statusListeners.forEach(t=>t(e))}}subscribe(e){return this.statusListeners.add(e),e(this._status),()=>{this.statusListeners.delete(e)}}get status(){return this._status}get pageVisible(){return this.isPageVisible}get connectionCount(){return this.connections.size}}let s=new n;e.s(["connectionManager",0,s])},54423,e=>{e.v(t=>Promise.all(["static/chunks/54d5670f5fa2abbe.css","static/chunks/0a4b215957655f38.js"].map(t=>e.l(t))).then(()=>t(81485)))},90178,e=>{e.v(e=>Promise.resolve().then(()=>e(87365)))}]);
@@ -140,6 +140,10 @@ export function ClaudeTerminalPanel({
140
140
  const [screenshotToast, setScreenshotToast] = useState<{ filename: string; previewUrl: string; status: 'uploading' | 'done' | 'error'; message?: string } | null>(null);
141
141
  const screenshotToastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
142
142
  const imagePasteInProgressRef = useRef(false);
143
+ // Exit output toast state (persists across terminal unmount)
144
+ const [exitToast, setExitToast] = useState<{ code: number; output: string } | null>(null);
145
+ // Spawn error toast — shown when session creation fails (e.g. posix_spawnp failed)
146
+ const [spawnError, setSpawnError] = useState<string | null>(null);
143
147
 
144
148
  // Instruction file check state
145
149
  const [instructionFileCheck, setInstructionFileCheck] = useState<{ needed: boolean; targetFile?: string; copySource?: string } | null>(null);
@@ -291,6 +295,8 @@ export function ClaudeTerminalPanel({
291
295
  const startSession = async (command?: SlyActionItem | { prompt: string } | null, customPromptText?: string) => {
292
296
  setIsStarting(true);
293
297
  setShowCustomPrompt(false);
298
+ setExitToast(null);
299
+ setSpawnError(null);
294
300
  try {
295
301
  // Build prompt if action provided — context is opt-in via {{cardContext}} etc.
296
302
  let prompt: string | undefined;
@@ -325,9 +331,13 @@ export function ClaudeTerminalPanel({
325
331
  if (instructionFileCheck?.needed && createInstructionFile) {
326
332
  setInstructionFileCheck(null);
327
333
  }
334
+ } else {
335
+ const body = await res.json().catch(() => ({ error: 'Unknown error' }));
336
+ setSpawnError(body.error || `Failed to start session (HTTP ${res.status})`);
328
337
  }
329
338
  } catch (err) {
330
339
  console.error('Failed to start session:', err);
340
+ setSpawnError('Could not reach the bridge server');
331
341
  } finally {
332
342
  setIsStarting(false);
333
343
  setCustomPrompt('');
@@ -386,9 +396,12 @@ export function ClaudeTerminalPanel({
386
396
  }
387
397
  };
388
398
 
389
- const handleSessionExit = () => {
399
+ const handleSessionExit = (code: number, output?: string) => {
390
400
  setShowTerminal(false);
391
401
  setIsConnected(false);
402
+ if (output && code !== 0) {
403
+ setExitToast({ code, output });
404
+ }
392
405
  fetchSessionInfo();
393
406
  };
394
407
 
@@ -753,6 +766,52 @@ export function ClaudeTerminalPanel({
753
766
  </div>
754
767
  )}
755
768
 
769
+ {/* Exit output toast — persists after terminal unmounts */}
770
+ {exitToast && (
771
+ <div className="absolute bottom-3 right-3 left-3 z-50 rounded-lg border border-red-500/30 bg-void-800/95 shadow-(--shadow-overlay) backdrop-blur-sm">
772
+ <div className="flex items-start justify-between gap-2 px-3 py-2">
773
+ <div className="flex items-center gap-1.5 text-xs font-medium text-red-400">
774
+ <svg className="h-3.5 w-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
775
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
776
+ </svg>
777
+ Session exited (code: {exitToast.code})
778
+ </div>
779
+ <button
780
+ onClick={() => setExitToast(null)}
781
+ className="text-void-500 hover:text-void-300 flex-shrink-0"
782
+ >
783
+ <svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
784
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
785
+ </svg>
786
+ </button>
787
+ </div>
788
+ <pre className="max-h-32 overflow-auto px-3 pb-2 text-xs text-void-300 font-mono whitespace-pre-wrap">{exitToast.output}</pre>
789
+ </div>
790
+ )}
791
+
792
+ {/* Spawn error toast — shown when session creation fails entirely */}
793
+ {spawnError && (
794
+ <div className="absolute bottom-3 right-3 left-3 z-50 rounded-lg border border-red-500/30 bg-void-800/95 shadow-(--shadow-overlay) backdrop-blur-sm">
795
+ <div className="flex items-start justify-between gap-2 px-3 py-2">
796
+ <div className="flex items-center gap-1.5 text-xs font-medium text-red-400">
797
+ <svg className="h-3.5 w-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
798
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
799
+ </svg>
800
+ Failed to start session
801
+ </div>
802
+ <button
803
+ onClick={() => setSpawnError(null)}
804
+ className="text-void-500 hover:text-void-300 flex-shrink-0"
805
+ >
806
+ <svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
807
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
808
+ </svg>
809
+ </button>
810
+ </div>
811
+ <pre className="max-h-32 overflow-auto px-3 pb-2 text-xs text-void-300 font-mono whitespace-pre-wrap">{spawnError}</pre>
812
+ </div>
813
+ )}
814
+
756
815
  {/* Footer controls - only when running */}
757
816
  {isRunning && (
758
817
  <div className={`flex flex-shrink-0 items-center gap-2 px-3 py-2 ${footerClassName || 'border-t border-void-700 bg-void-800'}`}>
@@ -17,7 +17,7 @@ interface TerminalProps {
17
17
  bridgeUrl?: string;
18
18
  tintColor?: string;
19
19
  onConnectionChange?: (connected: boolean) => void;
20
- onSessionExit?: (code: number) => void;
20
+ onSessionExit?: (code: number, output?: string) => void;
21
21
  onReady?: (handle: TerminalHandle) => void;
22
22
  onImagePaste?: (file: File) => void;
23
23
  }
@@ -375,7 +375,7 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
375
375
  try {
376
376
  const msg = JSON.parse(event.data);
377
377
  terminal.write(`\r\n\x1b[33mSession exited (code: ${msg.code})\x1b[0m\r\n`);
378
- onSessionExitRef.current?.(msg.code);
378
+ onSessionExitRef.current?.(msg.code, msg.output);
379
379
  } catch {
380
380
  terminal.write('\r\n\x1b[33mSession exited\x1b[0m\r\n');
381
381
  }