Skip to content

Commit 58753eb

Browse files
committed
노트북 진단 표면 + 셀 삭제 시 커널 변수 정리
리액티브 진단을 셀에 표시한다 — 순환은 빨강 conflict 상태 + 상단 경로 배너, 다중정의·외부변경은 노란 advisory 칩, 코드 편집으로 오래된 다운스트림은 "오래됨"(stale). reactiveDiagnostics lib가 stale 전이 BFS와 셀별 진단 투영을 담당하고, 진단은 notebookRuntime→hook→surface→패널로 흐른다. 셀 삭제 시 removeCell로 워커 registry를 정리해 좀비 변수를 없앤다(검증된 remove-cell 엔드포인트 호출). editor-runtime-preflight probe에 새 lib 스텁 추가.
1 parent be972e2 commit 58753eb

12 files changed

Lines changed: 253 additions & 8 deletions

editor/src/App.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,9 @@ function App() {
177177
const selectedBlock = activeDocument.blocks.find((block) => block.id === activeSelectedBlockId) ?? activeDocument.blocks.find(isExecutableBlock) ?? activeDocument.blocks[0];
178178
const {
179179
canRun,
180+
cleanupCellDefinitions,
180181
currentResult,
182+
diagnostics,
181183
notebookRunning,
182184
resetRuntimeState,
183185
results,
@@ -187,6 +189,7 @@ function App() {
187189
sessionId,
188190
setSessionId,
189191
setUiValue,
192+
staleBlockIds,
190193
variables,
191194
} = useNotebookRuntimeState({
192195
apiOnline,
@@ -369,6 +372,7 @@ function App() {
369372
categories={filteredCategories}
370373
contents={contents}
371374
curriculumDocument={curriculumDocument}
375+
diagnostics={diagnostics}
372376
document={document}
373377
drafts={drafts}
374378
eStop={eStop}
@@ -383,6 +387,7 @@ function App() {
383387
selectedBlockId={selectedBlockId}
384388
selectedCurriculumBlockId={selectedCurriculumBlockId}
385389
selectedContentId={selectedContentId}
390+
staleBlockIds={staleBlockIds}
386391
surface={surface}
387392
tasks={tasks}
388393
loadState={loadState}
@@ -393,7 +398,10 @@ function App() {
393398
onConnectAi={connectAiProvider}
394399
onCellAsk={askCellAssistant}
395400
onDraftChange={updateDraft}
396-
onDeleteCell={deleteNotebookCell}
401+
onDeleteCell={(blockId) => {
402+
cleanupCellDefinitions(blockId);
403+
deleteNotebookCell(blockId);
404+
}}
397405
onNewChat={startNewChat}
398406
onOpenTerminalCommand={openTerminalCommand}
399407
onPromptChange={setPrompt}

editor/src/components/app/mainSurface.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
EStopStatus,
1414
ExecutionResult,
1515
LoadState,
16+
ReactiveDiagnostics,
1617
SchedulerStatus,
1718
TaskDefinition,
1819
TaskListPayload,
@@ -40,6 +41,7 @@ type MainSurfaceProps = {
4041
categories: CurriculumCategory[];
4142
contents: CurriculumContentSummary[];
4243
curriculumDocument: CodaroDocument | null;
44+
diagnostics: ReactiveDiagnostics;
4345
document: CodaroDocument;
4446
drafts: Record<string, string>;
4547
eStop: EStopStatus;
@@ -56,6 +58,7 @@ type MainSurfaceProps = {
5658
selectedCategory: string;
5759
selectedContentId: string;
5860
selectedCurriculumBlockId: string;
61+
staleBlockIds: string[];
5962
surface: SurfaceMode;
6063
tasks: TaskListPayload;
6164
onAcceptPendingBlocks: () => void;
@@ -121,6 +124,7 @@ function MainSurfaceContent(props: MainSurfaceProps) {
121124
assistantLoading={props.assistantLoading}
122125
canRun={props.canRun}
123126
cellHelpByBlockId={props.cellHelpByBlockId}
127+
diagnostics={props.diagnostics}
124128
document={props.document}
125129
drafts={props.drafts}
126130
messages={props.messages}
@@ -130,6 +134,7 @@ function MainSurfaceContent(props: MainSurfaceProps) {
130134
results={props.results}
131135
runningBlockId={props.runningBlockId}
132136
selectedBlockId={props.selectedBlockId}
137+
staleBlockIds={props.staleBlockIds}
133138
onAcceptPendingBlocks={props.onAcceptPendingBlocks}
134139
onAddCell={props.onAddCell}
135140
onAsk={props.onAsk}

editor/src/components/app/notebookSurface.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
BlockConfig,
1010
CodaroDocument,
1111
ExecutionResult,
12+
ReactiveDiagnostics,
1213
} from "@/types";
1314

1415
type ResultMap = Record<string, ExecutionResult>;
@@ -21,6 +22,7 @@ export type NotebookSurfaceProps = {
2122
assistantLoading: boolean;
2223
canRun: boolean;
2324
cellHelpByBlockId: Record<string, CellAiHelpState>;
25+
diagnostics: ReactiveDiagnostics;
2426
document: CodaroDocument;
2527
drafts: Record<string, string>;
2628
messages: AssistantMessage[];
@@ -30,6 +32,7 @@ export type NotebookSurfaceProps = {
3032
results: ResultMap;
3133
runningBlockId: string | null;
3234
selectedBlockId: string;
35+
staleBlockIds: string[];
3336
onAcceptPendingBlocks: () => void;
3437
onAddCell: (type: "code" | "markdown", referenceBlockId?: string, placement?: "before" | "after") => void;
3538
onAsk: (messageOverride?: string, scopeOverride?: TeacherScope) => void;
@@ -57,13 +60,15 @@ export function NotebookSurface(props: NotebookSurfaceProps) {
5760
<NotebookPanel
5861
canRun={props.canRun}
5962
cellHelpByBlockId={props.cellHelpByBlockId}
63+
diagnostics={props.diagnostics}
6064
document={props.document}
6165
drafts={props.drafts}
6266
notebookRunning={props.notebookRunning}
6367
pendingBlocks={props.pendingBlocks}
6468
results={props.results}
6569
runningBlockId={props.runningBlockId}
6670
selectedBlockId={props.selectedBlockId}
71+
staleBlockIds={props.staleBlockIds}
6772
onAddCell={props.onAddCell}
6873
onDraftChange={props.onDraftChange}
6974
onAcceptPendingBlocks={props.onAcceptPendingBlocks}

editor/src/components/notebook/notebookPanel.tsx

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,14 @@ import { fetchCodeCompletions, type CompletionContextProvider } from "@/lib/code
4747
import { isExecutableBlock, type CellAiAction } from "@/lib/cellModel";
4848
import type { CellAiHelpState } from "@/lib/assistantTypes";
4949
import { statusLabel } from "@/lib/displayFormat";
50+
import {
51+
blockInCycle,
52+
cellDiagnosticChips,
53+
formatCyclePaths,
54+
type CellDiagnosticChip,
55+
} from "@/lib/reactiveDiagnostics";
5056
import { cn } from "@/lib/utils";
51-
import type { BlockConfig, CodaroDocument, ExecutionResult } from "@/types";
57+
import type { BlockConfig, CodaroDocument, ExecutionResult, ReactiveDiagnostics } from "@/types";
5258

5359
type ResultMap = Record<string, ExecutionResult>;
5460

@@ -114,13 +120,15 @@ const codeCellEditorTheme = EditorView.theme({
114120
export function NotebookPanel({
115121
canRun,
116122
cellHelpByBlockId,
123+
diagnostics,
117124
document,
118125
drafts,
119126
notebookRunning,
120127
pendingBlocks,
121128
results,
122129
runningBlockId,
123130
selectedBlockId,
131+
staleBlockIds,
124132
onAddCell,
125133
onAcceptPendingBlocks,
126134
onCellAsk,
@@ -134,13 +142,15 @@ export function NotebookPanel({
134142
}: {
135143
canRun: boolean;
136144
cellHelpByBlockId: Record<string, CellAiHelpState>;
145+
diagnostics: ReactiveDiagnostics;
137146
document: CodaroDocument;
138147
drafts: Record<string, string>;
139148
notebookRunning: boolean;
140149
pendingBlocks: BlockConfig[];
141150
results: ResultMap;
142151
runningBlockId: string | null;
143152
selectedBlockId: string;
153+
staleBlockIds: string[];
144154
onAddCell: (type: "code" | "markdown", referenceBlockId?: string, placement?: "before" | "after") => void;
145155
onAcceptPendingBlocks: () => void;
146156
onCellAsk: (action: CellAiAction, block: BlockConfig, question?: string) => void;
@@ -152,6 +162,8 @@ export function NotebookPanel({
152162
onRunNotebook: () => void;
153163
onSelectBlock: (blockId: string) => void;
154164
}) {
165+
const staleSet = new Set(staleBlockIds);
166+
const cyclePaths = formatCyclePaths(diagnostics.cycles);
155167
return (
156168
<section className="grid h-full min-h-0 grid-rows-[auto_auto_minmax(0,1fr)] p-2 sm:p-3">
157169
<div className="mb-2 flex min-h-8 items-center gap-2 pl-9">
@@ -180,6 +192,11 @@ export function NotebookPanel({
180192
onAccept={onAcceptPendingBlocks}
181193
onReject={onRejectPendingBlocks}
182194
/>
195+
{cyclePaths.length ? (
196+
<div className="rounded-md border border-destructive/40 bg-destructive/10 px-2.5 py-1.5 text-[11px] text-destructive">
197+
<span className="font-medium">순환 의존</span> — 실행 순서가 정해지지 않습니다: {cyclePaths.join(" · ")}
198+
</div>
199+
) : null}
183200
</div>
184201

185202
<ScrollArea className="h-full min-h-0">
@@ -194,6 +211,9 @@ export function NotebookPanel({
194211
result={results[block.id]}
195212
cellHelp={cellHelpByBlockId[block.id]}
196213
isRunning={runningBlockId === block.id}
214+
isStale={staleSet.has(block.id)}
215+
inCycle={blockInCycle(diagnostics, block.id)}
216+
diagnosticChips={cellDiagnosticChips(diagnostics, block.id)}
197217
onCellAsk={(action, question) => onCellAsk(action, block, question)}
198218
onDelete={() => onDeleteCell(block.id)}
199219
onDraftChange={(value) => onDraftChange(block.id, value)}
@@ -548,6 +568,9 @@ function DocumentBlock({
548568
draft,
549569
isSelected,
550570
isRunning,
571+
isStale = false,
572+
inCycle = false,
573+
diagnosticChips = [],
551574
result,
552575
cellHelp,
553576
onDraftChange,
@@ -562,6 +585,9 @@ function DocumentBlock({
562585
draft: string;
563586
isSelected: boolean;
564587
isRunning: boolean;
588+
isStale?: boolean;
589+
inCycle?: boolean;
590+
diagnosticChips?: CellDiagnosticChip[];
565591
result?: ExecutionResult;
566592
cellHelp?: CellAiHelpState;
567593
onCellAsk: (action: CellAiAction, question?: string) => void;
@@ -572,7 +598,8 @@ function DocumentBlock({
572598
onSelect: () => void;
573599
}) {
574600
const cellTitle = block.type === "markdown" ? "Markdown" : block.type === "automation" ? "Automation" : "Python";
575-
const resultStatus = isRunning ? "running" : result?.status ?? "idle";
601+
// 우선순위: 실행 중 → 순환(conflict, 빨강) → stale(오래됨) → 실행 결과 → 대기.
602+
const resultStatus = isRunning ? "running" : inCycle ? "conflict" : isStale ? "stale" : result?.status ?? "idle";
576603

577604
if (block.type === "markdown") {
578605
return (
@@ -583,6 +610,7 @@ function DocumentBlock({
583610
type="markdown"
584611
selected={isSelected}
585612
cellHelp={cellHelp}
613+
diagnosticChips={diagnosticChips}
586614
onCellAsk={onCellAsk}
587615
onDelete={onDelete}
588616
/>
@@ -614,6 +642,7 @@ function DocumentBlock({
614642
type="code"
615643
selected={isSelected}
616644
cellHelp={cellHelp}
645+
diagnosticChips={diagnosticChips}
617646
onCellAsk={onCellAsk}
618647
onDelete={onDelete}
619648
onRun={onRun}
@@ -664,6 +693,7 @@ function CellMetaBar({
664693
type,
665694
selected,
666695
cellHelp,
696+
diagnosticChips = [],
667697
onCellAsk,
668698
onDelete,
669699
onRun,
@@ -675,6 +705,7 @@ function CellMetaBar({
675705
type: "code" | "markdown";
676706
selected: boolean;
677707
cellHelp?: CellAiHelpState;
708+
diagnosticChips?: CellDiagnosticChip[];
678709
onCellAsk: (action: CellAiAction, question?: string) => void;
679710
onDelete: () => void;
680711
onRun?: () => void;
@@ -693,11 +724,24 @@ function CellMetaBar({
693724
<span className="truncate">{title}</span>
694725
</div>
695726
<div className="ml-auto flex shrink-0 items-center gap-1">
727+
{diagnosticChips.map((chip) => (
728+
<span
729+
key={chip.kind}
730+
className="h-6 rounded-md border border-amber-500/40 bg-amber-500/10 px-1.5 py-0.5 text-[11px] font-medium leading-5 text-amber-700 dark:text-amber-400"
731+
title="정합성 경고 — 실행은 진행되며, 마지막 정의가 적용됩니다."
732+
>
733+
{chip.label}
734+
</span>
735+
))}
696736
{status !== "idle" ? (
697737
<span
698738
className={cn(
699739
"h-6 rounded-md px-1.5 py-0.5 text-[11px] font-medium leading-5",
700-
status === "error" ? "bg-destructive text-destructive-foreground" : "border bg-background text-muted-foreground",
740+
status === "error" || status === "conflict"
741+
? "bg-destructive text-destructive-foreground"
742+
: status === "stale"
743+
? "border border-dashed bg-background text-muted-foreground"
744+
: "border bg-background text-muted-foreground",
701745
)}
702746
>
703747
{statusLabel(status)}

editor/src/hooks/useNotebookRuntimeState.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@ import { blockLabel, isExecutableBlock } from "@/lib/cellModel";
33
import type { ResultMap } from "@/lib/assistantContext";
44
import { translate } from "@/lib/localeCopy";
55
import {
6+
removeNotebookCellState,
67
resolveBlockRunCode,
78
runNotebookBlock,
89
runReactiveNotebook,
910
setNotebookUiValue,
1011
} from "@/lib/notebookRuntime";
12+
import { computeStaleBlockIds, emptyReactiveDiagnostics } from "@/lib/reactiveDiagnostics";
1113
import type { SurfaceMode } from "@/lib/surfaceModel";
1214
import type {
1315
AppNotice,
1416
BlockConfig,
1517
CodaroDocument,
18+
ReactiveDiagnostics,
1619
VariableInfo,
1720
} from "@/types";
1821

@@ -42,6 +45,9 @@ export function useNotebookRuntimeState({
4245
const [results, setResults] = useState<ResultMap>({});
4346
const [runningBlockId, setRunningBlockId] = useState<string | null>(null);
4447
const [notebookRunning, setNotebookRunning] = useState(false);
48+
const [diagnostics, setDiagnostics] = useState<ReactiveDiagnostics>(emptyReactiveDiagnostics);
49+
// 마지막 실행 시점에 보낸 셀 내용 스냅샷 — 이후 draft가 달라지면 그 셀(+다운스트림)이 stale.
50+
const [lastRunContent, setLastRunContent] = useState<Record<string, string>>({});
4551

4652
const codeBlocks = useMemo(() => document.blocks.filter(isExecutableBlock), [document.blocks]);
4753
const hasRunnableNotebook = codeBlocks.some((block) => (drafts[block.id] ?? block.content).trim());
@@ -51,6 +57,8 @@ export function useNotebookRuntimeState({
5157
const resetRuntimeState = useCallback(() => {
5258
setResults({});
5359
setVariables([]);
60+
setDiagnostics(emptyReactiveDiagnostics);
61+
setLastRunContent({});
5462
}, []);
5563

5664
const runBlock = useCallback(async (block: BlockConfig) => {
@@ -104,6 +112,9 @@ export function useNotebookRuntimeState({
104112
if (outcome.sessionId && outcome.sessionId !== sessionId) setSessionId(outcome.sessionId);
105113
if (outcome.results) setResults((current) => ({ ...current, ...outcome.results }));
106114
if (outcome.variables) setVariables(outcome.variables);
115+
if (outcome.diagnostics) setDiagnostics(outcome.diagnostics);
116+
// 실행에 사용한 draft를 스냅샷 → 이후 편집을 stale 판정의 기준선으로.
117+
setLastRunContent(Object.fromEntries(codeBlocks.map((block) => [block.id, drafts[block.id] ?? block.content])));
107118
if (outcome.notice) onNotice(outcome.notice);
108119
} finally {
109120
setNotebookRunning(false);
@@ -124,8 +135,26 @@ export function useNotebookRuntimeState({
124135
});
125136
if (outcome.results) setResults((current) => ({ ...current, ...outcome.results }));
126137
if (outcome.variables) setVariables(outcome.variables);
138+
if (outcome.diagnostics) setDiagnostics(outcome.diagnostics);
127139
}, [sessionId, document, drafts, variables]);
128140

141+
// 코드 편집(draft≠마지막 실행 내용)으로 stale해진 셀 + 다운스트림 전이 + 백엔드 early-stop stale.
142+
const staleBlockIds = useMemo(() => {
143+
const dirty = new Set<string>();
144+
for (const block of codeBlocks) {
145+
const current = drafts[block.id] ?? block.content;
146+
if (block.id in lastRunContent && current !== lastRunContent[block.id]) dirty.add(block.id);
147+
}
148+
const stale = computeStaleBlockIds(diagnostics.dependents, dirty);
149+
for (const blockId of diagnostics.staleBlockIds) stale.add(blockId);
150+
if (runningBlockId) stale.delete(runningBlockId);
151+
return Array.from(stale);
152+
}, [codeBlocks, drafts, lastRunContent, diagnostics, runningBlockId]);
153+
154+
const cleanupCellDefinitions = useCallback((blockId: string) => {
155+
void removeNotebookCellState(sessionId, blockId);
156+
}, [sessionId]);
157+
129158
useEffect(() => {
130159
if (typeof window === "undefined") return;
131160
const handler = (event: Event) => {
@@ -144,7 +173,9 @@ export function useNotebookRuntimeState({
144173

145174
return {
146175
canRun,
176+
cleanupCellDefinitions,
147177
currentResult,
178+
diagnostics,
148179
hasRunnableNotebook,
149180
notebookRunning,
150181
resetRuntimeState,
@@ -155,6 +186,7 @@ export function useNotebookRuntimeState({
155186
sessionId,
156187
setSessionId,
157188
setUiValue,
189+
staleBlockIds,
158190
variables,
159191
};
160192
}

0 commit comments

Comments
 (0)