> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Capabilities & Gaps

> The capability matrix, the honest gaps, and why a false flag does not mean a missing feature.

A capability flag describes what an engine can **report**, not what it can do.

```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { PRAISONAI_TS_CAPABILITIES } from "praisonai-mobile/engines/praisonai-ts";

if (PRAISONAI_TS_CAPABILITIES.tools) renderToolRows();
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Engine[🧠 Engine] --> Caps[📋 capabilities]
    Caps -->|true| Render[✅ UI renders rows]
    Caps -->|false| Skip[⚠️ UI renders nothing]

    classDef engine fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef caps fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef render fill:#10B981,stroke:#7C90A0,color:#fff
    classDef skip fill:#F59E0B,stroke:#7C90A0,color:#fff

    class Engine engine
    class Caps caps
    class Render render
    class Skip skip
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Closed Gaps"
        G4[💾 Gap 4: turn write] --> C[✅ CLOSED]
        G5[⌨️ Gap 5: keyboard seed] --> C
        G6[⚠️ Gap 6: decoder refusals] --> C
        G7[🗑 delete a conversation] --> C
        G8[🏷 accessible row names] --> C
        G9[💬 conversation memory] --> C
    end

    classDef gap fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef done fill:#10B981,stroke:#7C90A0,color:#fff

    class G4,G5,G6,G7,G8,G9 gap
    class C done
```

## Quick Start

