Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
95e65fa
fix(web): disable single-dollar inline math in remark-math (#805)
heavygee Jun 8, 2026
3ad9e37
fix(web): use session flavor label in voice context formatters (close…
heavygee Jun 8, 2026
834a5e9
fix(cursor): requeue user message on transient agent exit (auth, rate…
heavygee Jun 8, 2026
c05d875
fix(hub): preserve session metadata across archive transitions (#825)
heavygee Jun 8, 2026
bda1f03
feat(hub+web): add POST /sessions/:id/reopen + Reopen button on inact…
heavygee Jun 8, 2026
e961c57
fix(cursor): trim #784 safety patch to marker-only on legacy stream-j…
heavygee Jun 8, 2026
9bb3ab6
fix(cursor): merge SKU catalog under ACP lock and refcount agent guar…
swear01 Jun 8, 2026
58f71c3
fix(web): hide sidebar fake sessions for Cursor resume/archive (#836)
swear01 Jun 8, 2026
ed2439f
fix(cursor): register cursorSessionId before ACP session/load (#837)
swear01 Jun 8, 2026
6b36a80
Auto-approve Codex title MCP tool
tiann Jun 8, 2026
aeb3734
feat(web): scratchlist v1.1 — composer-toggle drawer + reusable FUE p…
heavygee Jun 8, 2026
3eedd6b
Release version 0.20.1
tiann Jun 8, 2026
70be39d
feat: add Fable model presets for Claude sessions (#860)
flyinghanger Jun 10, 2026
1a71185
feat(web): auto-return to chat when remote terminal exits (#857)
heavygee Jun 10, 2026
ad86801
fix(opencode): use ACP-reported reasoning effort options (#853)
swear01 Jun 10, 2026
81c77c9
fix(web): add missing i18n keys for session.inactive banner (#851)
swear01 Jun 10, 2026
ddb7613
feat(cursor): invisible sync-on-open migrator from legacy stream-json…
heavygee Jun 10, 2026
b89b9f2
fix(runner): self-restart resilience under systemd / external process…
heavygee Jun 10, 2026
578bd28
feat: silent in-place patch loop for mermaid render failures (#850)
heavygee Jun 10, 2026
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
37 changes: 37 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,43 @@ Before commit/push/PR: use the **`pre-push-review`** skill (`~/.cursor/skills/pr
- **Permission modes**: `default`, `acceptEdits`, `bypassPermissions`, `plan`
- **Namespaces**: Multi-user isolation via `CLI_API_TOKEN:<namespace>` suffix

## Adding new web features — consider an FUE

When you ship a non-essential feature (the 20% of sessions, not the 80%), consider wrapping its affordance in the generic First-User-Experience primitive so existing users discover it without a giant always-visible UI block.

- **Hook**: `web/src/lib/use-fue.ts` — `useFue(featureId)` returns `{ status, engage, dismiss }`. Storage namespace `hapi.fue.v1.<featureId>` (one localStorage key per feature, isolated from any upstream onboarding flow).
- **Components**: `web/src/components/Fue.tsx` — `<FueDot>` (small pulsing badge for the affordance) and `<FueCallout>` (portal-rendered popover with title/body + "Got it" affirmative-action dismiss).

Pattern (~10 lines around the affordance):

```tsx
const fue = useFue('my-feature')
const buttonRef = useRef<HTMLButtonElement>(null)
return (
<>
<button ref={buttonRef} onClick={() => { fue.engage(); doThing() }}>
<Icon />
{fue.status !== 'acknowledged' ? <FueDot pulsing={fue.status === 'unseen'} /> : null}
</button>
{fue.status === 'engaging' ? (
<FueCallout
title={t('myFeature.fueTitle')}
body={t('myFeature.fueBody')}
onDismiss={fue.dismiss}
anchorRef={buttonRef}
/>
) : null}
</>
)
```

Rules:
- Affirmative action only: there is no auto-timeout — user dismisses by clicking "Got it" (reading speed varies).
- The FUE dot and any feature-specific badge (e.g. an entry counter) should be **mutually exclusive**: onboarding signal beats inventory signal until acknowledged.
- Storage is opt-in per-feature; if upstream ships its own onboarding for a feature, just don't wrap that affordance.

Canonical example: scratchlist toggle in `web/src/components/AssistantChat/ComposerButtons.tsx` (`ScratchlistToggleButton`).

## Critical Thinking

1. Fix root cause (not band-aid).
Expand Down
26 changes: 16 additions & 10 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@twsxtd/hapi",
"version": "0.20.0",
"version": "0.20.1",
"description": "App for agentic coding - access coding agent anywhere",
"author": "Kirill Dubovitskiy & weishu",
"license": "AGPL-3.0-only",
Expand All @@ -26,11 +26,11 @@
}
},
"optionalDependencies": {
"@twsxtd/hapi-darwin-arm64": "0.20.0",
"@twsxtd/hapi-darwin-x64": "0.20.0",
"@twsxtd/hapi-linux-arm64": "0.20.0",
"@twsxtd/hapi-linux-x64": "0.20.0",
"@twsxtd/hapi-win32-x64": "0.20.0"
"@twsxtd/hapi-darwin-arm64": "0.20.1",
"@twsxtd/hapi-darwin-x64": "0.20.1",
"@twsxtd/hapi-linux-arm64": "0.20.1",
"@twsxtd/hapi-linux-x64": "0.20.1",
"@twsxtd/hapi-win32-x64": "0.20.1"
},
"scripts": {
"postinstall": "node -e \"try{require('fs').chmodSync(require('path').join(__dirname,'bin','hapi.cjs'),0o755)}catch(e){}\"",
Expand Down
73 changes: 64 additions & 9 deletions cli/src/agent/backends/acp/agentCliGuard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ function lockDir(): string {
return join(testHome, 'locks', 'agent-acp-active');
}

function writeTestAcpLock(args: { count: number; pids: number[] }): void {
const dir = lockDir();
mkdirSync(join(dir, 'pids'), { recursive: true });
writeFileSync(join(dir, 'count'), String(args.count), 'utf8');
for (const pid of args.pids) {
writeFileSync(join(dir, 'pids', String(pid)), String(pid), 'utf8');
}
}

function writeLegacyAcpLock(pid: number): void {
const dir = lockDir();
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'pid'), String(pid), 'utf8');
}

describe('agentCliGuard', () => {
const previousHome = process.env.HAPI_HOME;

Expand All @@ -35,32 +50,72 @@ describe('agentCliGuard', () => {
expect(isAgentAcpTransportActive()).toBe(false);
});

test('clears stale cross-process lock when pid is not running', () => {
test('keeps cross-process lock until the last transport unregisters', () => {
process.env.HAPI_HOME = testHome;
const dir = lockDir();
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'pid'), '99999999');
registerActiveAcpTransport();
registerActiveAcpTransport();

unregisterActiveAcpTransport();
expect(isAgentAcpTransportActive()).toBe(true);
expect(existsSync(lockDir())).toBe(true);

unregisterActiveAcpTransport();
expect(isAgentAcpTransportActive()).toBe(false);
expect(existsSync(dir)).toBe(false);
expect(existsSync(lockDir())).toBe(false);
});

test('keeps lock when pid file points at a live process', () => {
test('leaves refcount at one after the first of two in-process unregisters', () => {
process.env.HAPI_HOME = testHome;
registerActiveAcpTransport();
registerActiveAcpTransport();

const dir = lockDir();
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'pid'), String(process.pid));
unregisterActiveAcpTransport();

expect(isAgentAcpTransportActive()).toBe(true);
expect(existsSync(dir)).toBe(true);
expect(existsSync(join(dir, 'pids', String(process.pid)))).toBe(true);
});

test('clears stale cross-process lock when pid is not running', () => {
process.env.HAPI_HOME = testHome;
writeLegacyAcpLock(99999999);

expect(isAgentAcpTransportActive()).toBe(false);
expect(existsSync(lockDir())).toBe(false);
});

test('keeps legacy lock when pid file points at a live process', () => {
process.env.HAPI_HOME = testHome;
writeLegacyAcpLock(process.pid);

expect(isAgentAcpTransportActive()).toBe(true);
expect(existsSync(lockDir())).toBe(true);
});

test('clears lock when pid file is missing or invalid', () => {
test('clears refcount lock when pid entries are missing or invalid', () => {
process.env.HAPI_HOME = testHome;
const dir = lockDir();
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'count'), '1', 'utf8');

expect(isAgentAcpTransportActive()).toBe(false);
expect(existsSync(dir)).toBe(false);
});

test('clears refcount lock when all pid entries are stale', () => {
process.env.HAPI_HOME = testHome;
writeTestAcpLock({ count: 2, pids: [99999998, 99999999] });

expect(isAgentAcpTransportActive()).toBe(false);
expect(existsSync(lockDir())).toBe(false);
});

test('reconciles refcount lock down to live pid entries', () => {
process.env.HAPI_HOME = testHome;
writeTestAcpLock({ count: 3, pids: [process.pid, 99999999] });

expect(isAgentAcpTransportActive()).toBe(true);
expect(existsSync(lockDir())).toBe(true);
});
});
Loading
Loading