feat(mobile): v1.8 tab switcher + branch indicator + git_status tool

Mobile header is now two rows. Row 1: hamburger | project · branch
indicator (live via GET /api/projects/:id/git, 30s poll) | ModelPicker |
FolderTree. Row 2: pane-switcher pill (hand-rolled BottomSheet) +
NewPaneMenu. Chat-within-pane navigation hidden on mobile; users switch
panes via the sheet. Cross-tab status sync via chat_status frames
published from inference.ts at working/idle/error transitions; StatusDot
component renders amber-pulse/green/red/gray on each pane row and on
desktop ChatTabBar tabs. Level 1 git awareness exposes a read-only
git_status tool to the model, backed by services/git_meta.ts (execFile
+ 2s timeout + 30s cache). Workspace.tsx now receives panes/chats hooks
as props (hoisted into Session.tsx) so the header pill shares state
with the pane grid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 20:07:53 +00:00
parent 92bd3b1cdf
commit 2bce4d85fa
19 changed files with 1217 additions and 622 deletions

View File

@@ -9,6 +9,7 @@ import type { Project, AvailableProject } from '../types/api.js';
import { resolveProjectRoot, PathScopeError } from '../services/path_guard.js';
import { listDir, viewFile } from '../services/file_ops.js';
import { getProjectFiles } from '../services/file_index.js';
import { getGitMeta } from '../services/git_meta.js';
import {
bootstrapProject,
BootstrapNameError,
@@ -381,6 +382,38 @@ export function registerProjectRoutes(
}
);
// GET /api/projects/:id/git
// v1.8 mobile-tabs: feeds the header branch indicator and is the same
// resolver the model's git_status tool uses. Returns 200 with branch=null
// for non-git directories (not 404) so the UI can degrade gracefully.
app.get<{ Params: { id: string } }>(
'/api/projects/:id/git',
async (req, reply) => {
const { id } = req.params;
const rows = await sql<Project[]>`
SELECT id, name, path, added_at, last_session_id, status, gitea_remote
FROM projects WHERE id = ${id}
`;
if (rows.length === 0) {
reply.code(404);
return { error: 'not found' };
}
const project = rows[0]!;
let projectRoot: string;
try {
projectRoot = await resolveProjectRoot(project.path);
} catch (err) {
if (err instanceof PathScopeError) {
reply.code(404);
return { error: err.message };
}
throw err;
}
const meta = await getGitMeta(projectRoot);
return meta ?? { branch: null, is_dirty: false, ahead: 0, behind: 0 };
}
);
// GET /api/projects/:id/files
app.get<{ Params: { id: string } }>(
'/api/projects/:id/files',