<Steps>
  <Step title="Read a capability before rendering">
    ```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    const caps = engine.capabilities;
    ```

    Capabilities are a property, so the UI decides what to render before the first token.
  </Step>

  <Step title="Print the unsupported scenarios">
    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    npm run test
    ```

    The conformance suite prints every scenario an engine cannot produce.
  </Step>
</Steps>

***

## The Capability Matrix

What each shipped engine can report.

| Capability     | `praisonai-ts` | `remote-http` |
| -------------- | :------------: | :-----------: |
| `streaming`    |        ✅       |       ✅       |
| `reasoning`    |        ❌       |       ✅       |
| `tools`        |        ❌       |       ✅       |
| `approvals`    |        ❌       |       ✅       |
| `cancellation` |        ✅       |       ✅       |
| `attachments`  |        ❌       |       ✅       |

***

## Closed Gaps

The rows kept from the old gap report so a reader arriving from an old bug can find where they went.

| Capability                                                                               | Status      | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ---------------------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| In-process engine writes through the app's session                                       | ✅ Supported | Closes Gap 4. Only `praisonai-ts` writes; `remote-http` does not. (PR #4552)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Keyboard snapshot on first paint                                                         | ✅ Supported | Closes Gap 5. Seeded from `visualViewport`, guarded against pinch-zoom. (PR #4552)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| Decoder refusals reach the transcript                                                    | ✅ Supported | Closes Gap 6. A refused frame becomes a dropped row instead of vanishing. (PR #4560)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| Native shell events (lifecycle, safe-area, Android back-gesture) and secure link opening | ✅ Supported | Three of four Tauri shell events now emit natively; `tauri-plugin-back-gesture` (in-repo) owns the Android callback and Rust arbitrates via `Gate` with a 400 ms watchdog; `tauri-plugin-opener` makes `openExternal` actually reach the OS. (PR #4742)                                                                                                                                                                                                                                                                                                                       |
| A conversation can be deleted from the device                                            | ✅ Supported | Closes the `session.remove` → `repository.remove` → `storage.remove` unwired chain and the un-decoded `delete-chat` intent. Two-tap arm/confirm; a refusal is announced; deleting the open chat clears it. See [History & Reopen](/docs/features/mobile/history-and-reopen#delete-a-conversation). (PR #4794)                                                                                                                                                                                                                                                                      |
| Transcript rows carry accessible names                                                   | ✅ Supported | `paint()` now calls `accessibleName` on insert and update; tool/approval/error rows announce status/decision/title in words. See [i18n & A11y](/docs/features/mobile/i18n-and-a11y#transcript-rows-carry-accessible-names). (PR #4794)                                                                                                                                                                                                                                                                                                                                             |
| Tool status is announced in words, not colour alone                                      | ✅ Supported | A `.tool-status` span rendered from `strings.toolStatus(row.status)`; the CSS-only `unresolved` blind spot is closed. (PR #4794)                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Chat rows show when each conversation was last used                                      | ✅ Supported | `updatedLabel` reaches the visible row and the accessible name from one string (`strings.chatUpdated`). (PR #4794)                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `TimePort.epochMs` is the wall clock; `nowMs` paces the announcer                        | ✅ Supported | `main.ts` reads the port for records and uses the monotonic clock for elapsed comparisons — see [Time & Pacing](/docs/features/mobile/time-and-pacing#the-app-now-reads-the-port-not-datenow). (PR #4794)                                                                                                                                                                                                                                                                                                                                                                          |
| The in-process engine restores conversation memory to the model                          | ✅ Supported | Before this fix, the app stored and rendered a conversation but the model saw only the latest prompt. Now `Agent.setHistory(truncateHistory(historyFor(session).messages()))` runs on every turn, bounded to `HISTORY_CHAR_BUDGET` (24,000 chars, \~6,000 tokens). Survives an app relaunch because history is read from the session store. `remote-http` deliberately not covered — server owns it by `chat_id`. See [History & Reopen → The model actually gets the conversation](/docs/features/mobile/history-and-reopen#the-model-actually-gets-the-conversation). (PR #4816) |

***

## Open Gaps

Known-open, stated rather than implied so a follow-up author can pick them up.

| Capability                                     | Status      | Notes                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ---------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Truncation notice in the UI                    | ⚠️ Deferred | When history exceeds `HISTORY_CHAR_BUDGET`, oldest turns are dropped from the model's view. The transcript on screen is unchanged, but the model cannot refer to the oldest turns and **the user is not told**. Protocol v2 has no event for "context was trimmed" and adding one is a protocol bump. `truncateHistory` already returns `dropped`, so the value a future notice would carry exists; nothing consumes it yet. |
| Local mirroring of `remote-http` conversations | ⚠️ Deferred | A chat answered by the default `remote-http` engine leaves the local session empty and has no local history to restore. Same scope note as the existing `remote-http` boundary — the server owns that conversation by `chat_id`.                                                                                                                                                                                             |

***

## Reported, not fixed

The capabilities PR #4794 audited but deliberately left unwired, named here so a follow-up author can pick them up.

| Capability                                                                                      | Status       | Notes                                                                                                                                                                                                                                                                                                                                                                                                   |
| ----------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TranscriptView.actions` and `TranscriptView.usage` are computed and read by nothing            | ⏳ Reported   | `buildTranscript` returns `{ fork, delete, retry, copy, stop }` and a formatted usage line; `publish` uses only `built.rows`. So `strings.actionFork`, `actionDelete` (on a turn — distinct from the chat-list one wired here), `actionRetry`, `actionCopy`, `usageChars`, `usageElapsed`, `usageTimeToFirstToken` render nowhere, and the `retry`/`copy` intents `intents.ts` decodes are unreachable. |
| Error-row `recovery` has no affordance                                                          | ⏳ Reported   | `recoveryFor(kind)` decides `"retry"` vs `"settings"` and reaches the DOM only as `data-recovery`, which no CSS or handler reads. The `"settings"` branch belongs to the API-key work in flight.                                                                                                                                                                                                        |
| `shell.share()`, `shell.openExternal()`, `shell.haptic()` have zero callers                     | ⏳ Reported   | Both adapters implement all three and the contract suite exercises them. There is no share affordance, and `dom.ts` renders text only, so there is no link for `openExternal` to open. Genuinely unbuilt features rather than broken ones.                                                                                                                                                              |
| `formatRelativeLocalised`, `formatCountLocalised`, `formatDate`, `formatNumber` have no callers | ⏳ Deliberate | Only `formatElapsedLocalised` is called (from `announce.ts`). `format.ts` says the view layer may localise later; only `en` ships today, so noting rather than wiring keeps a second formatter from drifting from the one the view model publishes.                                                                                                                                                     |

