Skip to content

Commit 08ed28b

Browse files
committed
fix: Various bugfixes and improvements
1 parent 099778d commit 08ed28b

28 files changed

Lines changed: 322 additions & 87 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ After **every single file edit**, these hooks fire:
351351
| -------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
352352
| `file_checker.py` | Blocking | Dispatches to language-specific checkers: Python (ruff + basedpyright), TypeScript (Prettier + ESLint + tsc), Go (gofmt + golangci-lint). Auto-fixes formatting. |
353353
| `tdd_enforcer.py` | Non-blocking | Checks if implementation files were modified without failing tests first. Shows reminder to write tests. Excludes test files, docs, config, TSX, and infrastructure. |
354-
| `context_monitor.py` | Non-blocking | Monitors context usage. Warns at 75%+ (informational) and 80%+ (caution). Prompts `/learn` at key thresholds. |
354+
| `context_monitor.py` | Non-blocking | Monitors context usage. Warns at 65%+ (informational) and 75%+ (caution). Prompts `/learn` at key thresholds. |
355355
| Memory observer | Async | Captures development observations to persistent memory. |
356356

357357
#### PreCompact (before auto-compaction)
@@ -390,7 +390,7 @@ Production-tested best practices loaded into **every session**. These aren't sug
390390
<details>
391391
<summary><b>Core Workflow (3 rules)</b></summary>
392392

393-
- `workflow-enforcement.md` — Task management, /spec orchestration, deviation handling
393+
- `task-and-workflow.md` — Task management, /spec orchestration, deviation handling
394394
- `testing.md` — TDD workflow, test strategy, coverage requirements
395395
- `verification.md` — Execution verification, completion requirements
396396

console/src/ui/viewer/App.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { Router, useRouter } from './router';
44
import { DashboardView, MemoriesView, SessionsView, SpecView, UsageView, VaultView } from './views';
55
import { LogsDrawer } from './components/LogsModal';
66
import { CommandPalette } from './components/CommandPalette';
7+
import { LicenseGate } from './components/LicenseGate';
78
import { useTheme } from './hooks/useTheme';
89
import { useStats } from './hooks/useStats';
910
import { useHotkeys } from './hooks/useHotkeys';
11+
import { useLicense } from './hooks/useLicense';
1012
import { ToastProvider, ProjectProvider } from './context';
1113

