feat: land native mobile foundation
This commit is contained in:
@@ -9,6 +9,7 @@ build/
|
|||||||
.next/
|
.next/
|
||||||
coverage/
|
coverage/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
.gradle-user/
|
||||||
|
|
||||||
# env/secrets
|
# env/secrets
|
||||||
.env
|
.env
|
||||||
@@ -25,6 +26,9 @@ uploads/
|
|||||||
*.db-wal
|
*.db-wal
|
||||||
logs/
|
logs/
|
||||||
|
|
||||||
|
# local agent/audit workspace
|
||||||
|
.dev/
|
||||||
|
|
||||||
# OS/editor
|
# OS/editor
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable Hermes Mobile source changes are recorded here. Entries are added during the same work loop as their implementation; release artifacts are only listed after source commit, checks, and APK upload are verified.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added native Android chat, workspace, settings, local defaults, companion health, and file editing surfaces for the beta client.
|
||||||
|
- Added `PROJECT_PLAN.md` as the project-controlled milestone plan.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated companion `/api/chat` to call `hermes chat -Q --source tool -q <prompt>`, sanitize CLI banners/help/control output, truncate large mobile replies, and return structured failures.
|
||||||
|
- Reconciled `docs/ROADMAP.md` to the current native Android beta state.
|
||||||
|
- Ignored `.gradle-user/` and `.dev/` alongside existing generated build artifacts.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed companion/mobile lint issues from missing Node globals and an unused React settings value.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- `git diff --check` — passed
|
||||||
|
- `npm run typecheck --workspace @hermes-mobile/companion` — passed
|
||||||
|
- `npm run lint --workspace @hermes-mobile/companion` — passed
|
||||||
|
- `npm run build --workspace @hermes-mobile/companion` — passed
|
||||||
|
- `npm run typecheck` — passed
|
||||||
|
- `npm run lint` — passed
|
||||||
|
- `npm run build` — passed
|
||||||
|
- `GRADLE_USER_HOME=/root/hermes-mobile/.gradle-user ./gradlew :app:assembleDebug` from `apps/mobile/android` — blocked before compilation: network socket creation is not permitted while downloading Gradle 8.11.1
|
||||||
|
|
||||||
|
## Release history
|
||||||
|
|
||||||
|
_No versioned APK releases yet._
|
||||||
|
|
||||||
|
## Entry format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [v0.1.0-beta.1] - YYYY-MM-DD
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Concise user-visible change.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Concise refinement or architectural change.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Concise defect fix.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- `command` — passed
|
||||||
|
|
||||||
|
### APK
|
||||||
|
- `hermes-mobile-v0.1.0-beta.1.apk` — Telegram release topic message <id>
|
||||||
|
```
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
# Hermes Mobile — Delivery Plan
|
||||||
|
|
||||||
|
> **Operational contract:** Codex must read this file before each work loop. Work exactly one coherent milestone at a time. Move completed items to the completed section in the same commit that implements them. Add a concise entry to `CHANGELOG.md` in the same commit. Never mark work complete without running its listed verification.
|
||||||
|
|
||||||
|
## Product guardrails
|
||||||
|
|
||||||
|
- Native Android client for upstream Hermes Agent: **Kotlin + Jetpack Compose**.
|
||||||
|
- Material 3 base, deliberately customized through a reusable design system.
|
||||||
|
- Thin client: Hermes remains the authority for auth, sessions, streaming, tool semantics, and backend behavior.
|
||||||
|
- No WebView-first app, no cloned Hermes backend, no embedded credentials.
|
||||||
|
- Adaptive, accessible, compact-but-readable UI: phone, tablet, light/dark, font scaling, IME/keyboard, long code/output must remain safe.
|
||||||
|
- Keep DTO/transport, domain/UI models, repositories, view models/state, and composables separated.
|
||||||
|
- Never commit APKs, build outputs, credentials, or generated local environment files.
|
||||||
|
|
||||||
|
## Release policy
|
||||||
|
|
||||||
|
- Start artifacts at `v0.1.0-beta.1`.
|
||||||
|
- Patch: compatible fixes/polish. Minor: a coherent feature milestone. Major: stable `v1.0.0+` only.
|
||||||
|
- A release requires a pushed source commit, passing relevant checks, a versioned APK artifact, and a Telegram release post.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Now — finish the foundation already in progress
|
||||||
|
|
||||||
|
- [ ] **F1 — Compose design system extraction**
|
||||||
|
- Extract tokens for color, spacing, typography, shape, elevation, motion, and semantic status colors.
|
||||||
|
- Implement light/dark themes with readable contrast; eliminate default-sample styling.
|
||||||
|
- Split oversized UI into clear design, feature, and reusable component packages.
|
||||||
|
- Add Compose previews: small phone, normal phone, tablet width, light, dark.
|
||||||
|
- Verification: Android compile/build plus visual preview review.
|
||||||
|
|
||||||
|
- [ ] **F2 — Polished fake-data chat proving ground**
|
||||||
|
- Session rail/list, conversation screen, context controls, keyboard-safe composer, send/cancel/retry behavior.
|
||||||
|
- Render assistant/user/system/error/streaming messages from UI models.
|
||||||
|
- Add markdown-friendly surfaces, selectable long text, code blocks with horizontal scroll + copy, compact expandable tool results.
|
||||||
|
- Cover empty/loading/reconnecting/error states without clipped/overlapping content.
|
||||||
|
- Verification: focused UI tests where practical; Android debug build.
|
||||||
|
|
||||||
|
## Beta path — upstream-connected MVP
|
||||||
|
|
||||||
|
- [ ] **B1 — Transport/domain contracts**
|
||||||
|
- Define transport DTOs and a repository interface for server profiles, authentication, sessions, messages, stream events, tool results, and artifacts.
|
||||||
|
- Map DTOs to stable UI models; fake repository powers previews/tests.
|
||||||
|
- Keep the existing companion as an optional adapter only; do not change upstream Hermes semantics.
|
||||||
|
- Verification: unit tests for mapping/state reducers; build/typecheck.
|
||||||
|
|
||||||
|
- [ ] **B2 — Connection and authentication UX**
|
||||||
|
- Server connection screen with validation, health, auth/session configuration, offline/unauthorized/retry states.
|
||||||
|
- Store sensitive values using Android-appropriate secure storage; never log secrets.
|
||||||
|
- Verification: connection-state tests and manual unhappy-path walkthrough.
|
||||||
|
|
||||||
|
- [ ] **B3 — Session list and durable chat state**
|
||||||
|
- Fetch/list/search/select/create sessions through repository contracts.
|
||||||
|
- Restore selected session and protect against duplicate sends / lost drafts.
|
||||||
|
- Verification: fake-repository UI tests; companion/integration checks where available.
|
||||||
|
|
||||||
|
- [ ] **B4 — Streaming, reconnects, and resilient long-running work**
|
||||||
|
- Stream event reducer: token deltas, tool lifecycle, final/error/cancel, reconnect/backoff.
|
||||||
|
- Preserve partial output and make recovery obvious and safe.
|
||||||
|
- Verification: deterministic fake stream tests and real upstream-compatible exercise.
|
||||||
|
|
||||||
|
- [ ] **B5 — Tool, artifact, and output experience**
|
||||||
|
- Compact tool cards with lifecycle/status, expandable output, copy/share/save affordances.
|
||||||
|
- Safe rendering for large logs and code; no unbounded nested scroll failures.
|
||||||
|
- Artifact/file handoff only where upstream capability is actually supported.
|
||||||
|
- Verification: large-output and rotation/resume checks.
|
||||||
|
|
||||||
|
## Beta hardening and release
|
||||||
|
|
||||||
|
- [ ] **R1 — Adaptive/accessibility quality pass**
|
||||||
|
- Small phone, normal phone, tablet/two-pane behavior; portrait/landscape.
|
||||||
|
- Font scaling, TalkBack semantics, contrast, touch targets, reduced motion, IME behavior.
|
||||||
|
- Verification: emulator/device matrix evidence documented in `docs/ROADMAP.md`.
|
||||||
|
|
||||||
|
- [ ] **R2 — Beta packaging and install guide**
|
||||||
|
- Version `v0.1.0-beta.1`, reproducible debug/beta APK build, install/run guide, known limitations.
|
||||||
|
- Publish source commit, APK, release notes, and verification result.
|
||||||
|
- Verification: clean install on a real device or emulator plus full required checks.
|
||||||
|
|
||||||
|
## V2 backlog
|
||||||
|
|
||||||
|
- [ ] Multiple server profiles and fast switching.
|
||||||
|
- [ ] Rich file/artifact workflows based on upstream capability.
|
||||||
|
- [ ] Notifications for task completion.
|
||||||
|
- [ ] Mature tablet/two-pane session + optional terminal/log pane.
|
||||||
|
- [ ] Offline cache/search and diagnostic export with privacy controls.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Completed
|
||||||
|
|
||||||
|
_Move finished items here with date, commit, and verification. Keep this section factual; do not claim unverified work._
|
||||||
|
|
||||||
|
- [x] **F0 — Audit and safely land existing work** — 2026-07-24, commit `da79d48` (`feat: land native mobile foundation`).
|
||||||
|
- Reviewed existing Android and companion edits; preserved the native Android chat/workspace/settings work and companion chat hardening.
|
||||||
|
- Confirmed generated build outputs are ignored and added ignores for `.gradle-user/` and `.dev/`.
|
||||||
|
- Reconciled `docs/ROADMAP.md` to the actual native Android beta state and remaining validation gaps.
|
||||||
|
- Verification: `git diff --check` passed; `npm run typecheck --workspace @hermes-mobile/companion` passed; `npm run lint --workspace @hermes-mobile/companion` passed; `npm run build --workspace @hermes-mobile/companion` passed; `npm run typecheck` passed; `npm run lint` passed; `npm run build` passed.
|
||||||
|
- Android debug build attempted with `GRADLE_USER_HOME=/root/hermes-mobile/.gradle-user ./gradlew :app:assembleDebug` from `apps/mobile/android`; blocked before compilation because network socket creation is not permitted while downloading Gradle 8.11.1.
|
||||||
|
|
||||||
|
## Work-loop checklist
|
||||||
|
|
||||||
|
Before coding:
|
||||||
|
1. Read `PROJECT_PLAN.md`, `CHANGELOG.md`, `docs/ROADMAP.md`, git status, and the prior commit.
|
||||||
|
2. Choose the single highest-value unchecked item or a narrowly scoped sub-item.
|
||||||
|
3. Inspect existing implementation before rewriting it.
|
||||||
|
|
||||||
|
Before committing:
|
||||||
|
1. Run relevant checks; record exact pass/fail scope in the changelog.
|
||||||
|
2. Update this plan: check the completed item, move it into **Completed**, and leave the next executable item unchecked.
|
||||||
|
3. Add a concise `CHANGELOG.md` entry under **Unreleased**.
|
||||||
|
4. Commit using Conventional Commits and push `origin/main`.
|
||||||
|
5. Do not claim a release without an uploaded, verified versioned APK.
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Buffer } from 'node:buffer';
|
||||||
import { timingSafeEqual } from 'node:crypto';
|
import { timingSafeEqual } from 'node:crypto';
|
||||||
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
||||||
import { authValidateResponseSchema } from '@hermes-mobile/shared';
|
import { authValidateResponseSchema } from '@hermes-mobile/shared';
|
||||||
|
|||||||
@@ -5,33 +5,123 @@ import { toSafeAbsolutePath } from '../config/paths.js';
|
|||||||
import { getHermesHealth } from '../system/hermesHealth.js';
|
import { getHermesHealth } from '../system/hermesHealth.js';
|
||||||
import { runProcess } from '../system/processes.js';
|
import { runProcess } from '../system/processes.js';
|
||||||
|
|
||||||
|
const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[;?0-9]*[ -/]*[@-~]`, 'g');
|
||||||
|
|
||||||
export async function registerChatRoutes(app: FastifyInstance, config: CompanionConfig): Promise<void> {
|
export async function registerChatRoutes(app: FastifyInstance, config: CompanionConfig): Promise<void> {
|
||||||
app.post('/api/chat', async (request, reply) => {
|
app.post('/api/chat', async (request, reply) => {
|
||||||
const body = chatRequestSchema.parse(request.body);
|
const body = chatRequestSchema.safeParse(request.body);
|
||||||
|
if (!body.success) {
|
||||||
|
return reply.code(400).send(
|
||||||
|
chatResponseSchema.parse({
|
||||||
|
reply: 'Chat prompt is required.',
|
||||||
|
exitCode: null,
|
||||||
|
stderr: undefined,
|
||||||
|
hermesAvailable: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const hermesHealth = await getHermesHealth();
|
const hermesHealth = await getHermesHealth();
|
||||||
|
|
||||||
if (!hermesHealth.cliAvailable) {
|
if (!hermesHealth.cliAvailable) {
|
||||||
return chatResponseSchema.parse({
|
return reply.send(
|
||||||
|
chatResponseSchema.parse({
|
||||||
reply: 'Hermes CLI was not found on this machine. Install Hermes or make sure `hermes` is on PATH, then retry.',
|
reply: 'Hermes CLI was not found on this machine. Install Hermes or make sure `hermes` is on PATH, then retry.',
|
||||||
exitCode: null,
|
exitCode: null,
|
||||||
stderr: undefined,
|
stderr: undefined,
|
||||||
hermesAvailable: false,
|
hermesAvailable: false,
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await runProcess(hermesHealth.cliPath ?? 'hermes', [], {
|
const result = await runProcess(hermesHealth.cliPath ?? 'hermes', ['chat', '-Q', '--source', 'tool', '-q', body.data.prompt], {
|
||||||
cwd: toSafeAbsolutePath(config.workspaceRoot),
|
cwd: toSafeAbsolutePath(config.workspaceRoot),
|
||||||
timeoutMs: 30000,
|
timeoutMs: 120000,
|
||||||
input: body.prompt,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return reply.code(result.exitCode === 0 || result.exitCode === null ? 200 : 502).send(
|
const cleanStdout = sanitizeHermesOutput(result.stdout);
|
||||||
|
const cleanStderr = sanitizeHermesOutput(result.stderr);
|
||||||
|
const openedHelp = looksLikeTopLevelHermesHelp(result.stdout) || looksLikeTopLevelHermesHelp(result.stderr);
|
||||||
|
const spawnFailed = result.exitCode === null && cleanStderr.length > 0 && cleanStdout.length === 0;
|
||||||
|
const failed = result.timedOut || openedHelp || spawnFailed || (result.exitCode !== 0 && result.exitCode !== null);
|
||||||
|
const fallback = openedHelp
|
||||||
|
? 'Hermes opened its CLI help instead of running the prompt. Restart the companion server so it uses the updated chat command.'
|
||||||
|
: failed
|
||||||
|
? `Hermes did not finish successfully${result.exitCode === null ? '' : ` (exit ${result.exitCode})`}.${result.timedOut ? ' The request timed out.' : ''}`
|
||||||
|
: 'Hermes finished without output.';
|
||||||
|
const responseText = (openedHelp ? '' : cleanStdout) || (openedHelp ? '' : cleanStderr) || fallback;
|
||||||
|
|
||||||
|
return reply.code(failed ? (result.timedOut ? 504 : 502) : 200).send(
|
||||||
chatResponseSchema.parse({
|
chatResponseSchema.parse({
|
||||||
reply: result.stdout.trim() || result.stderr.trim() || 'Hermes finished without output.',
|
reply: truncateForMobile(responseText),
|
||||||
exitCode: result.exitCode,
|
exitCode: result.exitCode,
|
||||||
stderr: result.stderr || undefined,
|
stderr: cleanStderr ? truncateForMobile(cleanStderr) : undefined,
|
||||||
hermesAvailable: true,
|
hermesAvailable: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Chat request failed.';
|
||||||
|
return reply.send(
|
||||||
|
chatResponseSchema.parse({
|
||||||
|
reply: sanitizeHermesOutput(message) || 'Chat request failed.',
|
||||||
|
exitCode: null,
|
||||||
|
stderr: undefined,
|
||||||
|
hermesAvailable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeHermesOutput(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(ansiPattern, '')
|
||||||
|
.replace(/\r/g, '')
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => !isBannerOrHelpLine(line))
|
||||||
|
.join('\n')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBannerOrHelpLine(line: string): boolean {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) return false;
|
||||||
|
if (/session_id\s*:/i.test(trimmed)) return true;
|
||||||
|
if (/[\u2500-\u257F]/u.test(trimmed)) return true;
|
||||||
|
if (/^[+\-_|/\\()[\]{}<>\s*]+$/.test(trimmed) && trimmed.length > 8) return true;
|
||||||
|
if (/^(usage:|positional arguments:|options:|examples:|for more help on a command:|hermes <command> --help)/i.test(trimmed)) return true;
|
||||||
|
if (/^\{chat,model,gateway,setup,whatsapp,login,logout,auth,status,cron,webhook,doctor,config,pairing,skills,plugins,honcho,tools,mcp,sessions,insights,claw,version,update,uninstall,acp,profile,completion\}/i.test(trimmed)) return true;
|
||||||
|
if (/^(chat|model|gateway|setup|whatsapp|login|logout|auth|status|cron|webhook|doctor|config|pairing|skills|plugins|honcho|tools|mcp|sessions|insights|claw|version|update|uninstall|acp|profile|completion)\s{2,}/i.test(trimmed)) return true;
|
||||||
|
if (/^hermes\s+(chat|setup|gateway|status|doctor|model|config|sessions|update|uninstall)\b/i.test(trimmed)) return true;
|
||||||
|
if (/^(Hermes|⚕ Hermes)\b.*(Agent|CLI|v?\d)/i.test(trimmed)) return true;
|
||||||
|
if (/^Hermes Agent - AI assistant with tool-calling capabilities$/i.test(trimmed)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeTopLevelHermesHelp(text: string): boolean {
|
||||||
|
const clean = text.replace(ansiPattern, '');
|
||||||
|
return /usage:\s+hermes\b/i.test(clean) && /Hermes Agent - AI assistant with tool-calling capabilities/i.test(clean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selfCheckSanitizer(): void {
|
||||||
|
const sample = [
|
||||||
|
'\u001B[36m╭─ ⚕ Hermes v1.2.3 ─╮\u001B[0m',
|
||||||
|
'│ session_id: abc123 │',
|
||||||
|
'╰────────────────────╯',
|
||||||
|
'',
|
||||||
|
'Final answer line.',
|
||||||
|
].join('\n');
|
||||||
|
if (sanitizeHermesOutput(sample) !== 'Final answer line.') {
|
||||||
|
throw new Error('Hermes output sanitizer self-check failed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selfCheckSanitizer();
|
||||||
|
|
||||||
|
function truncateForMobile(text: string): string {
|
||||||
|
const maxChars = 24000;
|
||||||
|
if (text.length <= maxChars) return text;
|
||||||
|
return `${text.slice(0, maxChars)}\n\n[Output truncated]`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Buffer } from 'node:buffer';
|
||||||
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
||||||
import { basename, dirname, join } from 'node:path';
|
import { basename, dirname, join } from 'node:path';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
import { clearTimeout, setTimeout } from 'node:timers';
|
||||||
|
|
||||||
export type RunProcessResult = {
|
export type RunProcessResult = {
|
||||||
stdout: string;
|
stdout: string;
|
||||||
|
|||||||
+815
-215
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ import { useCompanion } from '../../api/CompanionContext.js';
|
|||||||
import { CompanionClient, DEFAULT_COMPANION_URL } from '../../api/companionClient.js';
|
import { CompanionClient, DEFAULT_COMPANION_URL } from '../../api/companionClient.js';
|
||||||
|
|
||||||
export function SettingsScreen() {
|
export function SettingsScreen() {
|
||||||
const { client, settings, updateSettings } = useCompanion();
|
const { settings, updateSettings } = useCompanion();
|
||||||
const [baseUrl, setBaseUrl] = useState(settings.baseUrl);
|
const [baseUrl, setBaseUrl] = useState(settings.baseUrl);
|
||||||
const [accessKey, setAccessKey] = useState(settings.accessKey);
|
const [accessKey, setAccessKey] = useState(settings.accessKey);
|
||||||
const [status, setStatus] = useState('Run companion setup, paste the access key, then test the connection.');
|
const [status, setStatus] = useState('Run companion setup, paste the access key, then test the connection.');
|
||||||
|
|||||||
+28
-130
@@ -1,136 +1,34 @@
|
|||||||
# Roadmap
|
# Hermes Mobile Beta Roadmap
|
||||||
|
|
||||||
## Phase 0 — Planning Scaffold
|
## Current Milestone: Native Android Beta Chat and Connection UX
|
||||||
|
|
||||||
Status: completed.
|
Status: in progress, close to beta validation. F0 source landing is complete; Android debug build validation remains environment-blocked.
|
||||||
|
|
||||||
Deliverables:
|
Done:
|
||||||
- Repo created
|
- Native Kotlin/Compose Android shell replaces the web-first mobile path for the beta surface.
|
||||||
- Product plan
|
- Companion settings flow stores companion URL and access key locally, validates bearer auth, and surfaces health errors in-app.
|
||||||
- Architecture plan
|
- Chat UI supports conversation list/thread navigation, native composer, busy state, selectable replies, error bubbles, and new-chat reset.
|
||||||
- Design direction
|
- Companion `/api/chat` invokes Hermes through the CLI with `hermes chat -Q --source tool -q <prompt>`.
|
||||||
- Install strategy
|
- Companion chat responses sanitize terminal banners/help/control output, truncate large replies for mobile, report CLI availability, and return structured failure responses.
|
||||||
- Project structure
|
- Workspace utilities remain native: command runner, health view, file browsing, read, and write.
|
||||||
|
- Local build artifacts, Android transient build output, `.gradle-user/`, and `.dev/` are ignored.
|
||||||
|
- Companion and workspace TypeScript typecheck, build, and lint pass as of 2026-07-24.
|
||||||
|
|
||||||
## Phase 1 — Skeleton
|
Remaining for beta:
|
||||||
|
- Validate Android debug build in an environment where Gradle can fetch or reuse the Gradle 8.11.1 distribution. On 2026-07-24, `GRADLE_USER_HOME=/root/hermes-mobile/.gradle-user ./gradlew :app:assembleDebug` failed before compilation because network socket creation is blocked while downloading `https://services.gradle.org/distributions/gradle-8.11.1-all.zip`.
|
||||||
|
- Exercise a real paired device against a running companion and authenticated Hermes CLI.
|
||||||
|
- Exercise upstream-compatible transport/auth/session/streaming behavior end to end: bearer auth, health, chat request/response, session continuity expectations, streamed/final output handling, and failure recovery.
|
||||||
|
- Add or run focused Android UI validation for chat/settings flows on small and large screens.
|
||||||
|
- Confirm adaptive accessibility: font scaling, TalkBack labels, contrast, keyboard/IME behavior, and reduced-motion tolerance.
|
||||||
|
- Document beta install/run steps after the native build is verified.
|
||||||
|
|
||||||
Goal: empty but runnable monorepo.
|
## Beta Release Criteria
|
||||||
|
|
||||||
Status: in progress.
|
- Native Kotlin/Compose Android app is the shipped beta client.
|
||||||
|
- No WebView, Capacitor runtime dependency, fake backend, embedded secrets, or new external infrastructure is introduced for beta.
|
||||||
Deliverables:
|
- Companion transport and auth are upstream-compatible and exercised against the real Hermes CLI.
|
||||||
- pnpm workspace
|
- Chat/session/streaming behavior is exercised with Hermes, including success, CLI unavailable, timeout, non-zero exit, and malformed response cases.
|
||||||
- `apps/mobile` Vite React app with Capacitor Android target and PWA fallback
|
- Settings connection flow is reliable: save, validate, health, Hermes test, offline/unauthorized states, and recovery are clear.
|
||||||
- `apps/companion` Fastify server
|
- Adaptive accessibility passes on target Android devices or emulators.
|
||||||
- shared TypeScript package for event/API schemas
|
- Required checks pass: `./gradlew :app:assembleDebug` from `apps/mobile/android`, companion typecheck/build, and relevant lint.
|
||||||
- basic app shell with bottom nav
|
- Release notes clearly list remaining known beta limitations.
|
||||||
- `/api/health`
|
|
||||||
- local dev scripts
|
|
||||||
|
|
||||||
## Phase 2 — Basic Hermes Prompting
|
|
||||||
|
|
||||||
Goal: phone can send text prompt and receive final answer.
|
|
||||||
|
|
||||||
Deliverables:
|
|
||||||
- pairing/auth MVP
|
|
||||||
- Settings connect flow
|
|
||||||
- task creation endpoint
|
|
||||||
- Hermes CLI/API adapter MVP
|
|
||||||
- Ask screen sends prompt
|
|
||||||
- response display
|
|
||||||
- task history in SQLite
|
|
||||||
|
|
||||||
## Phase 3 — Realtime Activity + Tool Timeline
|
|
||||||
|
|
||||||
Goal: see Hermes work live.
|
|
||||||
|
|
||||||
Deliverables:
|
|
||||||
- event bus
|
|
||||||
- WebSocket/SSE stream
|
|
||||||
- tool timeline UI
|
|
||||||
- Python worker adapter using AIAgent callbacks if needed
|
|
||||||
- coarse/fine tool event mapping
|
|
||||||
|
|
||||||
## Phase 4 — Uploads + Voice
|
|
||||||
|
|
||||||
Goal: prompt with files and voice notes.
|
|
||||||
|
|
||||||
Deliverables:
|
|
||||||
- multipart upload endpoint
|
|
||||||
- upload tray UI
|
|
||||||
- image/archive/pdf metadata
|
|
||||||
- voice recorder UI
|
|
||||||
- local STT or Hermes STT bridge
|
|
||||||
- attach files to task prompt
|
|
||||||
|
|
||||||
## Phase 5 — Notifications
|
|
||||||
|
|
||||||
Goal: close phone and get told when done.
|
|
||||||
|
|
||||||
Deliverables:
|
|
||||||
- Native Android push path or ntfy/Web Push fallback
|
|
||||||
- PWA service worker fallback
|
|
||||||
- notification provider abstraction
|
|
||||||
- notification permission onboarding
|
|
||||||
- completion/failure/approval notifications
|
|
||||||
|
|
||||||
## Phase 6 — Cron + Approvals
|
|
||||||
|
|
||||||
Goal: mobile control plane.
|
|
||||||
|
|
||||||
Deliverables:
|
|
||||||
- cron list/run/pause/resume/remove
|
|
||||||
- cron output viewer
|
|
||||||
- approval queue UI
|
|
||||||
- companion approval API
|
|
||||||
- Hermes approval integration strategy
|
|
||||||
|
|
||||||
## Phase 7 — Installer + Release
|
|
||||||
|
|
||||||
Goal: one-liner install.
|
|
||||||
|
|
||||||
Deliverables:
|
|
||||||
- production build
|
|
||||||
- install.sh
|
|
||||||
- systemd service
|
|
||||||
- update/uninstall modes
|
|
||||||
- release artifact
|
|
||||||
- pairing URL output
|
|
||||||
|
|
||||||
## Phase 8 — Android Polish
|
|
||||||
|
|
||||||
Goal: feels native.
|
|
||||||
|
|
||||||
Deliverables:
|
|
||||||
- refined Happy-inspired UI
|
|
||||||
- animations/microinteractions
|
|
||||||
- Capacitor Android build pipeline
|
|
||||||
- adaptive icons/splash
|
|
||||||
- native status/navigation bars
|
|
||||||
- share target
|
|
||||||
- haptics
|
|
||||||
- offline shell polish
|
|
||||||
- APK release/signing flow
|
|
||||||
|
|
||||||
## Phase 9 — Deep Hermes Integration
|
|
||||||
|
|
||||||
Goal: full Hermes-native experience.
|
|
||||||
|
|
||||||
Deliverables:
|
|
||||||
- structured event stream from Hermes core or stable Python adapter
|
|
||||||
- richer session browsing
|
|
||||||
- generated file detection
|
|
||||||
- background process control
|
|
||||||
- subagent visualization
|
|
||||||
- memory/skills dashboards possibly
|
|
||||||
|
|
||||||
## Open Feature Ideas
|
|
||||||
|
|
||||||
- Quick action tiles: “Check homelab”, “Run DV status”, “Start coding task”.
|
|
||||||
- Voice-to-voice mode.
|
|
||||||
- Per-task model/toolset selection.
|
|
||||||
- “Watch mode” for long-running coding tasks.
|
|
||||||
- Home Assistant widgets/actions.
|
|
||||||
- Secure notification actions: approve/deny from notification.
|
|
||||||
- Android native share target.
|
|
||||||
- QR pairing from terminal output.
|
|
||||||
|
|||||||
@@ -28,4 +28,17 @@ export default [
|
|||||||
...tseslint.configs.recommended.rules,
|
...tseslint.configs.recommended.rules,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['apps/mobile/src/**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
document: 'readonly',
|
||||||
|
fetch: 'readonly',
|
||||||
|
HTMLFormElement: 'readonly',
|
||||||
|
localStorage: 'readonly',
|
||||||
|
RequestInit: 'readonly',
|
||||||
|
URL: 'readonly',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user