Skip to content

Commit df152f8

Browse files
added codex support
1 parent b71e4d0 commit df152f8

28 files changed

Lines changed: 1330 additions & 148 deletions

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "hlid",
33
"description": "Hliðskjálf - Watcher of Worlds",
4-
"version": "0.0.72",
4+
"version": "0.0.73",
55
"private": true,
66
"type": "module",
77
"imports": {
@@ -16,10 +16,10 @@
1616
"postinstall": "bun scripts/embed-client.ts",
1717
"start": "bun src/server/index.ts",
1818
"preview": "vite preview",
19-
"test": "vitest run",
20-
"test:watch": "vitest",
19+
"test": "node ./node_modules/vitest/vitest.mjs run",
20+
"test:watch": "node ./node_modules/vitest/vitest.mjs",
2121
"test:db": "bun test src/db/",
22-
"test:all": "vitest run && bun test src/db/",
22+
"test:all": "bun run test && bun test src/db/",
2323
"format": "biome format",
2424
"lint": "biome lint",
2525
"check": "biome check",
@@ -96,4 +96,4 @@
9696
"ip-address": "^10.2.0",
9797
"@anthropic-ai/sdk": "^0.91.1"
9898
}
99-
}
99+
}

src/components/chat/ToolBlock.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,24 @@ describe("ToolBlock — expanded", () => {
7272
fireEvent.click(screen.getByRole("button", { expanded: false }));
7373
expect(screen.getByText(/^Error$/i)).not.toBeNull();
7474
});
75+
76+
it("renders Reasoning as prose without an empty input panel", () => {
77+
render(
78+
<ToolBlock
79+
event={makeEvent({
80+
name: "Reasoning",
81+
input: {},
82+
result: "Checking the repo layout before editing.",
83+
})}
84+
/>,
85+
);
86+
fireEvent.click(screen.getByRole("button", { expanded: false }));
87+
expect(screen.getAllByText(/^Reasoning$/i)).toHaveLength(2);
88+
expect(
89+
screen.getByText("Checking the repo layout before editing."),
90+
).not.toBeNull();
91+
expect(document.querySelector("pre")).toBeNull();
92+
});
7593
});
7694

