feat(health): configurable per-project stall threshold
This commit is contained in:
parent
267087937f
commit
6b420a6a1f
4 changed files with 42 additions and 3 deletions
|
|
@ -12,7 +12,7 @@
|
|||
import SshTab from './SshTab.svelte';
|
||||
import MemoriesTab from './MemoriesTab.svelte';
|
||||
import { getTerminalTabs } from '../../stores/workspace.svelte';
|
||||
import { getProjectHealth } from '../../stores/health.svelte';
|
||||
import { getProjectHealth, setStallThreshold } from '../../stores/health.svelte';
|
||||
import { fsWatchProject, fsUnwatchProject, onFsWriteDetected, fsWatcherStatus } from '../../adapters/fs-watcher-bridge';
|
||||
import { recordExternalWrite } from '../../stores/conflicts.svelte';
|
||||
import { notify, dismissNotification } from '../../stores/notifications.svelte';
|
||||
|
|
@ -52,6 +52,11 @@
|
|||
terminalExpanded = !terminalExpanded;
|
||||
}
|
||||
|
||||
// Sync per-project stall threshold to health store
|
||||
$effect(() => {
|
||||
setStallThreshold(project.id, project.stallThresholdMin ?? null);
|
||||
});
|
||||
|
||||
// S-1 Phase 2: start filesystem watcher for this project's CWD
|
||||
$effect(() => {
|
||||
const cwd = project.cwd;
|
||||
|
|
|
|||
|
|
@ -808,6 +808,26 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div class="card-field">
|
||||
<span class="card-field-label">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
||||
Stall Threshold
|
||||
</span>
|
||||
<div class="scale-slider">
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="60"
|
||||
step="5"
|
||||
value={project.stallThresholdMin ?? 15}
|
||||
oninput={(e) => {
|
||||
updateProject(activeGroupId, project.id, { stallThresholdMin: parseInt((e.target as HTMLInputElement).value) });
|
||||
}}
|
||||
/>
|
||||
<span class="scale-label">{project.stallThresholdMin ?? 15} min</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<button class="btn-remove" onclick={() => removeProject(activeGroupId, project.id)}>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export type AttentionItem = ProjectHealth & { projectName: string; projectIcon:
|
|||
|
||||
// --- Configuration ---
|
||||
|
||||
const STALL_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
|
||||
const DEFAULT_STALL_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
|
||||
const TICK_INTERVAL_MS = 5_000; // Update derived state every 5s
|
||||
const BURN_RATE_WINDOW_MS = 5 * 60 * 1000; // 5-minute window for burn rate calc
|
||||
|
||||
|
|
@ -74,6 +74,7 @@ interface ProjectTracker {
|
|||
}
|
||||
|
||||
let trackers = $state<Map<string, ProjectTracker>>(new Map());
|
||||
let stallThresholds = $state<Map<string, number>>(new Map()); // projectId → ms
|
||||
let tickTs = $state<number>(Date.now());
|
||||
let tickInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
|
|
@ -102,6 +103,15 @@ export function untrackProject(projectId: string): void {
|
|||
trackers.delete(projectId);
|
||||
}
|
||||
|
||||
/** Set per-project stall threshold in minutes (null to use default) */
|
||||
export function setStallThreshold(projectId: string, minutes: number | null): void {
|
||||
if (minutes === null) {
|
||||
stallThresholds.delete(projectId);
|
||||
} else {
|
||||
stallThresholds.set(projectId, minutes * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/** Update session ID for a tracked project */
|
||||
export function updateProjectSession(projectId: string, sessionId: string): void {
|
||||
const t = trackers.get(projectId);
|
||||
|
|
@ -177,6 +187,7 @@ export function stopHealthTick(): void {
|
|||
/** Clear all tracked projects */
|
||||
export function clearHealthTracking(): void {
|
||||
trackers = new Map();
|
||||
stallThresholds = new Map();
|
||||
}
|
||||
|
||||
// --- Derived health per project ---
|
||||
|
|
@ -217,7 +228,8 @@ function computeHealth(tracker: ProjectTracker, now: number): ProjectHealth {
|
|||
idleDurationMs = 0;
|
||||
} else {
|
||||
idleDurationMs = now - tracker.lastActivityTs;
|
||||
if (idleDurationMs >= STALL_THRESHOLD_MS) {
|
||||
const stallMs = stallThresholds.get(tracker.projectId) ?? DEFAULT_STALL_THRESHOLD_MS;
|
||||
if (idleDurationMs >= stallMs) {
|
||||
activityState = 'stalled';
|
||||
} else {
|
||||
activityState = 'idle';
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ export interface ProjectConfig {
|
|||
useWorktrees?: boolean;
|
||||
/** Anchor token budget scale (defaults to 'medium' = 6K tokens) */
|
||||
anchorBudgetScale?: AnchorBudgetScale;
|
||||
/** Stall detection threshold in minutes (defaults to 15) */
|
||||
stallThresholdMin?: number;
|
||||
}
|
||||
|
||||
export interface GroupConfig {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue