Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,10 @@ jobs:
- name: Install iii CLI + engine
if: steps.optout.outputs.skip != 'true'
env:
VERSION: '0.17.0'
# Must be a tag that still exists on iii-hq/iii (old releases get
# pruned — 0.17.0 vanished and broke every boot smoke). Keep aligned
# with the workers' iii-sdk pin.
VERSION: '0.21.6'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/create-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ on:
- lsp-vscode
- image-resize
- llm-router
- fp
- mcp
- memory
- memory-consolidate
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ on:
- 'lsp/v*'
- 'image-resize/v*'
- 'llm-router/v*'
- 'fp/v*'
- 'mcp/v*'
- 'memory/v*'
- 'memory-consolidate/v*'
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ npx skills add iii-hq/iii --all
| [`lsp`](lsp/) | Rust | Language Server for iii function ids, trigger configs, and worker discovery. Autocomplete / hover across JS/TS, Python, Rust. |
| [`lsp-vscode`](lsp-vscode/) | Node | VS Code extension package `iii-lsp`, embedding the `lsp` server. |
| [`image-resize`](image-resize/) | Rust | Image resize via channel I/O — JPEG/PNG/WebP with EXIF auto-orient, scale-to-fit / crop-to-fit. |
| [`fp`](fp/) | Rust | Lodash-style value transforms (`fp::get`/`pick`/`take`/…) and `fp::pipe` — worker-side pipelines that move big values function→function without routing them through the model. Injects its usage guidance via the harness `pre-generate` hook. |
| [`llm-router`](llm-router/) | Rust | One front door + provider protocol in front of every LLM provider — `router::chat`/`router::complete`/`router::embed`, provider registry + credentials, model catalog, and routing. See [`llm-router/README.md`](llm-router/README.md). |
| [`mcp`](mcp/) | Rust | MCP 2025-06-18 Streamable HTTP bridge — exposes iii functions tagged `mcp.expose` as MCP tools. |
| [`memory`](memory/) | Rust | Durable cross-session agent memory — named banks of always-injected markdown rules and auto-extracted memories, hybrid BM25 + entity + semantic recall, pinning, supersede-never-delete history, and two live trigger types. Plain files on disk; binds the harness `pre-generate` hook for injection and `turn-completed` for background capture. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { coderFixtures } from '@/stories/fixtures/coder-fixtures'
import { directoryFixtures } from '@/stories/fixtures/directory-fixtures'
import { engineFixtures } from '@/stories/fixtures/engine-fixtures'
import { harnessFixtures } from '@/stories/fixtures/harness-fixtures'
import { fpFixtures } from '@/stories/fixtures/fp-fixtures'
import { routerFixtures } from '@/stories/fixtures/router-fixtures'
import { sandboxFixtures } from '@/stories/fixtures/sandbox-fixtures'
import { scraplingFixtures } from '@/stories/fixtures/scrapling-fixtures'
Expand Down Expand Up @@ -207,6 +208,11 @@ export const HarnessFamily: Story = {
render: () => <FamilyGallery fixtures={harnessFixtures} />,
}

export const FpFamily: Story = {
name: 'fp family',
render: () => <FamilyGallery fixtures={fpFixtures} />,
}

