Skip to content

Commit f249917

Browse files
committed
refactor: simplify codebase and align desktop UI with OpenCode
1 parent 255f12f commit f249917

84 files changed

Lines changed: 2516 additions & 3182 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎scripts/build-app.sh‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ identity=$(hex_codesign_identity "$team_id")
88
icon_output="$root/target/AppIcon.assets"
99
icon_info="$root/target/AppIcon-info.plist"
1010
sparkle_dir=$("$root/scripts/setup-sparkle.sh")
11-
version=${HEX_VERSION:-$(cargo metadata --no-deps --format-version 1 --manifest-path "$root/Cargo.toml" | jq -r '.packages[0].version')}
12-
build_number=${HEX_BUILD_NUMBER:-$(printf '%s\n' "$version" | awk -F. '{ print ($1 * 10000) + ($2 * 100) + $3 }')}
11+
version=$(hex_version "$root")
12+
build_number=$(hex_build_number "$version")
1313
bundle_name=Hex.app
1414
bundle_id=com.kitlangton.hex2
1515
executable_name=hex

‎scripts/build-service-app.sh‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ team_id=${VOICE_CONTROL_TEAM_ID:?Set VOICE_CONTROL_TEAM_ID to the Apple Develope
77
identity=$(hex_codesign_identity "$team_id")
88
bundle="$root/target/HEX Service.app"
99
executable="$bundle/Contents/MacOS/hex-service"
10-
version=${HEX_VERSION:-$(cargo metadata --no-deps --format-version 1 --manifest-path "$root/Cargo.toml" | jq -r '.packages[0].version')}
11-
build_number=${HEX_BUILD_NUMBER:-$(printf '%s\n' "$version" | awk -F. '{ print ($1 * 10000) + ($2 * 100) + $3 }')}
10+
version=$(hex_version "$root")
11+
build_number=$(hex_build_number "$version")
1212

1313
cargo build --release --manifest-path "$root/Cargo.toml"
1414
rm -rf "$bundle"

‎scripts/capture-preview.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ for _ in 0..<200 {
5555
let bounds = window[kCGWindowBounds as String] as? [String: CGFloat]
5656
return (title == nil || title == "" || title == "HEX")
5757
&& (bounds?["Width"] ?? 0) >= 800
58+
&& (bounds?["Height"] ?? 0) >= 400
5859
}
5960
if let window = candidates.max(by: {
6061
let lhs = $0[kCGWindowBounds as String] as! [String: CGFloat]

‎scripts/macos-signing.sh‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ hex_codesign_identity() {
2121
esac
2222
printf '%s\n' "$identity"
2323
}
24+
25+
hex_version() {
26+
printf '%s\n' "${HEX_VERSION:-$(cargo metadata --no-deps --format-version 1 --manifest-path "$1/Cargo.toml" | jq -r '.packages[0].version')}"
27+
}
28+
29+
hex_build_number() {
30+
printf '%s\n' "${HEX_BUILD_NUMBER:-$(printf '%s\n' "$1" | awk -F. '{ print ($1 * 10000) + ($2 * 100) + $3 }')}"
31+
}

‎scripts/prepare-service-app.sh‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
set -eu
33

44
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5+
. "$root/scripts/macos-signing.sh"
56
notary_profile=${HEX_NOTARY_PROFILE:?Set HEX_NOTARY_PROFILE to the matching notarization profile}
6-
version=${HEX_VERSION:-$(cargo metadata --no-deps --format-version 1 --manifest-path "$root/Cargo.toml" | jq -r '.packages[0].version')}
7+
version=$(hex_version "$root")
78
arch=$(uname -m)
89
dist="$root/dist/service"
910
artifact="$dist/HEX-Service-$version-$arch.zip"

‎scripts/release-app.sh‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ set -eu
33

44
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
55
team_id=${VOICE_CONTROL_TEAM_ID:?Set VOICE_CONTROL_TEAM_ID to the Apple Developer signing team}
6+
. "$root/scripts/macos-signing.sh"
67
mode=${1:-prepare}
7-
version=${HEX_VERSION:-$(cargo metadata --no-deps --format-version 1 --manifest-path "$root/Cargo.toml" | jq -r '.packages[0].version')}
8-
build_number=${HEX_BUILD_NUMBER:-$(printf '%s\n' "$version" | awk -F. '{ print ($1 * 10000) + ($2 * 100) + $3 }')}
8+
version=$(hex_version "$root")
9+
build_number=$(hex_build_number "$version")
910
notary_profile=${HEX_NOTARY_PROFILE:?Set HEX_NOTARY_PROFILE to the matching notarization profile}
1011
bucket=${HEX_RELEASE_BUCKET:-hex-releases}
1112
base_url=${HEX_RELEASE_BASE_URL:-https://downloads.hex.kitlangton.dev}
@@ -112,7 +113,6 @@ export HEX_VERSION="$version" HEX_BUILD_NUMBER="$build_number"
112113
bundle=$("$root/scripts/build-app.sh")
113114
bundle_name=$(basename "$bundle")
114115
"$root/scripts/validate-app.sh" "$bundle" Hex.app "$version" "$build_number" >/dev/null
115-
. "$root/scripts/macos-signing.sh"
116116
identity=$(hex_codesign_identity "$team_id")
117117
rm -rf "$dist/staging" "$dist/update-staging" "$updates"
118118
rm -f "$dist/HEX-$version.zip" "$dist/$artifact" "$dist/$update_artifact"

‎scripts/smoke-service-app.sh‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
set -eu
33

44
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5-
version=${HEX_VERSION:-$(cargo metadata --no-deps --format-version 1 --manifest-path "$root/Cargo.toml" | jq -r '.packages[0].version')}
5+
. "$root/scripts/macos-signing.sh"
6+
version=$(hex_version "$root")
67
arch=$(uname -m)
78
artifact=${HEX_SERVICE_ARTIFACT:-$root/dist/service/HEX-Service-$version-$arch.zip}
89
digest="$artifact.sha256"

‎sdk/commands/src/captures.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ const MAX_UNION_MEMBERS = 16
88
const MAX_UNION_DEPTH = 4
99
const MAX_UNION_SERIALIZED_BYTES = 16 * 1024
1010

11-
const utf8Length = (value: string): number => new TextEncoder().encode(value).byteLength
12-
const boundedString = (value: unknown, label: string, maxBytes: number): string => {
11+
export const utf8Length = (value: string): number => Buffer.byteLength(value, "utf8")
12+
export const boundedString = (value: unknown, label: string, maxBytes: number): string => {
1313
if (typeof value !== "string" || value.length === 0 || utf8Length(value) > maxBytes) {
1414
throw new Error(`${label} must be a non-empty string no longer than ${maxBytes} UTF-8 bytes`)
1515
}
1616
return value
1717
}
18-
const record = (value: unknown): Record<string, unknown> | undefined =>
18+
export const record = (value: unknown): Record<string, unknown> | undefined =>
1919
typeof value === "object" && value !== null && !Array.isArray(value)
2020
? Object.fromEntries(Object.entries(value))
2121
: undefined
@@ -34,7 +34,7 @@ const letterAliases = new Set([
3434
])
3535

3636
// Match spoken_text::normalize: ASCII punctuation/case and spoken digits.
37-
export const normalizeSpokenWord = (value: string): string =>
37+
const normalizeSpokenWord = (value: string): string =>
3838
value.split(/\s+/u).flatMap((word) => {
3939
const trimmed = word.replace(
4040
/^[\x21-\x2f\x3a-\x40\x5b-\x60\x7b-\x7e]+|[\x21-\x2f\x3a-\x40\x5b-\x60\x7b-\x7e]+$/g,

‎sdk/commands/src/effect.ts‎

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ import {
1313
union,
1414
} from "./model.js"
1515
import type {
16+
CapturesFor,
1617
CommandDefinitionFor,
1718
CaptureSchema,
18-
CaptureValues,
1919
CommandMetadata,
2020
HandlerContext,
2121
HexCapabilities,
2222
HexConfigFor,
2323
NativeAction,
24+
UntypedCaptures,
2425
} from "./model.js"
2526

2627
export class ToolCallError extends Schema.TaggedError<ToolCallError>()(
@@ -30,12 +31,12 @@ export class ToolCallError extends Schema.TaggedError<ToolCallError>()(
3031

3132
export interface HexService extends HexCapabilities<Effect.Effect<void, ToolCallError>> {
3233
readonly context: HandlerContext
33-
readonly captures: Readonly<Record<string, string | number>>
34+
readonly captures: UntypedCaptures
3435
}
3536

3637
export class Hex extends Context.Service<Hex, HexService>()("@hex/commands/Hex") {}
3738

38-
export interface EffectHandlerArguments<Captures extends Readonly<Record<string, string | number>> = Readonly<Record<string, string>>> {
39+
export interface EffectHandlerArguments<Captures extends UntypedCaptures = Readonly<Record<string, string>>> {
3940
readonly context: HandlerContext
4041
readonly captures: Captures
4142
}
@@ -48,29 +49,23 @@ export type EffectCommandDefinition = CommandDefinitionFor<EffectHandler>
4849

4950
export interface EffectHexConfig extends HexConfigFor<EffectCommandDefinition> {}
5051

51-
type EffectCapturesFor<Schema> = [Schema] extends [never]
52-
? Readonly<Record<string, string>>
53-
: [Schema] extends [CaptureSchema]
54-
? CaptureValues<Extract<NoInfer<Schema>, CaptureSchema>>
55-
: Readonly<Record<string, string>>
56-
5752
type EffectCommandFor<Schema> = CommandMetadata
5853
& { readonly captures?: Schema & CaptureSchema }
5954
& (
6055
| {
6156
readonly action: [Schema] extends [never]
62-
? import("./model.js").NativeAction
63-
: [Schema] extends [CaptureSchema] ? never : import("./model.js").NativeAction
57+
? NativeAction
58+
: [Schema] extends [CaptureSchema] ? never : NativeAction
6459
readonly run?: never
6560
}
6661
| {
6762
readonly action?: never
6863
readonly run: [Schema] extends [CaptureSchema]
6964
? Effect.Effect<void, unknown, Hex>
70-
| ((arguments_: EffectHandlerArguments<EffectCapturesFor<Schema>>) => Effect.Effect<void, unknown, Hex>)
65+
| ((arguments_: EffectHandlerArguments<CapturesFor<Schema>>) => Effect.Effect<void, unknown, Hex>)
7166
: NativeAction
7267
| Effect.Effect<void, unknown, Hex>
73-
| ((arguments_: EffectHandlerArguments<EffectCapturesFor<Schema>>) => Effect.Effect<void, unknown, Hex>)
68+
| ((arguments_: EffectHandlerArguments<CapturesFor<Schema>>) => Effect.Effect<void, unknown, Hex>)
7469
}
7570
)
7671

‎sdk/commands/src/host.ts‎

Lines changed: 35 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { pathToFileURL } from "node:url"
2-
import { normalizeSpokenWord, validateCaptureDescriptor } from "./captures.js"
2+
import { boundedString, record, utf8Length, validateCaptureDescriptor } from "./captures.js"
33
import { Cause, Deferred, Effect, Exit, Fiber } from "effect"
44
import { Hex, ToolCallError } from "./effect.js"
55
import type { HexService } from "./effect.js"
@@ -13,6 +13,7 @@ import {
1313
} from "./model.js"
1414
import type {
1515
HandlerArguments,
16+
HandlerContext,
1617
CaptureDescriptor,
1718
CaptureSchema,
1819
HexCapabilities,
@@ -54,20 +55,6 @@ type HostHandlerFunction = (arguments_: HandlerArguments<UntypedCaptures>) =>
5455
void | Promise<void> | Effect.Effect<void, unknown, Hex>
5556
type HostHandler = HostHandlerFunction | Effect.Effect<void, unknown, Hex>
5657

57-
const utf8Length = (value: string): number => Buffer.byteLength(value, "utf8")
58-
59-
const boundedString = (value: unknown, label: string, maxBytes: number): string => {
60-
if (typeof value !== "string" || value.length === 0 || utf8Length(value) > maxBytes) {
61-
throw new Error(`${label} must be a non-empty string no longer than ${maxBytes} UTF-8 bytes`)
62-
}
63-
return value
64-
}
65-
66-
const record = (value: unknown): Record<string, unknown> | undefined =>
67-
typeof value === "object" && value !== null && !Array.isArray(value)
68-
? Object.fromEntries(Object.entries(value))
69-
: undefined
70-
7158
const isFunction = (value: unknown): value is HostHandlerFunction =>
7259
typeof value === "function"
7360

@@ -356,6 +343,33 @@ const errorMessage = (error: unknown): string => {
356343
: Buffer.from(message).subarray(0, MAX_ERROR_BYTES).toString("utf8")
357344
}
358345

346+
const decodeContext = (
347+
value: unknown,
348+
label: "invoke.context" | "transform.context",
349+
deriveBrowserHost: boolean,
350+
): HandlerContext => {
351+
const context = record(value)
352+
if (context === undefined) throw new Error(`${label} must be an object`)
353+
const application = context.application === undefined
354+
? undefined
355+
: boundedString(context.application, `${label}.application`, MAX_VALUE_BYTES)
356+
const browserUrl = context.browserUrl === undefined
357+
? undefined
358+
: boundedString(context.browserUrl, `${label}.browserUrl`, MAX_VALUE_BYTES)
359+
const windowTitle = context.windowTitle === undefined
360+
? undefined
361+
: boundedString(context.windowTitle, `${label}.windowTitle`, MAX_VALUE_BYTES)
362+
const browserHost = context.browserHost === undefined
363+
? deriveBrowserHost && browserUrl !== undefined ? new URL(browserUrl).hostname : undefined
364+
: boundedString(context.browserHost, `${label}.browserHost`, 253)
365+
return {
366+
...(application === undefined ? {} : { application }),
367+
...(browserHost === undefined ? {} : { browserHost }),
368+
...(browserUrl === undefined ? {} : { browserUrl }),
369+
...(windowTitle === undefined ? {} : { windowTitle }),
370+
}
371+
}
372+
359373
const decodeInput = (line: string): HostInput => {
360374
let value: unknown
361375
try {
@@ -365,21 +379,8 @@ const decodeInput = (line: string): HostInput => {
365379
}
366380
const input = record(value)
367381
switch (input?.type) {
368-
case "invoke":
369-
const context = record(input.context)
370-
if (context === undefined) throw new Error("invoke.context must be an object")
371-
const application = context.application === undefined
372-
? undefined
373-
: boundedString(context.application, "invoke.context.application", MAX_VALUE_BYTES)
374-
const browserUrl = context.browserUrl === undefined
375-
? undefined
376-
: boundedString(context.browserUrl, "invoke.context.browserUrl", MAX_VALUE_BYTES)
377-
const windowTitle = context.windowTitle === undefined
378-
? undefined
379-
: boundedString(context.windowTitle, "invoke.context.windowTitle", MAX_VALUE_BYTES)
380-
const browserHost = context.browserHost === undefined
381-
? browserUrl === undefined ? undefined : new URL(browserUrl).hostname
382-
: boundedString(context.browserHost, "invoke.context.browserHost", 253)
382+
case "invoke": {
383+
const context = decodeContext(input.context, "invoke.context", true)
383384
const rawCaptures = input.captures === undefined ? {} : record(input.captures)
384385
if (rawCaptures === undefined) throw new Error("invoke.captures must be an object")
385386
const captureEntries = Object.entries(rawCaptures)
@@ -397,17 +398,12 @@ const decodeInput = (line: string): HostInput => {
397398
type: "invoke",
398399
invocationId: boundedString(input.invocationId, "invoke.invocationId", MAX_ID_BYTES),
399400
commandId: boundedString(input.commandId, "invoke.commandId", MAX_ID_BYTES),
400-
context: {
401-
...(application === undefined ? {} : { application }),
402-
...(browserHost === undefined ? {} : { browserHost }),
403-
...(browserUrl === undefined ? {} : { browserUrl }),
404-
...(windowTitle === undefined ? {} : { windowTitle }),
405-
},
401+
context,
406402
captures,
407403
}
404+
}
408405
case "transform": {
409-
const context = record(input.context)
410-
if (context === undefined) throw new Error("transform.context must be an object")
406+
const context = decodeContext(input.context, "transform.context", false)
411407
if (!Array.isArray(input.transformationIds)
412408
|| input.transformationIds.length === 0
413409
|| input.transformationIds.length > MAX_TRANSFORMATIONS_PER_INVOCATION) {
@@ -424,20 +420,7 @@ const decodeInput = (line: string): HostInput => {
424420
transformationIds: input.transformationIds.map((id, index) =>
425421
boundedString(id, `transform.transformationIds[${index}]`, MAX_ID_BYTES)),
426422
text,
427-
context: {
428-
...(context.application === undefined ? {} : {
429-
application: boundedString(context.application, "transform.context.application", MAX_VALUE_BYTES),
430-
}),
431-
...(context.browserHost === undefined ? {} : {
432-
browserHost: boundedString(context.browserHost, "transform.context.browserHost", 253),
433-
}),
434-
...(context.browserUrl === undefined ? {} : {
435-
browserUrl: boundedString(context.browserUrl, "transform.context.browserUrl", MAX_VALUE_BYTES),
436-
}),
437-
...(context.windowTitle === undefined ? {} : {
438-
windowTitle: boundedString(context.windowTitle, "transform.context.windowTitle", MAX_VALUE_BYTES),
439-
}),
440-
},
423+
context,
441424
}
442425
}
443426
case "toolResult": {

0 commit comments

Comments
 (0)