***

## Why `tools: false` For praisonai-ts

`praisonai-ts` executes tools normally; it just never announces them. Upstream `Agent.streamEvents()` emits a three-variant union — `text`, `finish`, `error` — and none of those carry a tool call, so the engine cannot report one.

```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
type AgentEvent =
  | { type: "text";   delta: string }
  | { type: "finish"; text: string }
  | { type: "error";  error: Error };
```

The flag describes what the engine can report. A UI that renders tool rows off a `true` flag would render nothing and look broken — and a tool call that silently failed would be indistinguishable from a normal answer, which is exactly what `tool_result.ok` was introduced to prevent.

***

## How Each Gap Closed

Gap 4 was "mechanism landed, not wired": `createSession` was called and `RunPersistence.record` was called, and nothing connected them — the two signatures did not even line up (`record(prompt, answer)` against `record(request, answer)`).

```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
// The named adapter where the two vocabularies meet.
export function persistenceFor(session: Session) {
  return { record: (request, answer) => session.record(request.prompt, answer) };
}
```

Gap 5 was "property added, snapshot not seeded": `keyboardHeightPx` was declared `= 0` and only updated by an event, so a component mounting while the keyboard was already up laid out at 0 for one frame and then jumped.

```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
let keyboardHeightPx = readKeyboardHeight(view); // seeded at construction
```

Both are verified by a positive control: reverting either makes a named test fail.

Gap 6 was also "mechanism landed, not wired": the `Dropped` type, the view-model row and the seven user-facing strings all existed with no producer. `remote-http` was the only production caller of `decodeEvent` and discarded every rejection, so a malformed frame made its tool vanish and the turn rendered as a clean answer.

```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
// The channel between the engine and the controller, neither of which can
// call the other. Refusals are drained onto the transcript per event and
// again in finally, so one beside the last frame is still shown.
export function createDropSink(): DropSink { /* note + drain */ }
```

A `carry` field fixed the cross-turn contamination: `apply(start)` no longer carries the previous turn's entire `dropped` list, so one refusal on turn 1 no longer paints every later turn as damaged. Two composition tests pin the two wiring hops — `createApp` and the real `enginesFor` — because removing the sink from `createRunController` or dropping the registry's forward each left the suite green in isolation. See [Dropped Events](/docs/features/mobile/dropped-events).