export const StateFamily: Story = {
name: 'state family',
render: () => <FamilyGallery fixtures={stateFixtures} />,
Expand Down
125 changes: 125 additions & 0 deletions console/web/src/components/chat/fp/PipeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { FilterChip } from '@/components/chat/engine/shared'
import { MetaRow, StatusPill } from '@/components/chat/sandbox/shared'
import {
type PipeResponse,
pipeRequestSchema,
pipeResponseSchema,
safeParseRequest,
} from './parsers'

interface PipeViewProps {
input: unknown
/** Already unwrapped once by the dispatcher — never unwrap again. */
output?: unknown
running?: boolean
}

/** Single-line payload summary under each step; empty payloads render
nothing (the raw json tab keeps the full call). */
function compactPayload(payload: unknown): string | null {
if (payload == null) return null
if (
typeof payload === 'object' &&
!Array.isArray(payload) &&
Object.keys(payload as Record<string, unknown>).length === 0
) {
return null
}
return JSON.stringify(payload)
}

/** Dim any `ns::` prefix so the op tail reads clearly (`state::set`,
`fp::get`), mirroring the header id labels. */
function StepFunctionLabel({ functionId }: { functionId: string }) {
const splitAt = functionId.lastIndexOf('::')
if (splitAt <= 0) return <span className="text-ink">{functionId}</span>
return (
<>
<span className="text-ink-faint">{functionId.slice(0, splitAt + 2)}</span>
<span className="text-ink">{functionId.slice(splitAt + 2)}</span>
</>
)
}

/** `fp::pipe` — the step route with per-step receipt sizes, then the
final value preview. The threaded values themselves never reach the chat
(that is the point of the pipe), so there is no value pane. */
export function PipeView({ input, output, running }: PipeViewProps) {
const req = safeParseRequest(pipeRequestSchema, input)
if (!req?.through?.length) return null

const parsed = running ? null : pipeResponseSchema.safeParse(output)
const res: PipeResponse | null = parsed?.success ? parsed.data : null
const receipts = res?.steps ?? null
const threaded = receipts?.length
? receipts[receipts.length - 1]?.chars
: null

return (
<div className="border-t border-rule-2 bg-bg">
<MetaRow>
{/* only claim ok once receipts exist — approval previews render
this view before the pipe has run */}
<StatusPill
label={running ? 'piping…' : receipts ? 'pipe ok' : 'pipe'}
variant={!running && receipts ? 'accent' : 'default'}
/>
<FilterChip label="steps" value={String(req.through.length)} />
{threaded != null ? (
<FilterChip
label="threaded"
value={`${threaded.toLocaleString()} chars`}
/>
) : null}
</MetaRow>
<ul className="divide-y divide-rule-2">
{req.through.map((step, i) => {
const receipt = receipts?.[i]
const payload = compactPayload(step.payload)
return (
<li
key={`${i}-${step.function}`}
className="px-3 py-1.5 font-mono text-[12.5px]"
>
<div className="flex items-center gap-2">
<span className="w-4 shrink-0 text-right text-ink-ghost">
{i + 1}
</span>
<span className="min-w-0 truncate">
<StepFunctionLabel functionId={step.function} />
</span>
{step.into ? (
<FilterChip label="into" value={step.into} />
) : null}
{receipt?.chars != null ? (
<span className="ml-auto shrink-0 text-[11px] text-ink-faint">
→ {receipt.chars.toLocaleString()} chars
</span>
) : null}
</div>
{payload ? (
<div className="mt-0.5 break-all pl-6 text-[11px] leading-[1.5] text-ink-faint line-clamp-2">
{payload}
</div>
) : null}
</li>
)
})}
</ul>
{running ? (
<div className="px-3 py-3 font-mono text-[12.5px] text-ink-ghost animate-pulse">
· piping…
</div>
) : res?.value_preview ? (
<div>
<div className="px-3 pt-2 font-mono text-[10px] uppercase tracking-[0.06em] text-ink-ghost">
preview
</div>
<pre className="overflow-x-auto whitespace-pre-wrap break-all px-3 py-2 font-mono text-[12.5px] leading-[1.55] text-ink">
{res.value_preview}
</pre>
</div>
) : null}
</div>
)
}
81 changes: 81 additions & 0 deletions console/web/src/components/chat/fp/UtilView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { FilterChip } from '@/components/chat/engine/shared'
import { MetaRow, StatusPill } from '@/components/chat/sandbox/shared'
import { JsonHighlight } from '@/lib/syntax'
import {
safeParseRequest,
transformOp,
unwrapEnvelope,
utilRequestSchema,
utilResponseSchema,
} from './parsers'

interface UtilViewProps {
functionId: string
input: unknown
output: unknown
running?: boolean
}

/** `null`, `undefined`, `""`, `[]`, `{}` render as a compact "empty" note
* rather than a noisy JSON block. Mirrors `StateView`. */
function isEmptyValue(v: unknown): boolean {
if (v === null || v === undefined) return true
if (typeof v === 'string') return v.length === 0
if (Array.isArray(v)) return v.length === 0
if (typeof v === 'object') {
return Object.keys(v as Record<string, unknown>).length === 0
}
return false
}

/** `fp::*` — the seventeen lodash-style transforms (`FP_TRANSFORM_OPS`).
Op label + its params as chips, then the transformed value. */
export function UtilView({ functionId, input, output, running }: UtilViewProps) {
const req = safeParseRequest(utilRequestSchema, input)
const op = transformOp(functionId)

// Success details is the `UtilResponse { value }` wrapper — show the value.
const unwrapped = running ? undefined : unwrapEnvelope(output)
const parsed = utilResponseSchema.safeParse(unwrapped)
const value = parsed.success ? parsed.data.value : unwrapped
const empty = !running && isEmptyValue(value)

return (
<div className="border-t border-rule-2 bg-bg">
<MetaRow>
<StatusPill label={op} variant={running ? 'default' : 'accent'} />
{req?.path != null ? (
<FilterChip label="path" value={req.path} />
) : null}
{req?.paths?.length ? (
<FilterChip label="paths" value={req.paths.join(', ')} />
) : null}
{req?.n != null ? <FilterChip label="n" value={String(req.n)} /> : null}
{req?.separator != null ? (
<FilterChip label="sep" value={JSON.stringify(req.separator)} />
) : null}
{req?.matches ? (
<FilterChip label="matches" value={JSON.stringify(req.matches)} />
) : null}
{req !== null && req.default !== undefined ? (
<FilterChip label="default" value={JSON.stringify(req.default)} />
) : null}
</MetaRow>
{running ? (
<div className="px-3 py-3 font-mono text-[12.5px] text-ink-ghost animate-pulse">
· running…
</div>
) : empty ? (
<div className="px-3 py-3 font-mono text-[12.5px] text-ink-ghost">
· empty
</div>
) : typeof value === 'string' ? (
<pre className="overflow-x-auto whitespace-pre-wrap break-all px-3 py-2 font-mono text-[12.5px] leading-[1.55] text-ink">
{value}
</pre>
) : (
<JsonHighlight code={JSON.stringify(value ?? null, null, 2)} />
)}
</div>
)
}
Loading
Loading