Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions specs/069-drawer-deploy-state/evidence/unit-test-results.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@

 RUN  v4.0.18 /Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-drawer-deploy-state

stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > deploy() > sets deployError when server action returns error
[useDeployAction] deploy failed: No dev script found

stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > deploy() > auto-clears deployError after 5 seconds
[useDeployAction] deploy failed: No dev script found

stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > deploy() > catches thrown errors and sets deployError
[useDeployAction] deploy threw exception: Network error Error: Network error
at /Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-drawer-deploy-state/tests/unit/presentation/web/hooks/use-deploy-action.test.ts:138:43
at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-drawer-deploy-state/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/index.js:145:11
at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-drawer-deploy-state/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/index.js:915:26
at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-drawer-deploy-state/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/index.js:1243:20
at new Promise (<anonymous>)
at runWithTimeout (file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-drawer-deploy-state/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/index.js:1209:10)
at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-drawer-deploy-state/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/index.js:1653:37
at Traces.$ (file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-drawer-deploy-state/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected][email protected][email protected]/node_modules/vitest/dist/chunks/traces.CCmnQaNT.js:142:27)
at trace (file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-drawer-deploy-state/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected][email protected][email protected]/node_modules/vitest/dist/chunks/test.B8ej_ZHS.js:239:21)
at runTest (file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-drawer-deploy-state/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/index.js:1653:12)

stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > deploy() > is no-op when input is null
[useDeployAction] deploy() called but input is null — no-op

stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > deploy() > is no-op while deployLoading is true
[useDeployAction] deploy() called but already loading — no-op

stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > polling > does not start polling when deploy fails
[useDeployAction] deploy failed: No dev script

stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > cleanup > cleans up error timer on unmount
[useDeployAction] deploy failed: Failed

✓  web  tests/unit/presentation/web/hooks/use-deploy-action.test.ts (25 tests) 33ms
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
Not implemented: HTMLMediaElement's pause() method
✓  web  tests/unit/presentation/web/features/control-center/control-center.test.tsx (3 tests) 75ms

 Test Files  2 passed (2)
 Tests  28 passed (28)
 Start at  19:11:37
 Duration  1.01s (transform 337ms, setup 167ms, import 560ms, tests 108ms, environment 595ms)

33 changes: 33 additions & 0 deletions specs/069-drawer-deploy-state/feature.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
feature:
id: 069-drawer-deploy-state
name: drawer-deploy-state
number: 69
branch: feat/069-drawer-deploy-state
lifecycle: research
createdAt: '2026-03-16T16:54:53Z'
status:
phase: research
progress:
completed: 0
total: 0
percentage: 0
currentTask: null
lastUpdated: '2026-03-16T16:54:53Z'
lastUpdatedBy: feature-agent
completedPhases:
- fast-implement
validation:
lastRun: null
gatesPassed: []
autoFixesApplied: []
tasks:
current: null
blocked: []
failed: []
checkpoints:
- phase: feature-created
completedAt: '2026-03-16T16:54:53Z'
completedBy: feature-agent
errors:
current: null
history: []
51 changes: 51 additions & 0 deletions specs/069-drawer-deploy-state/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Feature Specification (YAML)
# This is the source of truth. Markdown is auto-generated from this file.

name: drawer-deploy-state
number: 069
branch: feat/069-drawer-deploy-state
oneLiner: Dev server is getting "lost" from the feature drawer
userQuery: >
Dev server is getting "lost" from the feature drawer
summary: >
Dev server is getting "lost" from the feature drawer
phase: Analysis
sizeEstimate: M

# Relationships
relatedFeatures: []

technologies: []

relatedLinks: []

# Open questions (must be resolved before implementation)
openQuestions: []

# Markdown content (the actual spec)
content: |
## Problem Statement

Dev server is getting "lost" from the feature drawer

## Success Criteria

- [ ] TBD

## Affected Areas

| Area | Impact | Reasoning |
| ---- | ------ | --------- |
| TBD | TBD | TBD |

## Dependencies

None identified.

## Size Estimate

**M** - To be refined during research

---