<Note>
  The `showDiagnostics` setting was removed in PraisonAI [#4646](https://github.com/MervinPraison/PraisonAI/pull/4646) because no shipping code path read it, and its `false` default described a hiding that never happened — dropped rows are rendered unconditionally. A visibility toggle will be re-added the day a settings screen consumes it.
</Note>

***

## The Unsupported Scenario

From `src/praisonai-mobile/docs/gaps.md`, the one conformance scenario `praisonai-ts` still declares unsupported. Tool scenarios and the single-approval scenario are produced and passing since upstream gained `tool_call`/`tool_result` and the dead `approval` skip guard was retired in PraisonAI PR [#4641](https://github.com/MervinPraison/PraisonAI/pull/4641).

| Scenario        | Reason                                                                                                                                             |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `two_approvals` | `ApprovalManager` gates tool execution upstream but its prompt cannot reach the event channel, so two `approval_request` events cannot be emitted. |

Each is printed on every run, so a contract that quietly shrinks is visible rather than silently green.

`approval` runs because that case only asserts that deciding an *unknown* id returns `false`, which `praisonai-ts`'s `decide()` always does — no approval channel needed. `capabilities.approvals` stays `false` because the engine still cannot *emit* an approval request, which is what `two_approvals` needs.

<Info>
  Closing the remaining gap needs an upstream change: `AgentEvent` gaining an approval variant so `praisonai-ts` can emit `approval_request` events. Until then, use `remote-http` for the two-approvals flow.
</Info>

***

## Roadmap

The in-process engine now **ships** and boots on a device: the mobile path imports `praisonai/mobile` — the package's webview-safe entry (praisonai-ts [#4720](https://github.com/MervinPraison/PraisonAI/pull/4720)) — as a lazy chunk, and `defaultEngineIdFor("tauri")` returns `praisonai-ts`. The CLI-oriented `agent/simple.ts` still carries Node-only imports (`crypto`, and `events` in `ai/tool-approval.ts`), but those are **not for the webview** — the `/mobile` entry avoids them, so they no longer block the mobile app.

The remaining device gap is the **API-key setting**: the registry declares no secret setting yet, so a first turn reaches the model layer and fails recoverably for want of a key. A key setting is the tracked follow-up. See [Engines → The known API-key gap](/docs/features/mobile/engines#the-known-api-key-gap).

**Remaining native-shell gaps** (tracked in `src/praisonai-mobile/docs/gaps.md`):

* `keyboard-height` is still never emitted natively; the TypeScript reads `visualViewport` and would take a native event as an override.
* iOS edge-swipe back is not installed.
* Haptics and share have no plugin; their bridge invokes reject and the bridge degrades.

See [Native Shell](/docs/features/mobile/native-shell) for the events that now emit.

<Note>
  **Verified reachable in PR #4794.** `shell.onBackGesture`, `shell.onInsetsChanged`, `shell.onKeyboardHeightChanged`, `shell.onLifecycleChanged`, `time.createScheduler`/`every`/`setTimer`, and the settings-registry unread-key path (already pinned by `registry.test.ts`) were audited as starting leads and found **already reachable** — no doc change needed. Recorded here so a reader does not chase them again.
</Note>

***

## Best Practices

<AccordionGroup>
  <Accordion title="Render from the capability, not from hope">
    Check `capabilities.tools` before drawing tool rows; the flag is the contract the conformance suite enforces in both directions.
  </Accordion>

  <Accordion title="Declare gaps, never fake them">
    An honest `unsupported` entry keeps the suite meaningful; a faked scenario hides a defect it exists to catch.
  </Accordion>

  <Accordion title="Switch engines to gain capabilities">
    `remote-http` speaks the full vocabulary because the desktop server already emits it.
  </Accordion>

  <Accordion title="Close a gap only when it is wired end-to-end">
    A mechanism existing is not the same as a mechanism working. Gaps 4 and 5 each landed a type or a function before the wiring, and read as closed from either end until re-audited.
  </Accordion>

  <Accordion title="Keep retired gap rows visible">
    The Gap-4 and Gap-5 rows stay in the matrix, linked to PR #4552, so a reader coming from an old bug report can find where they went rather than assuming they were dropped.
  </Accordion>

  <Accordion title="State scope so it is not read later as an oversight">
    The remote-http exclusion is deliberate. Documenting it up front stops a future reader from filing the empty local session as a regression.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Agent Engine Port" icon="plug" href="/docs/features/mobile/engines">
    The port and its conformance harness.
  </Card>

  <Card title="The 11 Events" icon="network-wired" href="/docs/features/mobile/protocol">
    The full event vocabulary.
  </Card>

  <Card title="Mobile Engines" icon="microchip" href="/docs/features/mobile/engines">
    Which engine owns the write.
  </Card>

  <Card title="Shell & Adapters" icon="mobile-screen" href="/docs/features/mobile/shell-and-adapters">
    The keyboard snapshot and its guard.
  </Card>

  <Card title="Dropped Events" icon="triangle-exclamation" href="/docs/features/mobile/dropped-events">
    The closed decode-rejection gap, end to end.
  </Card>
</CardGroup>