1214
const routes = [
@@ -25,6 +27,7 @@ export function App() {
2527
const { path, navigate } = useRouter();
2628
const { resolvedTheme, setThemePreference } = useTheme();
2729
const { workerStatus } = useStats();
30+
const { license, isLoading: licenseLoading, refetch: refetchLicense } = useLicense();
2831

2932
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
3033
const isMobile = typeof window !== 'undefined' && window.innerWidth < 1024;
@@ -79,6 +82,24 @@ export function App() {
7982

8083
useHotkeys(handleShortcut);
8184

85+
const isLicenseValid = !licenseLoading && license?.valid === true && !license.isExpired;
86+
87+
if (licenseLoading) {
88+
return (
89+
<div className="min-h-screen flex items-center justify-center bg-base-200" data-theme={resolvedTheme === 'dark' ? 'claude-pilot' : 'claude-pilot-light'}>
90+
<span className="loading loading-spinner loading-lg text-primary" />
91+
</div>
92+
);
93+
}
94+
95+
if (!isLicenseValid) {
96+
return (
97+
<div data-theme={resolvedTheme === 'dark' ? 'claude-pilot' : 'claude-pilot-light'}>
98+
<LicenseGate license={license} onActivated={refetchLicense} />
99+
</div>
100+
);
101+
}
102+
82103
return (
83104
<ProjectProvider>
84105
<ToastProvider>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { useState, useCallback } from 'react';
2+
import type { LicenseResponse } from '../../../services/worker/http/routes/LicenseRoutes.js';
3+
4+
interface LicenseGateProps {
5+
license: LicenseResponse | null;
6+
onActivated: () => void;
7+
}
8+
9+
export function LicenseGate({ license, onActivated }: LicenseGateProps) {
10+
const [key, setKey] = useState('');
11+
const [error, setError] = useState<string | null>(null);
12+
const [isSubmitting, setIsSubmitting] = useState(false);
13+
14+
const handleSubmit = useCallback(async () => {
15+
const trimmed = key.trim();
16+
if (!trimmed) return;
17+
18+
setError(null);
19+
setIsSubmitting(true);
20+
21+
try {
22+
const res = await fetch('/api/license/activate', {
23+
method: 'POST',
24+
headers: { 'Content-Type': 'application/json' },
25+
body: JSON.stringify({ key: trimmed }),
26+
});
27+
const data = await res.json();
28+
29+
if (data.success) {
30+
setKey('');
31+
setError(null);
32+
onActivated();
33+
} else {
34+
setError(data.error ?? 'Activation failed');
35+
}
36+
} catch {
37+
setError('Connection failed. Is the Pilot worker running?');
38+
} finally {
39+
setIsSubmitting(false);
40+
}
41+
}, [key, onActivated]);
42+
43+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
44+
if (e.key === 'Enter' && !isSubmitting) {
45+
handleSubmit();
46+
}
47+
}, [handleSubmit, isSubmitting]);
48+
49+
const isExpired = license?.isExpired === true;
50+
const title = isExpired ? 'License Expired' : 'License Required';
51+
const subtitle = isExpired
52+
? 'Your Claude Pilot license has expired. Please activate a new license to continue using the Console.'
53+
: 'Claude Pilot Console requires an active license or trial. Activate your license key below to get started.';
54+
55+
return (
56+
<div className="min-h-screen flex items-center justify-center bg-base-200 p-4">
57+
<div className="card bg-base-100 shadow-xl w-full max-w-md">
58+
<div className="card-body items-center text-center gap-4">
59+
<div className="text-5xl mb-2">
60+
{isExpired ? '\u{1F6AB}' : '\u{1F512}'}
61+
</div>
62+
63+
<h1 className="card-title text-2xl">{title}</h1>
64+
<p className="text-base-content/60 text-sm">{subtitle}</p>
65+
66+
<div className="w-full space-y-3 mt-2">
67+
<input
68+
type="text"
69+
className="input input-bordered w-full"
70+
placeholder="Enter your license key"
71+
value={key}
72+
onChange={(e) => { setKey(e.target.value); setError(null); }}
73+
onKeyDown={handleKeyDown}
74+
disabled={isSubmitting}
75+
autoFocus
76+
/>
77+
78+
{error && (
79+
<p className="text-error text-sm text-left">{error}</p>
80+
)}
81+
82+
<button
83+
className="btn btn-primary w-full"
84+
onClick={handleSubmit}
85+
disabled={isSubmitting || !key.trim()}
86+
>
87+
{isSubmitting ? 'Activating...' : 'Activate License'}
88+
</button>
89+
</div>
90+
91+
<div className="divider text-base-content/40 text-xs my-1">or</div>
92+
93+
<a
94+
href="https://claude-pilot.com/#pricing"
95+
target="_blank"
96+
rel="noopener noreferrer"
97+
className="btn btn-outline btn-sm w-full"
98+
>
99+
Get a License
100+
</a>
101+
102+
<p className="text-base-content/40 text-xs mt-2">
103+
Visit{' '}
104+
<a
105+
href="https://claude-pilot.com"
106+
target="_blank"
107+
rel="noopener noreferrer"
108+
className="text-primary hover:underline"
109+
>
110+
claude-pilot.com
111+
</a>
112+
{' '}to learn more about Claude Pilot.
113+
</p>
114+
</div>
115+
</div>
116+
</div>
117+
);
118+
}

console/src/ui/viewer/hooks/useLicense.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export function useLicense(): UseLicenseResult {
2626

2727
useEffect(() => {
2828
fetchLicense();
29+
const interval = setInterval(() => fetchLicense(true), 60_000);
30+
return () => clearInterval(interval);
2931
}, [fetchLicense]);
3032

3133
const refetch = useCallback(() => fetchLicense(true), [fetchLicense]);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Tests for LicenseGate component
3+
*
4+
* Tests the full-page license gate screen that blocks console access
5+
* when no valid license is present.
6+
*/
7+
import { describe, it, expect } from "bun:test";
8+
import React from "react";
9+
import { renderToStaticMarkup } from "react-dom/server";
10+
import { LicenseGate } from "../../src/ui/viewer/components/LicenseGate.js";
11+
import type { LicenseResponse } from "../../src/services/worker/http/routes/LicenseRoutes.js";
12+
13+
function renderGate(license: LicenseResponse | null) {
14+
return renderToStaticMarkup(
15+
React.createElement(LicenseGate, { license, onActivated: () => {} }),
16+
);
17+
}
18+
19+
describe("LicenseGate", () => {
20+
it("should render license required title when no license", () => {
21+
const html = renderGate({
22+
valid: false,
23+
tier: null,
24+
email: null,
25+
daysRemaining: null,
26+
isExpired: false,
27+
});
28+
29+
expect(html).toContain("License Required");
30+
expect(html).toContain("Enter your license key");
31+
});
32+
33+
it("should render expired title when license is expired", () => {
34+
const html = renderGate({
35+
valid: false,
36+
tier: "trial",
37+
email: "user@example.com",
38+
daysRemaining: null,
39+
isExpired: true,
40+
});
41+
42+
expect(html).toContain("License Expired");
43+
expect(html).toContain("has expired");
44+
});
45+
46+
it("should contain activation input", () => {
47+
const html = renderGate(null);
48+
49+
expect(html).toContain("Enter your license key");
50+
expect(html).toContain("Activate License");
51+
});
52+
53+
it("should contain link to pricing page", () => {
54+
const html = renderGate(null);
55+
56+
expect(html).toContain("claude-pilot.com/#pricing");
57+
expect(html).toContain("Get a License");
58+
});
59+
60+
it("should contain link to main site", () => {
61+
const html = renderGate(null);
62+
63+
expect(html).toContain("claude-pilot.com");
64+
});
65+
66+
it("should render activate button as disabled by default (empty key)", () => {
67+
const html = renderGate(null);
68+
69+
expect(html).toContain("disabled");
70+
expect(html).toContain("Activate License");
71+
});
72+
73+
it("should use lock icon for no license", () => {
74+
const html = renderGate({
75+
valid: false,
76+
tier: null,
77+
email: null,
78+
daysRemaining: null,
79+
isExpired: false,
80+
});
81+
82+
expect(html).toContain("\u{1F512}");
83+
});
84+
85+
it("should use prohibited icon for expired license", () => {
86+
const html = renderGate({
87+
valid: false,
88+
tier: "trial",
89+
email: null,
90+
daysRemaining: null,
91+
isExpired: true,
92+
});
93+
94+
expect(html).toContain("\u{1F6AB}");
95+
});
96+
});

docs/site/src/components/DeepDiveSection.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const hooksPipeline = [
4949
hooks: [
5050
"File checker: auto-format, lint, type-check (Python, TypeScript, Go)",
5151
"TDD enforcer: warns if no failing test exists",
52-
"Context monitor: warns at 75%+, caution at 80%+",
52+
"Context monitor: warns at 65%+, caution at 75%+",
5353
"Memory observation: captures development context (async)",
5454
],
5555
color: "text-primary",
@@ -216,7 +216,7 @@ const DeepDiveSection = () => {
216216
<div className="rounded-2xl p-5 border border-amber-400/30 bg-card/30 backdrop-blur-sm">
217217
<div className="flex items-center gap-2 mb-3">
218218
<AlertTriangle className="h-5 w-5 text-amber-400" />
219-
<span className="text-lg font-bold text-foreground">75%</span>
219+
<span className="text-lg font-bold text-foreground">65%</span>
220220
<span className="text-xs text-amber-400 font-medium">INFO</span>
221221
</div>
222222
<p className="text-sm text-muted-foreground">
@@ -226,7 +226,7 @@ const DeepDiveSection = () => {
226226
<div className="rounded-2xl p-5 border border-orange-500/30 bg-card/30 backdrop-blur-sm">
227227
<div className="flex items-center gap-2 mb-3">
228228
<AlertTriangle className="h-5 w-5 text-orange-500" />
229-
<span className="text-lg font-bold text-foreground">80%</span>
229+
<span className="text-lg font-bold text-foreground">75%</span>
230230
<span className="text-xs text-orange-500 font-medium">CAUTION</span>
231231
</div>
232232
<p className="text-sm text-muted-foreground">

docs/site/src/components/WhatsInside.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const insideItems: InsideItem[] = [
6666
icon: GitBranch,
6767
title: "Isolated Workspaces",
6868
description: "Safe experimentation, clean git history",
69-
summary: "Spec implementation runs in isolated git worktrees. Review changes, squash merge when verified, or discard without touching your main branch. Worktree state survives session restarts.",
69+
summary: "Spec implementation runs in isolated git worktrees. Review changes, squash merge when verified, or discard without touching your main branch. Worktree state survives across auto-compaction cycles.",
7070
},
7171
];
7272

docs/site/src/content/blog/claude-code-hooks-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ Claude Code passes context to hooks via environment variables:
160160
Claude Pilot installs several hooks automatically:
161161

162162
- **TDD Enforcer** (PostToolUse): Reminds Claude to write tests before production code
163-
- **Context Monitor** (PostToolUse): Tracks context usage and prepares for auto-compaction at 90%
163+
- **Context Monitor** (PostToolUse): Tracks context usage and warns at 65%+ and 75%+ as compaction approaches
164164
- **Tool Redirect** (PreToolUse): Blocks inefficient tools and suggests better alternatives
165165
- **PreCompact** (PreCompact): Captures active plan, task progress, and key context to memory before compaction
166166
- **Session End** (SessionEnd): Stops worker daemon when no other sessions are active and sends completion notifications

docs/site/src/content/blog/claude-code-task-management.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ This visibility is especially valuable for long features where you need to track
6868

6969
## Cross-Session Persistence
7070

71-
Tasks survive session restarts. When auto-compaction triggers, the new session picks up the task list exactly where the old one left off. At the start of a new session, Claude checks `TaskList` to find where to resume.
71+
Tasks survive auto-compaction. When auto-compaction triggers, the task list is preserved exactly where it left off. Claude checks `TaskList` to find where to resume and continues seamlessly.
7272

7373
Stale tasks from previous sessions are cleaned up automatically — each session starts by reviewing the task list and removing anything no longer relevant.
7474

docs/site/src/content/blog/managing-context-long-sessions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,11 @@ Here's how it works:
101101

102102
### 1. Context Monitoring
103103

104-
A background hook continuously tracks context usage percentage. At 75%, it warns that context is getting high. At 80%+, state-preservation hooks prepare for Claude Code's built-in auto-compaction at ~83%.
104+
A background hook continuously tracks context usage percentage. At 65%, it warns that context is getting high. At 75%+, state-preservation hooks prepare for Claude Code's built-in auto-compaction at ~83%.
105105

106106
### 2. State Preservation
107107

108-
Before the session clears, the current state is captured:
108+
Before auto-compaction fires, the current state is captured:
109109
- What task is being worked on
110110
- What's been completed
111111
- What's in progress

0 commit comments

Comments
 (0)