_Generated by feature agent — proceed with research_
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { toast } from 'sonner';
import { Loader2, Trash2, Play, Square, Copy, Check, Code2, ExternalLink } from 'lucide-react';
import { Loader2, Trash2, Copy, Check, Code2, ExternalLink } from 'lucide-react';
import type {
PrdApprovalPayload,
QuestionSelectionChange,
Expand All @@ -19,15 +19,12 @@ import { getMergeReviewData } from '@/app/actions/get-merge-review-data';
import { useFeatureFlags } from '@/hooks/feature-flags-context';
import { useSoundAction } from '@/hooks/use-sound-action';
import { useGuardedDrawerClose } from '@/hooks/drawer-close-guard';
import { useDeployAction } from '@/hooks/use-deploy-action';
import type { DeployActionInput } from '@/hooks/use-deploy-action';
import { useAgentEventsContext } from '@/hooks/agent-events-provider';
import { BaseDrawer } from '@/components/common/base-drawer';
import { DeploymentStatusBadge } from '@/components/common/deployment-status-badge';
import { DrawerTitle, DrawerDescription } from '@/components/ui/drawer';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { DeleteFeatureDialog } from '@/components/common/delete-feature-dialog';
import { ActionButton } from '@/components/common/action-button';
import { OpenActionMenu } from '@/components/common/open-action-menu';
import { FeatureDrawerTabs } from '@/components/common/feature-drawer-tabs';
import { useFeatureActions } from '@/components/common/feature-drawer/use-feature-actions';
Expand Down Expand Up @@ -424,19 +421,15 @@ export function FeatureDrawerClient({ view: initialView, urlTab }: FeatureDrawer
: null;
const featureActions = useFeatureActions(featureActionsInput);

const featureDeployTarget =
const featureDeployTarget: DeployActionInput | undefined =
featureNode?.repositoryPath && featureNode.branch
? {
targetId: featureNode.featureId,
targetType: 'feature' as const,
repositoryPath: featureNode.repositoryPath,
branch: featureNode.branch,
}
: null;

const deployAction = useDeployAction(featureDeployTarget);
const isFeatureDeployActive =
deployAction.status === 'Booting' || deployAction.status === 'Ready';
: undefined;

// ── Short ID copy ───────────────────────────────────────────────────
const COPY_FEEDBACK_DELAY = 2000;
Expand Down Expand Up @@ -491,38 +484,6 @@ export function FeatureDrawerClient({ view: initialView, urlTab }: FeatureDrawer
repositoryPath={featureActionsInput.repositoryPath}
showSpecs={!!featureActionsInput.specPath}
/>
{featureFlags.envDeploy && featureDeployTarget ? (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<ActionButton
label={isFeatureDeployActive ? 'Stop Dev Server' : 'Start Dev Server'}
onClick={isFeatureDeployActive ? deployAction.stop : deployAction.deploy}
loading={deployAction.deployLoading || deployAction.stopLoading}
error={!!deployAction.deployError}
icon={isFeatureDeployActive ? Square : Play}
iconOnly
variant="outline"
size="icon-sm"
/>
</span>
</TooltipTrigger>
<TooltipContent>
{isFeatureDeployActive ? 'Stop Dev Server' : 'Start Dev Server'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
{isFeatureDeployActive ? (
<DeploymentStatusBadge
status={deployAction.status}
url={deployAction.url}
targetId={featureDeployTarget?.targetId}
/>
) : null}
</>
) : null}
<div className="ml-auto flex items-center gap-1.5">
<code className="bg-muted text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs">
{shortId}
Expand Down Expand Up @@ -623,6 +584,7 @@ export function FeatureDrawerClient({ view: initialView, urlTab }: FeatureDrawer
size="md"
modal={false}
header={header}
deployTarget={featureFlags.envDeploy ? featureDeployTarget : undefined}
data-testid={view.type === 'feature' ? 'feature-drawer' : 'repository-drawer'}
>
{body}
Expand Down
38 changes: 38 additions & 0 deletions src/presentation/web/hooks/use-deploy-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,41 @@ export function useDeployAction(input: DeployActionInput | null): DeployActionSt
};
}, []);

// Fetch initial deployment status on mount (or when input changes) so we
// pick up any already-running dev server that was started before this
// component mounted. Without this, closing and reopening the drawer would
// lose awareness of the running deployment.
const pollAfterInitialFetchRef = useRef<((id: string) => void) | null>(null);
const initialFetchIdRef = useRef<string | null>(null);
useEffect(() => {
const targetId = input?.targetId ?? null;
// Only fetch once per targetId to avoid redundant calls
if (targetId === initialFetchIdRef.current) return;
initialFetchIdRef.current = targetId;

if (!targetId) return;

let cancelled = false;

getDeploymentStatus(targetId).then((result) => {
if (cancelled || !mountedRef.current) return;

if (result && result.state !== 'Stopped') {
log.info(`initial status fetch: state=${result.state}, url=${result.url}`);
setStatus(result.state as DeploymentState);
setUrl(result.url);
// Start polling to keep the state fresh
// (startPolling is defined below, but the effect runs after render
// so it will be available via the ref-based closure)
pollAfterInitialFetchRef.current?.(targetId);
}
});

return () => {
cancelled = true;
};
}, [input?.targetId]);

const stopPolling = useCallback(() => {
if (pollIntervalRef.current) {
log.debug('stopping polling');
Expand Down Expand Up @@ -107,6 +142,9 @@ export function useDeployAction(input: DeployActionInput | null): DeployActionSt
[stopPolling]
);

// Keep ref in sync so the initial-fetch effect can start polling
pollAfterInitialFetchRef.current = startPolling;

const handleDeploy = useCallback(async () => {
if (!input) {
log.warn('deploy() called but input is null — no-op');
Expand Down
2 changes: 1 addition & 1 deletion src/presentation/web/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ vi.mock('@/app/actions/check-agent-auth', () => ({
),
}));

vi.mock('@/app/actions/get-deployment-status', () => ({
getDeploymentStatus: vi.fn(() => Promise.resolve(null)),
}));

vi.mock('@/components/common/feature-node/agent-type-icons', () => ({
getAgentTypeIcon: () => {
function MockIcon(props: Record<string, unknown>) {
Expand Down
Loading
Loading