Skip to content

Commit e4d68b4

Browse files
yoyowormsHAPI
andcommitted
merge: upstream/main, adopt upstream attention indicators, drop our unread
Pulls in 24 upstream commits including session list status indicators (attention + scheduled), lightbox image preview, agent flavor icons in session header, Claude effort options, contextWindow propagation, permissionMode persistence across hub restart, voice picker overhaul, unique assistant-ui message IDs, and a heap of smaller fixes. Upstream's PR tiann#699 (session-list status indicators) overlaps with the unread badge we shipped in d530ee2. Per user direction, dropped our implementation in favor of upstream's classifySessionAttention (which distinguishes permission > input > background > unread per session) plus the localStorage-backed sessionLastSeen tracker. Our cross-device "unreadAt" metadata field, hub markSessionUnread/Read, /mark-read route, NotificationHub mark-unread hooks, and SessionChat auto-clear effect are all removed. Kept from the fork: - Recent (12h) section + cross-device pin (server-side metadata.pinnedAt) - YOLO default for cursor / opencode permission mode - Continue + auto-discovery fallback for resume in spawnSession - usage/accountStatus on Session, UsageBar in settings, claude-mem auto-status, Claude 4.6/4.7 1M presets, etc. Conflict resolutions of note: - SessionList.tsx: combined our Recent+Pin rendering with upstream's SessionAttentionIndicator; SessionItem now accepts both isPinned/onTogglePin and showDetailedStatus. - syncEngine.spawnSession: kept our useContinue + listAgentSessions fallback, took upstream's preferredPermissionMode formula and threaded it through the spawn call. - applySessionConfig: kept our legacy `applied.model` backfill BEFORE upstream's strict requested-key check. - StatusBar: kept our usage/accountStatus block, took upstream's opencode reasoning label extension. - SessionHeader: took upstream's AgentFlavorIcon, kept our getAgentDisplayName for the label. - UserMessage: kept our timestamp / queued / metadata-toggle bubble, replaced upstream's simpler bubble; reintroduced toggleMetadata/onMetadataKeyDown callbacks that were stale in the fork commit. - settings/index.tsx: kept our UsageBar/formatResetTime + upstream's PlayIcon/StopIcon, deduped useAppContext import and `api` destructure. - MessageAttachments: kept upstream's ImagePreview lightbox but with overflow-clip (so trackpad scroll doesn't latch on the image). - permissionModePersistence test mock: extended spawnSession mock signature with _sandbox/_continueLatest so the new 13th-arg permissionMode is captured. - modelOptions.test: expected arrays now include our claude-opus-4-6 and 4-7 1M presets. Build 48 -> 49. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
2 parents d530ee2 + 6200ae1 commit e4d68b4

171 files changed

Lines changed: 5982 additions & 1140 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
125 KB
Loading
132 KB
Loading
414 KB
Loading
325 KB
Loading
419 KB
Loading

CONTRIBUTING.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,7 @@ Thank you for your interest in contributing to HAPI! We welcome bug fixes, featu
1111

1212
## AI-Generated Code Policy (Vibe Coding)
1313

14-
If you are using AI tools to generate code ("Vibe Coding"), please note:
15-
16-
**We only accept code generated by the GPT-5.4 model.**
17-
18-
Pull requests containing code generated by other AI models (including other GPT versions, Claude, Gemini, or any other LLM) will be rejected. This policy helps us maintain consistency and quality standards across the codebase.
19-
20-
If you're unsure whether your AI-assisted code meets this requirement, please open an issue to discuss before submitting.
14+
If you used AI tools to generate code ("Vibe Coding"), please disclose the model in your PR description. Code is judged on merit — focused scope, tests, and the other expectations below apply equally regardless of how the code was authored.
2115

2216
## Pull Request Guidelines
2317

bun.lock

Lines changed: 11 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/src/agent/backends/acp/AcpSdkBackend.test.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,45 @@ describe('AcpSdkBackend', () => {
155155
});
156156
});
157157

