agentful 0.3.9 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +283 -20
  2. package/package.json +2 -1
package/dist/index.cjs CHANGED
@@ -3040,11 +3040,11 @@ function banner() {
3040
3040
  paint.dim(brand.tagline)
3041
3041
  ].join("\n");
3042
3042
  }
3043
- var VERSION, brand, useColor, wrap, paint, sym, ui;
3043
+ var VERSION, brand, useColor, wrap, paint, tuiDesign, sym, ui;
3044
3044
  var init_branding = __esm({
3045
3045
  "src/branding.ts"() {
3046
3046
  "use strict";
3047
- VERSION = "0.3.9" ? "0.3.9" : null.version;
3047
+ VERSION = "0.4.1" ? "0.4.1" : null.version;
3048
3048
  brand = {
3049
3049
  name: "agentful",
3050
3050
  // the command users type
@@ -3067,6 +3067,12 @@ var init_branding = __esm({
3067
3067
  dim: wrap("2"),
3068
3068
  bold: wrap("1")
3069
3069
  };
3070
+ tuiDesign = {
3071
+ /** Sits in front of the input line. U+276F, the shell prompt sigil. */
3072
+ promptMark: "\u276F",
3073
+ /** Marks an item in progress in the sidebar's task list. U+203A. */
3074
+ activeMark: "\u203A"
3075
+ };
3070
3076
  sym = {
3071
3077
  ok: paint.success("\u2714"),
3072
3078
  fail: paint.error("\u2716"),
@@ -3504,40 +3510,282 @@ function brandingPluginSource() {
3504
3510
  const logoLiteral = JSON.stringify(logo_default.replace(/\n+$/, "").split("\n"));
3505
3511
  return `// Generated by ${brand.name} v${VERSION} \u2014 do not edit; rewritten on every start.
3506
3512
  const LOGO = ${logoLiteral}
3513
+ const MARK = ${JSON.stringify(tuiDesign.promptMark)}
3514
+ const ACTIVE = ${JSON.stringify(tuiDesign.activeMark)}
3515
+ const USD = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
3516
+
3517
+ // Ours, not the engine's generic examples ("Fix a TODO in the codebase").
3518
+ const PLACEHOLDERS = {
3519
+ normal: [
3520
+ 'Build a landing page for my newsletter',
3521
+ 'Add a contact form and push it live',
3522
+ 'What changed since my last push?',
3523
+ ],
3524
+ shell: ['agentful push', 'agentful open', 'git status'],
3525
+ }
3526
+
3527
+ /** Engine state reads are best-effort: a renamed field must not blank the UI. */
3528
+ function safe(fn, fallback) {
3529
+ try {
3530
+ const value = fn()
3531
+ return value === undefined || value === null ? fallback : value
3532
+ } catch (err) {
3533
+ return fallback
3534
+ }
3535
+ }
3536
+
3537
+ /** Keep the tail of a path \u2014 the file name matters more than its parent. */
3538
+ function tail(text, max) {
3539
+ const s = String(text)
3540
+ return s.length <= max ? s : '\u2026' + s.slice(s.length - (max - 1))
3541
+ }
3507
3542
 
3508
3543
  const plugin = {
3509
3544
  id: 'agentful-branding',
3510
3545
  tui(api) {
3546
+ const Prompt = api.ui.Prompt
3547
+ const HostSlot = api.ui.Slot
3548
+ // The slot context carries no colors; the theme lives on the plugin api.
3549
+ const t = () => api.theme.current
3550
+
3551
+ // \u2500\u2500 band pieces \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3552
+ // Sidebar dividers only. The prompt has no rule of its own: the engine
3553
+ // already draws one under it, and a second line above it made the input
3554
+ // look boxed in.
3555
+ function Rule() {
3556
+ return <box height={1} flexShrink={0} border={['top']} borderColor={t().borderSubtle} />
3557
+ }
3558
+
3559
+ function Row(props) {
3560
+ return (
3561
+ <box flexDirection="row" justifyContent="space-between" gap={1}>
3562
+ <text fg={t().textMuted} wrapMode="none">{props.label}</text>
3563
+ <text fg={props.fg || t().text} flexShrink={0} wrapMode="none">{props.value}</text>
3564
+ </box>
3565
+ )
3566
+ }
3567
+
3568
+ function Section(props) {
3569
+ return (
3570
+ <box flexDirection="column" flexShrink={0}>
3571
+ <Rule />
3572
+ <text fg={t().textMuted}>{props.label}</text>
3573
+ {props.children}
3574
+ </box>
3575
+ )
3576
+ }
3577
+
3578
+ // \u2500\u2500 sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3579
+ function Sidebar(props) {
3580
+ const messages = () => safe(() => api.state.session.messages(props.sessionID), [])
3581
+ const usage = () => safe(() => {
3582
+ const last = messages().findLast((m) => m.role === 'assistant' && m.tokens && m.tokens.output > 0)
3583
+ if (!last) return { tokens: 0, percent: null }
3584
+ const tk = last.tokens
3585
+ const total = tk.input + tk.output + tk.reasoning + tk.cache.read + tk.cache.write
3586
+ const provider = api.state.provider.find((p) => p.id === last.providerID)
3587
+ const model = provider && provider.models ? provider.models[last.modelID] : undefined
3588
+ const limit = model && model.limit ? model.limit.context : 0
3589
+ return { tokens: total, percent: limit ? Math.round((total / limit) * 100) : null }
3590
+ }, { tokens: 0, percent: null })
3591
+ // Only shown once it is real: platform models bill credits server-side
3592
+ // and report 0 here, and a permanent "$0.00" reads like a promise.
3593
+ const cost = () => safe(() => api.state.session.get(props.sessionID).cost, 0)
3594
+ const lsp = () => safe(() => api.state.lsp(), [])
3595
+ const lspDisabled = () => safe(() => !api.state.config.lsp, true)
3596
+ const mcp = () => safe(() => api.state.mcp(), [])
3597
+ const todos = () => safe(() => api.state.session.todo(props.sessionID), [])
3598
+ const openTodos = () => todos().filter((item) => item.status !== 'completed')
3599
+ const files = () => safe(() => api.state.session.diff(props.sessionID), [])
3600
+
3601
+ return (
3602
+ <box flexDirection="column" gap={1} flexShrink={0}>
3603
+ <Section label="CONTEXT">
3604
+ <Row label="tokens" value={() => usage().tokens.toLocaleString('en-US')} />
3605
+ {() => usage().percent === null
3606
+ ? null
3607
+ : <Row label="window" value={() => usage().percent + '%'} />}
3608
+ {() => cost() > 0
3609
+ ? <Row label="cost" value={() => USD.format(cost())} />
3610
+ : null}
3611
+ </Section>
3612
+
3613
+ <Section label="TOOLS">
3614
+ <Row
3615
+ label="language server"
3616
+ value={() => lsp().length
3617
+ ? lsp().filter((s) => s.status === 'connected').length + '/' + lsp().length
3618
+ : (lspDisabled() ? 'off' : 'idle')}
3619
+ />
3620
+ {() => mcp().length
3621
+ ? <Row
3622
+ label="mcp"
3623
+ value={() => mcp().filter((s) => s.status === 'connected').length + '/' + mcp().length}
3624
+ />
3625
+ : null}
3626
+ </Section>
3627
+
3628
+ {() => openTodos().length === 0 ? null : (
3629
+ <Section label="TASKS">
3630
+ {() => todos().slice(0, 8).map((item) => (
3631
+ <box flexDirection="row" gap={1}>
3632
+ <text
3633
+ flexShrink={0}
3634
+ fg={item.status === 'completed'
3635
+ ? t().success
3636
+ : item.status === 'in_progress' ? t().primary : t().textMuted}
3637
+ >
3638
+ {item.status === 'completed' ? '\u2713' : item.status === 'in_progress' ? ACTIVE : '\xB7'}
3639
+ </text>
3640
+ <text fg={item.status === 'completed' ? t().textMuted : t().text}>{item.content}</text>
3641
+ </box>
3642
+ ))}
3643
+ {() => todos().length > 8
3644
+ ? <text fg={t().textMuted}>{'+' + (todos().length - 8) + ' more'}</text>
3645
+ : null}
3646
+ </Section>
3647
+ )}
3648
+
3649
+ {() => files().length === 0 ? null : (
3650
+ <Section label="CHANGES">
3651
+ {() => files().slice(0, 8).map((file) => (
3652
+ <box flexDirection="row" gap={1} justifyContent="space-between">
3653
+ <text fg={t().text} wrapMode="none">{tail(file.file, 24)}</text>
3654
+ <box flexDirection="row" gap={1} flexShrink={0}>
3655
+ {file.additions ? <text fg={t().diffAdded}>{'+' + file.additions}</text> : null}
3656
+ {file.deletions ? <text fg={t().diffRemoved}>{'-' + file.deletions}</text> : null}
3657
+ </box>
3658
+ </box>
3659
+ ))}
3660
+ {() => files().length > 8
3661
+ ? <text fg={t().textMuted}>{'+' + (files().length - 8) + ' more'}</text>
3662
+ : null}
3663
+ </Section>
3664
+ )}
3665
+ </box>
3666
+ )
3667
+ }
3668
+
3669
+ // \u2500\u2500 prompt \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3670
+ /** Left half of the line under the input; replaces the engine's plain cwd. */
3671
+ function Hint(props) {
3672
+ const directory = () => safe(
3673
+ () => api.state.session.get(props.sessionID).directory,
3674
+ safe(() => api.state.path.directory, ''),
3675
+ )
3676
+ const branch = () => safe(() => api.state.vcs.branch, '')
3677
+ return (
3678
+ <box flexDirection="row" gap={1} marginLeft={1}>
3679
+ <text fg={t().textMuted} wrapMode="none">{() => directory()}</text>
3680
+ {() => branch() ? <text fg={t().textMuted} wrapMode="none">{'\xB7 ' + branch()}</text> : null}
3681
+ </box>
3682
+ )
3683
+ }
3684
+
3685
+ /**
3686
+ * Marker plus the engine's own input, with the engine's chrome taken off.
3687
+ *
3688
+ * The engine wraps its input in a box with border ["left"] \u2014 the
3689
+ * agent-coloured rail. There is no prop, config key or theme token that
3690
+ * hides it (checked against v1.18.18 and against main), so we clip it
3691
+ * instead: the wrapper hides its overflow and the prompt hangs one column
3692
+ * to the left, which puts the rail outside the visible area. Everything
3693
+ * else keeps working because it is only a shift.
3694
+ *
3695
+ * \u26A0\uFE0F One column is the limit. The status line under the input (our hint
3696
+ * on the left) sits OUTSIDE that padded box and carries a margin of just
3697
+ * 1, so a wider clip eats the start of the working directory \u2014
3698
+ * "/Users/\u2026" came out as "sers/\u2026" at -3. The two columns left between
3699
+ * the marker and the text are the engine's own padding; taking those
3700
+ * would cost the status line.
3701
+ */
3702
+ function Band(props) {
3703
+ return (
3704
+ <box flexDirection="row" width="100%">
3705
+ <box flexShrink={0} paddingTop={1}>
3706
+ <text fg={t().primary}>{MARK}</text>
3707
+ </box>
3708
+ <box flexGrow={1} overflow="hidden">
3709
+ <box marginLeft={-1} width="100%">{props.children}</box>
3710
+ </box>
3711
+ </box>
3712
+ )
3713
+ }
3714
+
3511
3715
  api.slots.register({
3512
3716
  order: 1,
3513
3717
  slots: {
3514
- home_logo(ctx) {
3515
- const accent = ctx && ctx.theme ? ctx.theme.primary : undefined
3516
- const muted = ctx && ctx.theme ? ctx.theme.textMuted : undefined
3718
+ home_logo() {
3517
3719
  return (
3518
3720
  <box flexDirection="column">
3519
3721
  {LOGO.map((line) => (
3520
- <text fg={accent}>{line}</text>
3722
+ <text fg={t().primary}>{line}</text>
3521
3723
  ))}
3522
3724
  <text> </text>
3523
- <text fg={muted}>${brand.tagline}</text>
3725
+ <text fg={t().textMuted}>${brand.tagline}</text>
3726
+ </box>
3727
+ )
3728
+ },
3729
+
3730
+ home_footer() {
3731
+ return <text fg={t().textMuted}>${brand.name} v${VERSION}</text>
3732
+ },
3733
+
3734
+ home_prompt(ctx, data) {
3735
+ return (
3736
+ <Band>
3737
+ <Prompt
3738
+ ref={data.ref}
3739
+ placeholders={PLACEHOLDERS}
3740
+ right={<HostSlot name="home_prompt_right" />}
3741
+ />
3742
+ </Band>
3743
+ )
3744
+ },
3745
+
3746
+ session_prompt(ctx, data) {
3747
+ return (
3748
+ <Band>
3749
+ <Prompt
3750
+ sessionID={data.session_id}
3751
+ visible={data.visible}
3752
+ disabled={data.disabled}
3753
+ onSubmit={data.on_submit}
3754
+ ref={data.ref}
3755
+ placeholders={PLACEHOLDERS}
3756
+ hint={<Hint sessionID={data.session_id} />}
3757
+ right={<HostSlot name="session_prompt_right" session_id={data.session_id} />}
3758
+ />
3759
+ </Band>
3760
+ )
3761
+ },
3762
+
3763
+ sidebar_title(ctx, data) {
3764
+ return (
3765
+ <box flexDirection="column">
3766
+ <text fg={t().text}>{data.title}</text>
3767
+ {() => data.share_url
3768
+ ? <text fg={t().accent} wrapMode="none">{data.share_url}</text>
3769
+ : null}
3524
3770
  </box>
3525
3771
  )
3526
3772
  },
3527
- home_footer(ctx) {
3528
- const muted = ctx && ctx.theme ? ctx.theme.textMuted : undefined
3529
- return <text fg={muted}>${brand.name} v${VERSION}</text>
3773
+
3774
+ sidebar_content(ctx, data) {
3775
+ return <Sidebar sessionID={data.session_id} />
3530
3776
  },
3531
- // Bottom of the sidebar \u2014 this is where the engine prints its own
3532
- // name and engine version (for example "OpenCode 1.18.18"); ours replaces it.
3533
- sidebar_footer(ctx) {
3534
- const accent = ctx && ctx.theme ? ctx.theme.primary : undefined
3535
- const muted = ctx && ctx.theme ? ctx.theme.textMuted : undefined
3777
+
3778
+ // Bottom of the sidebar \u2014 where the engine prints its own name and
3779
+ // version (for example "OpenCode 1.18.18"); ours replaces it.
3780
+ sidebar_footer() {
3536
3781
  return (
3537
- <text>
3538
- <span fg={accent}>${brand.displayName}</span>
3539
- <span fg={muted}> v${VERSION}</span>
3540
- </text>
3782
+ <box flexDirection="column">
3783
+ <Rule />
3784
+ <text>
3785
+ <span fg={t().primary}>${brand.name}</span>
3786
+ <span fg={t().textMuted}> v${VERSION}</span>
3787
+ </text>
3788
+ </box>
3541
3789
  )
3542
3790
  },
3543
3791
  },
@@ -6883,6 +7131,13 @@ ${backendSessionLines(backend)}
6883
7131
 
6884
7132
  // src/lib/engineConfig.ts
6885
7133
  var PROVIDER_ID = "agentful";
7134
+ var DISABLED_ENGINE_PLUGINS = {
7135
+ "internal:sidebar-context": false,
7136
+ "internal:sidebar-lsp": false,
7137
+ "internal:sidebar-mcp": false,
7138
+ "internal:sidebar-todo": false,
7139
+ "internal:sidebar-files": false
7140
+ };
6886
7141
  var CLOUD_AGENT = "agentful-build";
6887
7142
  var LOCAL_AGENT = "local-only";
6888
7143
  var CLOUD_AGENT_PROMPT = `You are the Agentful Cloud build agent: everything you build in this directory targets deployment on Agentful Cloud via \`agentful push\`. Follow the platform contract in AGENTFUL_CLOUD.md strictly \u2014 static files only, no server process, no build-time env vars or secrets. When a push or cloud build fails, diagnose ONLY from the \`agentful push\` output and \`agentful build-status\`; quote the record and never invent causes or settings it does not show. If the user wants development that is not meant for the platform, suggest the ${LOCAL_AGENT} agent (Tab).`;
@@ -7012,7 +7267,15 @@ async function writeEngineSession(session, catalog, localProviders = {}, framewo
7012
7267
  (0, import_node_fs12.writeFileSync)(pluginPath, brandingPluginSource2());
7013
7268
  (0, import_node_fs12.writeFileSync)((0, import_node_path14.join)(configDir2, "tui.json"), JSON.stringify({
7014
7269
  theme: "agentful",
7015
- plugin: [`file://${pluginPath}`]
7270
+ plugin: [`file://${pluginPath}`],
7271
+ // The engine's own sidebar sections are plugins with fixed ids, and
7272
+ // sidebar_content composes rather than replaces — without switching them
7273
+ // off, ours would render UNDER theirs and the column would show every
7274
+ // number twice. `plugin_enabled` is keyed by plugin id and applies to the
7275
+ // built-in ones too (verified in the v1.18.18 binary); a user's own toggle
7276
+ // in the plugin manager still wins, because the engine merges its kv
7277
+ // state over this map.
7278
+ plugin_enabled: DISABLED_ENGINE_PLUGINS
7016
7279
  }, null, 2));
7017
7280
  (0, import_node_fs12.writeFileSync)((0, import_node_path14.join)(themesDir, "agentful.json"), JSON.stringify(await resolveTheme(), null, 2));
7018
7281
  for (const name of Object.keys(SLASH_COMMANDS)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentful",
3
- "version": "0.3.9",
3
+ "version": "0.4.1",
4
4
  "description": "Agentful in your terminal — local development with push-to-cloud previews",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://agentful.dev",
@@ -23,6 +23,7 @@
23
23
  "sync-starters": "node scripts/sync-starters.mjs",
24
24
  "sync-skills": "node scripts/sync-skills.mjs",
25
25
  "typecheck": "tsc --noEmit",
26
+ "check-engine": "node scripts/check-engine-contract.mjs",
26
27
  "dev": "tsx src/index.ts",
27
28
  "sync-theme": "cp src/branding/agentful-theme.json ../frontend/public/cli/theme.json",
28
29
  "test": "node --import tsx --test src/lib/*.test.ts"