7795
describe("looksLikeMarkdown", () => {

src/components/chat/ToolBlock.tsx

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,16 @@ export function ToolBlock({
6363
permissionLabel?: string;
6464
}) {
6565
const [open, setOpen] = useState(false);
66-
const pills = Object.entries(event.input ?? {}).slice(0, 3);
66+
const inputEntries = Object.entries(event.input ?? {});
67+
const pills = inputEntries.slice(0, 3);
68+
const isReasoning = event.name === "Reasoning";
6769
const hasResult = typeof event.result === "string";
6870
const resultText = event.result ?? "";
6971
const strippedResult = stripReadLineNumbers(resultText);
7072
const renderResultAsMarkdown =
71-
hasResult && !event.isError && looksLikeMarkdown(strippedResult);
73+
hasResult &&
74+
!event.isError &&
75+
(isReasoning || looksLikeMarkdown(strippedResult));
7276
const resultPreview = hasResult
7377
? firstLine(resultText).slice(0, RESULT_PREVIEW_CHARS)
7478
: null;
@@ -132,32 +136,40 @@ export function ToolBlock({
132136
)}
133137
{open && (
134138
<PrivacyMask className="mx-3 mb-1.5 border border-[var(--tool-panel-border)] bg-[var(--tool-panel)]">
135-
<div className="text-[11px] text-primary/60 font-mono leading-relaxed p-3 overflow-auto max-h-48 space-y-1">
136-
{Object.entries(event.input ?? {}).map(([k, v]) => (
137-
<div key={k} className="flex gap-1.5 min-w-0">
138-
<span className="text-primary/40 shrink-0">{k}:</span>
139-
{typeof v === "string" ? (
140-
<span className="whitespace-pre-wrap break-words min-w-0">
141-
{v}
142-
</span>
143-
) : (
144-
<span className="whitespace-pre-wrap break-words min-w-0">
145-
{JSON.stringify(v, null, 2)}
146-
</span>
147-
)}
148-
</div>
149-
))}
150-
</div>
139+
{inputEntries.length > 0 && (
140+
<div className="text-[11px] text-primary/60 font-mono leading-relaxed p-3 overflow-auto max-h-48 space-y-1">
141+
{inputEntries.map(([k, v]) => (
142+
<div key={k} className="flex gap-1.5 min-w-0">
143+
<span className="text-primary/40 shrink-0">{k}:</span>
144+
{typeof v === "string" ? (
145+
<span className="whitespace-pre-wrap break-words min-w-0">
146+
{v}
147+
</span>
148+
) : (
149+
<span className="whitespace-pre-wrap break-words min-w-0">
150+
{JSON.stringify(v, null, 2)}
151+
</span>
152+
)}
153+
</div>
154+
))}
155+
</div>
156+
)}
151157
{hasResult && (
152-
<div className="border-t border-[var(--tool-panel-border)]">
158+
<div
159+
className={
160+
inputEntries.length > 0
161+
? "border-t border-[var(--tool-panel-border)]"
162+
: undefined
163+
}
164+
>
153165
<div
154166
className={`text-[9px] tracking-widest uppercase px-3 pt-2 pb-1 ${
155167
event.isError
156168
? "text-destructive/70"
157169
: "text-muted-foreground/50"
158170
}`}
159171
>
160-
{event.isError ? "Error" : "Result"}
172+
{event.isError ? "Error" : isReasoning ? "Reasoning" : "Result"}
161173
</div>
162174
{renderResultAsMarkdown ? (
163175
<div className="px-3 pb-3 overflow-auto max-h-64 text-[12px] text-primary/80 leading-relaxed">

src/components/einherjar/AddAgentPanel.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export function AddAgentPanel({
5959
const modelOptions = activeProvider?.models ?? [];
6060
const effortOptions = activeProvider?.effortLevels ?? [];
6161
const permissionOptions = activeProvider?.permissionModes ?? [];
62+
const isClaudeProvider = activeProvider?.id === "claude";
6263

6364
async function handleSubmit() {
6465
if (!form.path.trim()) {
@@ -296,7 +297,11 @@ export function AddAgentPanel({
296297
}
297298
className="flex-1 bg-secondary border border-border px-2 py-1 text-xs font-mono text-foreground focus:outline-none focus:border-primary/50 transition-colors appearance-none cursor-pointer"
298299
>
299-
<option value="">— default (haiku) —</option>
300+
<option value="">
301+
{isClaudeProvider
302+
? "— default (haiku) —"
303+
: "— provider default —"}
304+
</option>
300305
{modelOptions.map((m) => (
301306
<option key={m.value} value={m.value}>
302307
{m.label}

src/components/einherjar/AgentCard.tsx

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export function AgentCard({
8787
const modelOptions = activeProvider?.models ?? [];
8888
const effortOptions = activeProvider?.effortLevels ?? [];
8989
const permissionOptions = activeProvider?.permissionModes ?? [];
90+
const isClaudeProvider = activeProvider?.id === "claude";
9091

9192
async function handleToggleView() {
9293
if (expanded) {
@@ -120,7 +121,10 @@ export function AgentCard({
120121
maxTurns: editing.maxTurns || undefined,
121122
permissionMode: editing.permissionMode || undefined,
122123
recapModel: editing.recapModel || undefined,
123-
interactiveMode: editing.interactiveMode || undefined,
124+
interactiveMode:
125+
editing.provider === "claude"
126+
? editing.interactiveMode || undefined
127+
: undefined,
124128
},
125129
);
126130
setEditing(null);
@@ -442,35 +446,45 @@ export function AgentCard({
442446
}
443447
className="flex-1 bg-secondary border border-border px-2 py-1 text-xs font-mono text-foreground focus:outline-none focus:border-primary/50 transition-colors appearance-none cursor-pointer"
444448
>
445-
<option value="">— default (haiku) —</option>
449+
<option value="">
450+
{isClaudeProvider
451+
? "— default (haiku) —"
452+
: "— provider default —"}
453+
</option>
446454
{modelOptions.map((m) => (
447455
<option key={m.value} value={m.value}>
448456
{m.label}
449457
</option>
450458
))}
451459
</select>
452460
</div>
453-
<div className="flex items-center gap-2">
454-
<span className="text-[9px] tracking-widest text-muted-foreground/50 uppercase shrink-0 w-24">
455-
Interactive mode
456-
</span>
457-
<label className="flex items-center gap-2 cursor-pointer">
458-
<input
459-
type="checkbox"
460-
checked={editing.interactiveMode}
461-
onChange={(e) =>
462-
setEditing(
463-
(s) => s && { ...s, interactiveMode: e.target.checked },
464-
)
465-
}
466-
className="w-3.5 h-3.5 accent-primary"
467-
/>
468-
<span className="text-xs text-muted-foreground">
469-
to not go against your &quot;programmatic&quot; usage, if
470-
you desire
461+
{editing.provider === "claude" && (
462+
<div className="flex items-center gap-2">
463+
<span className="text-[9px] tracking-widest text-muted-foreground/50 uppercase shrink-0 w-24">
464+
Interactive mode
471465
</span>
472-
</label>
473-
</div>
466+
<label className="flex items-center gap-2 cursor-pointer">
467+
<input
468+
type="checkbox"
469+
checked={editing.interactiveMode}
470+
onChange={(e) =>
471+
setEditing(
472+
(s) =>
473+
s && {
474+
...s,
475+
interactiveMode: e.target.checked,
476+
},
477+
)
478+
}
479+
className="w-3.5 h-3.5 accent-primary"
480+
/>
481+
<span className="text-xs text-muted-foreground">
482+
to not go against your &quot;programmatic&quot; usage, if
483+
you desire
484+
</span>
485+
</label>
486+
</div>
487+
)}
474488
</div>
475489
)}
476490
<div className="flex items-center gap-2 pt-1">

src/components/forge/ClaudeSection.tsx

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export function ClaudeSection({
2828
const modelOptions = activeProvider?.models ?? [];
2929
const effortOptions = activeProvider?.effortLevels ?? [];
3030
const permissionOptions = activeProvider?.permissionModes ?? [];
31+
const isClaude = claude.vaultProvider === "claude";
32+
const allowsProviderDefaultModel = !isClaude;
3133
// Show provider-specific settings only when the provider declares capabilities.
3234
const hasProviderOptions =
3335
modelOptions.length > 0 ||
@@ -69,6 +71,9 @@ export function ClaudeSection({
6971
onChange={(e) => onChange({ model: e.target.value })}
7072
className="w-32 sm:w-48 bg-secondary border border-border px-2.5 py-1.5 text-xs font-mono text-foreground focus:outline-none focus:border-primary/50 transition-colors appearance-none cursor-pointer"
7173
>
74+
{allowsProviderDefaultModel && (
75+
<option value="">— provider default —</option>
76+
)}
7277
{modelOptions.map((m) => (
7378
<option key={m.value} value={m.value}>
7479
{m.label}
@@ -160,7 +165,7 @@ export function ClaudeSection({
160165
)}
161166
<Field
162167
label="Max turns"
163-
hint="max turns Claude can run, blank means no limit"
168+
hint="max turns the provider can run, blank means no limit"
164169
>
165170
<input
166171
type="number"
@@ -183,7 +188,7 @@ export function ClaudeSection({
183188
</Field>
184189
<Field
185190
label="Turn recaps"
186-
hint="generate a brief Haiku summary after turns with tool use"
191+
hint="generate a brief summary after turns with tool use"
187192
>
188193
<label className="flex items-center gap-2 cursor-pointer">
189194
<input
@@ -201,30 +206,32 @@ export function ClaudeSection({
201206
onChange={(e) => onChange({ recapModel: e.target.value })}
202207
className="w-32 sm:w-48 bg-secondary border border-border px-2.5 py-1.5 text-xs font-mono text-foreground focus:outline-none focus:border-primary/50 transition-colors appearance-none cursor-pointer"
203208
>
204-
<option value="">default (haiku)</option>
209+
<option value="">provider default</option>
205210
{modelOptions.map((m) => (
206211
<option key={m.value} value={m.value}>
207212
{m.label}
208213
</option>
209214
))}
210215
</select>
211216
</Field>
212-
<Field
213-
label="Interactive mode"
214-
hint="to not go against your &quot;programmatic&quot; usage, if you desire"
215-
>
216-
<label className="flex items-center gap-2 cursor-pointer">
217-
<input
218-
type="checkbox"
219-
checked={claude.interactiveMode}
220-
onChange={(e) =>
221-
onChange({ interactiveMode: e.target.checked })
222-
}
223-
className="w-3.5 h-3.5 accent-primary"
224-
/>
225-
<span className="text-xs text-muted-foreground">enabled</span>
226-
</label>
227-
</Field>
217+
{isClaude && (
218+
<Field
219+
label="Interactive mode"
220+
hint="to not go against your &quot;programmatic&quot; usage, if you desire"
221+
>
222+
<label className="flex items-center gap-2 cursor-pointer">
223+
<input
224+
type="checkbox"
225+
checked={claude.interactiveMode}
226+
onChange={(e) =>
227+
onChange({ interactiveMode: e.target.checked })
228+
}
229+
className="w-3.5 h-3.5 accent-primary"
230+
/>
231+
<span className="text-xs text-muted-foreground">enabled</span>
232+
</label>
233+
</Field>
234+
)}
228235
</>
229236
)}
230237
</Section>

src/config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ const ClaudeSchema = z.object({
4545
interactive_mode: z.boolean().default(false),
4646
});
4747

48+
const CodexSchema = z.object({
49+
model: z.string().default(""),
50+
effort: z.enum(["low", "medium", "high", "xhigh", "max"]).default("medium"),
51+
max_turns: z.number().int().positive().optional(),
52+
permission_mode: z
53+
.enum(["default", "acceptEdits", "bypassPermissions", "plan"])
54+
.default("default"),
55+
turn_recaps: z.boolean().default(true),
56+
recap_model: z.string().optional(),
57+
executable: z.string().optional(),
58+
});
59+
4860
const UiSchema = z.object({
4961
enter_to_submit: z.boolean().default(true),
5062
hide_skills_index: z.boolean().default(true),
@@ -125,6 +137,12 @@ export const HlidConfigSchema = z.object({
125137
turn_recaps: true,
126138
interactive_mode: false,
127139
})),
140+
codex: CodexSchema.default(() => ({
141+
model: "",
142+
effort: "medium" as const,
143+
permission_mode: "default" as const,
144+
turn_recaps: true,
145+
})),
128146
ui: UiSchema.default(() => ({
129147
enter_to_submit: true,
130148
hide_skills_index: true,

0 commit comments

Comments
 (0)