158+
it('captures model metadata from configOptions when models block is missing', async () => {
159+
const backend = new AcpSdkBackend({ command: 'opencode' });
160+
const backendInternal = backend as unknown as {
161+
transport: { sendRequest: (method: string, params: unknown) => Promise<unknown>; close: () => Promise<void> } | null;
162+
};
163+
backendInternal.transport = {
164+
sendRequest: async (method) => {
165+
if (method === 'session/new') {
166+
return {
167+
sessionId: 'opencode-session-config-options',
168+
configOptions: [
169+
{
170+
id: 'model',
171+
category: 'model',
172+
currentValue: 'opencode/big-pickle',
173+
options: [
174+
{ value: 'opencode/big-pickle', name: 'OpenCode Zen/Big Pickle' },
175+
{ value: 'deepseek/deepseek-chat', name: 'DeepSeek/DeepSeek Chat' }
176+
]
177+
}
178+
]
179+
};
180+
}
181+
return null;
182+
},
183+
close: async () => {}
184+
};
185+
186+
const sessionId = await backend.newSession({ cwd: '/tmp/x', mcpServers: [] });
187+
188+
expect(backend.getSessionModelsMetadata(sessionId)).toEqual({
189+
availableModels: [
190+
{ modelId: 'opencode/big-pickle', name: 'OpenCode Zen/Big Pickle' },
191+
{ modelId: 'deepseek/deepseek-chat', name: 'DeepSeek/DeepSeek Chat' }
192+
],
193+
currentModelId: 'opencode/big-pickle'
194+
});
195+
});
196+
158197
it('returns undefined session metadata when session/new omits models', async () => {
159198
const backend = new AcpSdkBackend({ command: 'gemini' });
160199
const backendInternal = backend as unknown as {
@@ -213,6 +252,67 @@ describe('AcpSdkBackend', () => {
213252
});
214253
});
215254

255+
256+
257+
it('captures and sets OpenCode thought-level config option', async () => {
258+
const backend = new AcpSdkBackend({ command: 'opencode' });
259+
const calls: Array<{ method: string; params: unknown }> = [];
260+
const backendInternal = backend as unknown as {
261+
transport: { sendRequest: (method: string, params: unknown) => Promise<unknown>; close: () => Promise<void> } | null;
262+
};
263+
backendInternal.transport = {
264+
sendRequest: async (method, params) => {
265+
calls.push({ method, params });
266+
if (method === 'session/new') {
267+
return {
268+
sessionId: 's1',
269+
configOptions: [{
270+
id: 'effort',
271+
name: 'Effort',
272+
category: 'thought_level',
273+
type: 'select',
274+
currentValue: 'low',
275+
options: [
276+
{ value: 'low', name: 'Low' },
277+
{ value: 'high', name: 'High' }
278+
]
279+
}]
280+
};
281+
}
282+
if (method === 'session/set_config_option') {
283+
return {
284+
configOptions: [{
285+
id: 'effort',
286+
category: 'thought_level',
287+
currentValue: 'high',
288+
options: [{ value: 'high', name: 'High' }]
289+
}]
290+
};
291+
}
292+
return null;
293+
},
294+
close: async () => {}
295+
};
296+
297+
await backend.newSession({ cwd: '/tmp/x', mcpServers: [] });
298+
expect(backend.getThoughtLevelConfigOption('s1')).toMatchObject({
299+
id: 'effort',
300+
currentValue: 'low',
301+
options: [{ value: 'low', name: 'Low' }, { value: 'high', name: 'High' }]
302+
});
303+
304+
await backend.setConfigOption('s1', 'effort', 'high');
305+
306+
expect(calls).toContainEqual({
307+
method: 'session/set_config_option',
308+
params: { sessionId: 's1', configId: 'effort', value: 'high' }
309+
});
310+
expect(backend.getThoughtLevelConfigOption('s1')).toMatchObject({
311+
id: 'effort',
312+
currentValue: 'high'
313+
});
314+
});
315+
216316
it('emits turn_complete after trailing tool updates from the same turn', async () => {
217317
backendStatics.UPDATE_QUIET_PERIOD_MS = 25;
218318
backendStatics.UPDATE_DRAIN_TIMEOUT_MS = 200;
@@ -284,4 +384,65 @@ describe('AcpSdkBackend', () => {
284384
'turn_complete'
285385
]);
286386
});
387+
388+
it('combines OpenCode usage_update and prompt usage into a usage message', async () => {
389+
backendStatics.UPDATE_QUIET_PERIOD_MS = 25;
390+
backendStatics.UPDATE_DRAIN_TIMEOUT_MS = 200;
391+
backendStatics.PRE_PROMPT_UPDATE_QUIET_PERIOD_MS = 1;
392+
backendStatics.PRE_PROMPT_UPDATE_DRAIN_TIMEOUT_MS = 50;
393+
394+
const backend = new AcpSdkBackend({ command: 'opencode' });
395+
const backendInternal = backend as unknown as {
396+
transport: {
397+
sendRequest: (...args: unknown[]) => Promise<unknown>;
398+
close: () => Promise<void>;
399+
} | null;
400+
handleSessionUpdate: (params: unknown) => void;
401+
};
402+
403+
const messages: AgentMessage[] = [];
404+
backendInternal.transport = {
405+
sendRequest: async () => {
406+
setTimeout(() => {
407+
backendInternal.handleSessionUpdate({
408+
sessionId: 'session-1',
409+
update: {
410+
sessionUpdate: 'usage_update',
411+
used: 13_879,
412+
size: 65_536,
413+
}
414+
});
415+
}, 0);
416+
417+
await sleep(5);
418+
419+
return {
420+
stopReason: 'end_turn',
421+
usage: {
422+
totalTokens: 13_892,
423+
inputTokens: 8_119,
424+
outputTokens: 2,
425+
thoughtTokens: 11,
426+
cachedReadTokens: 5_760
427+
}
428+
};
429+
},
430+
close: async () => {}
431+
};
432+
433+
await backend.prompt('session-1', [{ type: 'text', text: 'hello' }], (message) => {
434+
messages.push(message);
435+
});
436+
437+
expect(messages).toContainEqual({
438+
type: 'usage',
439+
inputTokens: 8_119,
440+
outputTokens: 2,
441+
cacheReadTokens: 5_760,
442+
thoughtTokens: 11,
443+
totalTokens: 13_892,
444+
contextTokens: 13_879,
445+
contextWindow: 65_536
446+
});
447+
});
287448
});

0 commit comments

Comments
 (0)