Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c87f88f
fix(web): disable single-dollar inline math in remark-math (#805)
heavygee Jun 8, 2026
9349acb
fix(web): use session flavor label in voice context formatters (close…
heavygee Jun 8, 2026
edc3acc
fix(cursor): requeue user message on transient agent exit (auth, rate…
heavygee Jun 8, 2026
cd99cfb
fix(hub): preserve session metadata across archive transitions (#825)
heavygee Jun 8, 2026
cb72703
feat(hub+web): add POST /sessions/:id/reopen + Reopen button on inact…
heavygee Jun 8, 2026
6d2d0d4
fix(cursor): trim #784 safety patch to marker-only on legacy stream-j…
heavygee Jun 8, 2026
ad038bb
fix(cursor): merge SKU catalog under ACP lock and refcount agent guar…
swear01 Jun 8, 2026
8094b50
fix(web): hide sidebar fake sessions for Cursor resume/archive (#836)
swear01 Jun 8, 2026
fa363c2
fix(cursor): register cursorSessionId before ACP session/load (#837)
swear01 Jun 8, 2026
deb05bb
Auto-approve Codex title MCP tool
tiann Jun 8, 2026
393cd7b
feat(web): scratchlist v1.1 — composer-toggle drawer + reusable FUE p…
heavygee Jun 8, 2026
1f92a31
Release version 0.20.1
tiann Jun 8, 2026
e2625b8
feat: add Fable model presets for Claude sessions (#860)
flyinghanger Jun 10, 2026
3473a88
feat(web): auto-return to chat when remote terminal exits (#857)
heavygee Jun 10, 2026
cad58cf
fix(opencode): use ACP-reported reasoning effort options (#853)
swear01 Jun 10, 2026
ddf3a55
fix(web): add missing i18n keys for session.inactive banner (#851)
swear01 Jun 10, 2026
55d1bbb
feat(cursor): invisible sync-on-open migrator from legacy stream-json…
heavygee Jun 10, 2026
a617601
fix(runner): self-restart resilience under systemd / external process…
heavygee Jun 10, 2026
17439ae
fix(cli): bypass proxy for loopback addresses at CLI entrypoint (#868)
tiann Jun 10, 2026
1a54492
fix(cursor): migrator path-priority + ambiguity surface (closes #844 …
heavygee Jun 10, 2026
fe37552
fix(cursor): address cold review on migrator path-priority fix
heavygee Jun 10, 2026
af8c5a8
fix(cursor): cold-review polish on migrator path-priority (tiann/hapi…
heavygee Jun 10, 2026
e2f8b8a
fix(cursor): correct log-search strings in ambiguous banner copy
heavygee Jun 11, 2026
3358658
fix(cursor): address #877 bot Minor findings (trim + boundary guard)
heavygee Jun 11, 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