macOS: show friendly device names in Instances
This commit is contained in:
69
apps/macos/Sources/Clawdis/DeviceModelCatalog.swift
Normal file
69
apps/macos/Sources/Clawdis/DeviceModelCatalog.swift
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct DevicePresentation: Sendable {
|
||||||
|
let title: String
|
||||||
|
let symbol: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DeviceModelCatalog {
|
||||||
|
static func presentation(deviceFamily: String?, modelIdentifier: String?) -> DevicePresentation? {
|
||||||
|
let family = (deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let model = (modelIdentifier ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
let modelEntry = model.isEmpty ? nil : modelIdentifierTable[model]
|
||||||
|
let symbol = modelEntry?.symbol ?? fallbackSymbol(for: family, modelIdentifier: model)
|
||||||
|
|
||||||
|
let title = if let name = modelEntry?.name, !name.isEmpty {
|
||||||
|
name
|
||||||
|
} else if !family.isEmpty, !model.isEmpty {
|
||||||
|
"\(family) (\(model))"
|
||||||
|
} else if !family.isEmpty {
|
||||||
|
family
|
||||||
|
} else if !model.isEmpty {
|
||||||
|
model
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
if title.isEmpty { return nil }
|
||||||
|
return DevicePresentation(title: title, symbol: symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func fallbackSymbol(for familyRaw: String, modelIdentifier: String) -> String? {
|
||||||
|
let family = familyRaw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if family.isEmpty { return nil }
|
||||||
|
switch family.lowercased() {
|
||||||
|
case "ipad":
|
||||||
|
return "ipad"
|
||||||
|
case "iphone":
|
||||||
|
return "iphone"
|
||||||
|
case "mac":
|
||||||
|
return "laptopcomputer"
|
||||||
|
case "android":
|
||||||
|
// Prefer tablet glyph when we know it's an Android tablet. (No attempt to infer phone/tablet here.)
|
||||||
|
return "cpu"
|
||||||
|
case "linux":
|
||||||
|
return "cpu"
|
||||||
|
default:
|
||||||
|
return "cpu"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ModelEntry: Sendable {
|
||||||
|
let name: String
|
||||||
|
let symbol: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Friendly model names for a small set of known identifiers.
|
||||||
|
// Extend this table as needed; unknown identifiers fall back to the raw value.
|
||||||
|
private static let modelIdentifierTable: [String: ModelEntry] = [
|
||||||
|
// iPad
|
||||||
|
"iPad16,5": .init(name: "iPad Pro 11-inch (M4)", symbol: "ipad"),
|
||||||
|
"iPad16,6": .init(name: "iPad Pro 13-inch (M4)", symbol: "ipad"),
|
||||||
|
|
||||||
|
// Mac
|
||||||
|
"Mac16,6": .init(name: "MacBook Pro (14-inch, 2024)", symbol: "laptopcomputer"),
|
||||||
|
"Mac16,8": .init(name: "MacBook Pro (16-inch, 2024)", symbol: "laptopcomputer"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -70,10 +70,11 @@ struct InstancesSettings: View {
|
|||||||
if let platform = inst.platform, let prettyPlatform = self.prettyPlatform(platform) {
|
if let platform = inst.platform, let prettyPlatform = self.prettyPlatform(platform) {
|
||||||
self.label(icon: self.platformIcon(platform), text: prettyPlatform)
|
self.label(icon: self.platformIcon(platform), text: prettyPlatform)
|
||||||
}
|
}
|
||||||
if let deviceText = self.deviceDescription(inst),
|
if let device = DeviceModelCatalog.presentation(
|
||||||
let deviceIcon = self.deviceIcon(inst)
|
deviceFamily: inst.deviceFamily,
|
||||||
|
modelIdentifier: inst.modelIdentifier)
|
||||||
{
|
{
|
||||||
self.label(icon: deviceIcon, text: deviceText)
|
self.label(icon: device.symbol, text: device.title)
|
||||||
}
|
}
|
||||||
self.label(icon: "clock", text: inst.lastInputDescription)
|
self.label(icon: "clock", text: inst.lastInputDescription)
|
||||||
if let mode = inst.mode { self.label(icon: "network", text: mode) }
|
if let mode = inst.mode { self.label(icon: "network", text: mode) }
|
||||||
@@ -94,9 +95,11 @@ struct InstancesSettings: View {
|
|||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func label(icon: String, text: String) -> some View {
|
private func label(icon: String?, text: String) -> some View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
|
if let icon {
|
||||||
Image(systemName: icon).foregroundStyle(.secondary).font(.caption)
|
Image(systemName: icon).foregroundStyle(.secondary).font(.caption)
|
||||||
|
}
|
||||||
Text(text)
|
Text(text)
|
||||||
}
|
}
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
@@ -120,28 +123,6 @@ struct InstancesSettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deviceIcon(_ inst: InstanceInfo) -> String? {
|
|
||||||
let family = inst.deviceFamily?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
||||||
if family.isEmpty { return nil }
|
|
||||||
switch family.lowercased() {
|
|
||||||
case "ipad":
|
|
||||||
return "ipad"
|
|
||||||
case "iphone":
|
|
||||||
return "iphone"
|
|
||||||
case "mac":
|
|
||||||
return "laptopcomputer"
|
|
||||||
default:
|
|
||||||
return "cpu"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func deviceDescription(_ inst: InstanceInfo) -> String? {
|
|
||||||
let family = inst.deviceFamily?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
||||||
let model = inst.modelIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
||||||
if !family.isEmpty, !model.isEmpty { return "\(family) (\(model))" }
|
|
||||||
if !model.isEmpty { return model }
|
|
||||||
return family.isEmpty ? nil : family
|
|
||||||
}
|
|
||||||
private func prettyPlatform(_ raw: String) -> String? {
|
private func prettyPlatform(_ raw: String) -> String? {
|
||||||
let (prefix, version) = self.parsePlatform(raw)
|
let (prefix, version) = self.parsePlatform(raw)
|
||||||
if prefix.isEmpty { return nil }
|
if prefix.isEmpty { return nil }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-ai";
|
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-ai";
|
||||||
import type { TSchema } from "@sinclair/typebox";
|
|
||||||
import { codingTools, readTool } from "@mariozechner/pi-coding-agent";
|
import { codingTools, readTool } from "@mariozechner/pi-coding-agent";
|
||||||
|
import type { TSchema } from "@sinclair/typebox";
|
||||||
|
|
||||||
import { detectMime } from "../media/mime.js";
|
import { detectMime } from "../media/mime.js";
|
||||||
|
|
||||||
|
|||||||
@@ -949,7 +949,10 @@ export async function startGatewayServer(
|
|||||||
if (!key) {
|
if (!key) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: { code: ErrorCodes.INVALID_REQUEST, message: "key required" },
|
error: {
|
||||||
|
code: ErrorCodes.INVALID_REQUEST,
|
||||||
|
message: "key required",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1102,7 +1105,10 @@ export async function startGatewayServer(
|
|||||||
chatAbortControllers.delete(runId);
|
chatAbortControllers.delete(runId);
|
||||||
chatRunBuffers.delete(runId);
|
chatRunBuffers.delete(runId);
|
||||||
const current = chatRunSessions.get(active.sessionId);
|
const current = chatRunSessions.get(active.sessionId);
|
||||||
if (current?.clientRunId === runId && current.sessionKey === sessionKey) {
|
if (
|
||||||
|
current?.clientRunId === runId &&
|
||||||
|
current.sessionKey === sessionKey
|
||||||
|
) {
|
||||||
chatRunSessions.delete(active.sessionId);
|
chatRunSessions.delete(active.sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2126,10 +2132,13 @@ export async function startGatewayServer(
|
|||||||
: [];
|
: [];
|
||||||
const hardMax = 1000;
|
const hardMax = 1000;
|
||||||
const defaultLimit = 200;
|
const defaultLimit = 200;
|
||||||
const requested = typeof limit === "number" ? limit : defaultLimit;
|
const requested =
|
||||||
|
typeof limit === "number" ? limit : defaultLimit;
|
||||||
const max = Math.min(hardMax, requested);
|
const max = Math.min(hardMax, requested);
|
||||||
const sliced =
|
const sliced =
|
||||||
rawMessages.length > max ? rawMessages.slice(-max) : rawMessages;
|
rawMessages.length > max
|
||||||
|
? rawMessages.slice(-max)
|
||||||
|
: rawMessages;
|
||||||
const capped = capArrayByJsonBytes(
|
const capped = capArrayByJsonBytes(
|
||||||
sliced,
|
sliced,
|
||||||
MAX_CHAT_HISTORY_MESSAGES_BYTES,
|
MAX_CHAT_HISTORY_MESSAGES_BYTES,
|
||||||
@@ -2268,7 +2277,9 @@ export async function startGatewayServer(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { storePath, store, entry } = loadSessionEntry(p.sessionKey);
|
const { storePath, store, entry } = loadSessionEntry(
|
||||||
|
p.sessionKey,
|
||||||
|
);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const sessionId = entry?.sessionId ?? randomUUID();
|
const sessionId = entry?.sessionId ?? randomUUID();
|
||||||
const sessionEntry: SessionEntry = {
|
const sessionEntry: SessionEntry = {
|
||||||
@@ -2441,7 +2452,10 @@ export async function startGatewayServer(
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const p = params as { id: string; patch: Record<string, unknown> };
|
const p = params as {
|
||||||
|
id: string;
|
||||||
|
patch: Record<string, unknown>;
|
||||||
|
};
|
||||||
const job = await cron.update(
|
const job = await cron.update(
|
||||||
p.id,
|
p.id,
|
||||||
p.patch as unknown as CronJobPatch,
|
p.patch as unknown as CronJobPatch,
|
||||||
@@ -2683,7 +2697,9 @@ export async function startGatewayServer(
|
|||||||
const version =
|
const version =
|
||||||
typeof params.version === "string" ? params.version : undefined;
|
typeof params.version === "string" ? params.version : undefined;
|
||||||
const platform =
|
const platform =
|
||||||
typeof params.platform === "string" ? params.platform : undefined;
|
typeof params.platform === "string"
|
||||||
|
? params.platform
|
||||||
|
: undefined;
|
||||||
const deviceFamily =
|
const deviceFamily =
|
||||||
typeof params.deviceFamily === "string"
|
typeof params.deviceFamily === "string"
|
||||||
? params.deviceFamily
|
? params.deviceFamily
|
||||||
@@ -2725,7 +2741,8 @@ export async function startGatewayServer(
|
|||||||
normalizedReason === "heartbeat";
|
normalizedReason === "heartbeat";
|
||||||
if (!(isNodePresenceLine && looksPeriodic)) {
|
if (!(isNodePresenceLine && looksPeriodic)) {
|
||||||
const compactNodeText =
|
const compactNodeText =
|
||||||
isNodePresenceLine && (host || ip || version || mode || reason)
|
isNodePresenceLine &&
|
||||||
|
(host || ip || version || mode || reason)
|
||||||
? `Node: ${host?.trim() || "Unknown"}${ip ? ` (${ip})` : ""} · app ${version?.trim() || "unknown"} · mode ${mode?.trim() || "unknown"} · reason ${reason?.trim() || "event"}`
|
? `Node: ${host?.trim() || "Unknown"}${ip ? ` (${ip})` : ""} · app ${version?.trim() || "unknown"} · mode ${mode?.trim() || "unknown"} · reason ${reason?.trim() || "event"}`
|
||||||
: text;
|
: text;
|
||||||
enqueueSystemEvent(compactNodeText);
|
enqueueSystemEvent(compactNodeText);
|
||||||
@@ -3137,7 +3154,11 @@ export async function startGatewayServer(
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
|
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
|
||||||
dedupe.set(`send:${idem}`, { ts: Date.now(), ok: false, error });
|
dedupe.set(`send:${idem}`, {
|
||||||
|
ts: Date.now(),
|
||||||
|
ok: false,
|
||||||
|
error,
|
||||||
|
});
|
||||||
respond(false, undefined, error, {
|
respond(false, undefined, error, {
|
||||||
provider,
|
provider,
|
||||||
error: formatForLog(err),
|
error: formatForLog(err),
|
||||||
@@ -3180,7 +3201,8 @@ export async function startGatewayServer(
|
|||||||
const message = params.message.trim();
|
const message = params.message.trim();
|
||||||
|
|
||||||
const requestedSessionKey =
|
const requestedSessionKey =
|
||||||
typeof params.sessionKey === "string" && params.sessionKey.trim()
|
typeof params.sessionKey === "string" &&
|
||||||
|
params.sessionKey.trim()
|
||||||
? params.sessionKey.trim()
|
? params.sessionKey.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
let resolvedSessionId = params.sessionId?.trim() || undefined;
|
let resolvedSessionId = params.sessionId?.trim() || undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user